diff options
Diffstat (limited to 'src/main')
315 files changed, 26030 insertions, 0 deletions
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7bde645f --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="eu.siacs.conversations"> + + <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" /> + <uses-permission android:name="android.permission.READ_PROFILE" /> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + <uses-permission android:name="android.permission.VIBRATE" /> + + <application + android:allowBackup="true" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + tools:replace="android:label" + android:theme="@style/ConversationsTheme" > + <service android:name="eu.siacs.conversations.services.XmppConnectionService" /> + + <receiver android:name="eu.siacs.conversations.services.EventReceiver" > + <intent-filter> + <action android:name="android.intent.action.BOOT_COMPLETED" /> + <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> + <action android:name="android.intent.action.ACTION_SHUTDOWN" /> + </intent-filter> + </receiver> + + <activity + android:name="eu.siacs.conversations.ui.ConversationActivity" + android:label="@string/title_activity_conversations" + android:launchMode="singleTask" + android:windowSoftInputMode="stateHidden" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + <activity + android:name="eu.siacs.conversations.ui.StartConversationActivity" + android:configChanges="orientation|screenSize" + android:label="@string/title_activity_start_conversation" + android:logo="@drawable/ic_activity" > + <intent-filter> + <action android:name="android.intent.action.SENDTO" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:scheme="imto" /> + <data android:host="jabber" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + + <data android:scheme="xmpp" /> + </intent-filter> + </activity> + <activity + android:name="eu.siacs.conversations.ui.SettingsActivity" + android:label="@string/title_activity_settings" > + </activity> + <activity + android:name="eu.siacs.conversations.ui.ChooseContactActivity" + android:label="@string/title_activity_choose_contact" > + </activity> + <activity + android:name="eu.siacs.conversations.ui.ManageAccountActivity" + android:configChanges="orientation|screenSize" + android:label="@string/title_activity_manage_accounts" > + </activity> + <activity + android:name="eu.siacs.conversations.ui.EditAccountActivity" + android:windowSoftInputMode="stateHidden|adjustResize" > + </activity> + <activity + android:name="eu.siacs.conversations.ui.ConferenceDetailsActivity" + android:label="@string/title_activity_conference_details" + android:windowSoftInputMode="stateHidden" > + </activity> + <activity + android:name="eu.siacs.conversations.ui.ContactDetailsActivity" + android:label="@string/title_activity_contact_details" + android:windowSoftInputMode="stateHidden" > + </activity> + <activity + android:name="eu.siacs.conversations.ui.PublishProfilePictureActivity" + android:label="@string/mgmt_account_publish_avatar" + android:windowSoftInputMode="stateHidden" > + </activity> + <activity + android:name="eu.siacs.conversations.ui.ShareWithActivity" + android:label="@string/title_activity_conversations" > + <intent-filter> + <action android:name="android.intent.action.SEND" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:mimeType="text/plain" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:mimeType="image/*" /> + </intent-filter> + </activity> + <activity android:name="de.duenndns.ssl.MemorizingActivity" /> + </application> + +</manifest> diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java new file mode 100644 index 00000000..7dd5a799 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -0,0 +1,26 @@ +package eu.siacs.conversations; + +import android.graphics.Bitmap; + +public final class Config { + + public static final String LOGTAG = "conversations"; + + public static final int PING_MAX_INTERVAL = 300; + public static final int PING_MIN_INTERVAL = 30; + public static final int PING_TIMEOUT = 10; + public static final int CONNECT_TIMEOUT = 90; + public static final int CARBON_GRACE_PERIOD = 60; + public static final int MINI_GRACE_PERIOD = 750; + + public static final int AVATAR_SIZE = 192; + public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP; + + public static final int MESSAGE_MERGE_WINDOW = 20; + + public static final boolean PARSE_EMOTICONS = false; + + private Config() { + + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java new file mode 100644 index 00000000..e0bd0e79 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java @@ -0,0 +1,231 @@ +package eu.siacs.conversations.crypto; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +import org.json.JSONException; +import org.json.JSONObject; + +import android.util.Log; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +import net.java.otr4j.OtrEngineHost; +import net.java.otr4j.OtrException; +import net.java.otr4j.OtrPolicy; +import net.java.otr4j.OtrPolicyImpl; +import net.java.otr4j.session.InstanceTag; +import net.java.otr4j.session.SessionID; + +public class OtrEngine implements OtrEngineHost { + + private Account account; + private OtrPolicy otrPolicy; + private KeyPair keyPair; + private XmppConnectionService mXmppConnectionService; + + public OtrEngine(XmppConnectionService service, Account account) { + this.account = account; + this.otrPolicy = new OtrPolicyImpl(); + this.otrPolicy.setAllowV1(false); + this.otrPolicy.setAllowV2(true); + this.otrPolicy.setAllowV3(true); + this.keyPair = loadKey(account.getKeys()); + this.mXmppConnectionService = service; + } + + private KeyPair loadKey(JSONObject keys) { + if (keys == null) { + return null; + } + try { + BigInteger x = new BigInteger(keys.getString("otr_x"), 16); + BigInteger y = new BigInteger(keys.getString("otr_y"), 16); + BigInteger p = new BigInteger(keys.getString("otr_p"), 16); + BigInteger q = new BigInteger(keys.getString("otr_q"), 16); + BigInteger g = new BigInteger(keys.getString("otr_g"), 16); + KeyFactory keyFactory = KeyFactory.getInstance("DSA"); + DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g); + DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g); + PublicKey publicKey = keyFactory.generatePublic(pubKeySpec); + PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); + return new KeyPair(publicKey, privateKey); + } catch (JSONException e) { + return null; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (InvalidKeySpecException e) { + return null; + } + } + + private void saveKey() { + PublicKey publicKey = keyPair.getPublic(); + PrivateKey privateKey = keyPair.getPrivate(); + KeyFactory keyFactory; + try { + keyFactory = KeyFactory.getInstance("DSA"); + DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec( + privateKey, DSAPrivateKeySpec.class); + DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, + DSAPublicKeySpec.class); + this.account.setKey("otr_x", privateKeySpec.getX().toString(16)); + this.account.setKey("otr_g", privateKeySpec.getG().toString(16)); + this.account.setKey("otr_p", privateKeySpec.getP().toString(16)); + this.account.setKey("otr_q", privateKeySpec.getQ().toString(16)); + this.account.setKey("otr_y", publicKeySpec.getY().toString(16)); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } + + } + + @Override + public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void finishedSessionMessage(SessionID arg0, String arg1) + throws OtrException { + + } + + @Override + public String getFallbackMessage(SessionID arg0) { + return "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that"; + } + + @Override + public byte[] getLocalFingerprintRaw(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + public PublicKey getPublicKey() { + if (this.keyPair == null) { + return null; + } + return this.keyPair.getPublic(); + } + + @Override + public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { + if (this.keyPair == null) { + KeyPairGenerator kg; + try { + kg = KeyPairGenerator.getInstance("DSA"); + this.keyPair = kg.genKeyPair(); + this.saveKey(); + mXmppConnectionService.databaseBackend.updateAccount(account); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, + "error generating key pair " + e.getMessage()); + } + } + return this.keyPair; + } + + @Override + public String getReplyForUnreadableMessage(SessionID arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public OtrPolicy getSessionPolicy(SessionID arg0) { + return otrPolicy; + } + + @Override + public void injectMessage(SessionID session, String body) + throws OtrException { + MessagePacket packet = new MessagePacket(); + packet.setFrom(account.getFullJid()); + if (session.getUserID().isEmpty()) { + packet.setTo(session.getAccountID()); + } else { + packet.setTo(session.getAccountID() + "/" + session.getUserID()); + } + packet.setBody(body); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setType(MessagePacket.TYPE_CHAT); + account.getXmppConnection().sendMessagePacket(packet); + } + + @Override + public void messageFromAnotherInstanceReceived(SessionID id) { + Log.d(Config.LOGTAG, + "unreadable message received from " + id.getAccountID()); + } + + @Override + public void multipleInstancesDetected(SessionID arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void requireEncryptedMessage(SessionID arg0, String arg1) + throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void showError(SessionID arg0, String arg1) throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void smpAborted(SessionID arg0) throws OtrException { + // TODO Auto-generated method stub + + } + + @Override + public void smpError(SessionID arg0, int arg1, boolean arg2) + throws OtrException { + throw new OtrException(new Exception("smp error")); + } + + @Override + public void unencryptedMessageReceived(SessionID arg0, String arg1) + throws OtrException { + throw new OtrException(new Exception("unencrypted message received")); + } + + @Override + public void unreadableMessageReceived(SessionID arg0) throws OtrException { + throw new OtrException(new Exception("unreadable message received")); + } + + @Override + public void unverify(SessionID arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void verify(SessionID arg0, String arg1, boolean arg2) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java new file mode 100644 index 00000000..9a2b4a11 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -0,0 +1,394 @@ +package eu.siacs.conversations.crypto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; + +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.UiCallback; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.net.Uri; + +public class PgpEngine { + private OpenPgpApi api; + private XmppConnectionService mXmppConnectionService; + + public PgpEngine(OpenPgpApi api, XmppConnectionService service) { + this.api = api; + this.mXmppConnectionService = service; + } + + public void decrypt(final Message message, + final UiCallback<Message> callback) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid()); + if (message.getType() == Message.TYPE_TEXT) { + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + message.setBody(os.toString()); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + if (message.trusted() && message.bodyContainsDownloadable()) { + mXmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + } + callback.success(message); + } + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + return; + } + + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + return; + default: + return; + } + } + }); + } else if (message.getType() == Message.TYPE_IMAGE) { + try { + final DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + final DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + outputFile.createNewFile(); + InputStream is = new FileInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + URL url = message.getImageParams().url; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile( + outputFile.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + if (url == null) { + message.setBody(Long.toString(outputFile + .getSize()) + + '|' + + imageWidth + + '|' + + imageHeight); + } else { + message.setBody(url.toString() + "|" + + Long.toString(outputFile.getSize()) + + '|' + imageWidth + '|' + imageHeight); + } + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + PgpEngine.this.mXmppConnectionService + .updateMessage(message); + inputFile.delete(); + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(outputFile)); + mXmppConnectionService.sendBroadcast(intent); + callback.success(message); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + return; + default: + return; + } + } + }); + } catch (FileNotFoundException e) { + callback.error(R.string.error_decrypting_file, message); + } catch (IOException e) { + callback.error(R.string.error_decrypting_file, message); + } + + } + } + + public void encrypt(final Message message, + final UiCallback<Message> callback) { + + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_ENCRYPT); + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { + long[] keys = { message.getConversation().getContact() + .getPgpKeyId() }; + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys); + } else { + params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, message.getConversation() + .getMucOptions().getPgpKeyIds()); + } + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message + .getConversation().getAccount().getJid()); + + if (message.getType() == Message.TYPE_TEXT) { + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + + InputStream is = new ByteArrayInputStream(message.getBody() + .getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + try { + os.flush(); + StringBuilder encryptedMessageBody = new StringBuilder(); + String[] lines = os.toString().split("\n"); + for (int i = 2; i < lines.length - 1; ++i) { + if (!lines[i].contains("Version")) { + encryptedMessageBody.append(lines[i].trim()); + } + } + message.setEncryptedBody(encryptedMessageBody + .toString()); + callback.success(message); + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + } + + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + break; + } + } + }); + } else if (message.getType() == Message.TYPE_IMAGE) { + try { + DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + outputFile.createNewFile(); + InputStream is = new FileInputStream(inputFile); + OutputStream os = new FileOutputStream(outputFile); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + callback.success(message); + break; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried( + (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + message); + break; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, message); + break; + } + } + }); + } catch (FileNotFoundException e) { + callback.error(R.string.openpgp_error, message); + return; + } catch (IOException e) { + callback.error(R.string.openpgp_error, message); + return; + } + } + } + + public long fetchKeyId(Account account, String status, String signature) { + if ((signature == null) || (api == null)) { + return 0; + } + if (status == null) { + status = ""; + } + StringBuilder pgpSig = new StringBuilder(); + pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(status); + pgpSig.append('\n'); + pgpSig.append("-----BEGIN PGP SIGNATURE-----"); + pgpSig.append('\n'); + pgpSig.append('\n'); + pgpSig.append(signature.replace("\n", "").trim()); + pgpSig.append('\n'); + pgpSig.append("-----END PGP SIGNATURE-----"); + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes()); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Intent result = api.executeApi(params, is, os); + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, + OpenPgpApi.RESULT_CODE_ERROR)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + OpenPgpSignatureResult sigResult = result + .getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); + if (sigResult != null) { + return sigResult.getKeyId(); + } else { + return 0; + } + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + return 0; + case OpenPgpApi.RESULT_CODE_ERROR: + return 0; + } + return 0; + } + + public void generateSignature(final Account account, String status, + final UiCallback<Account> callback) { + Intent params = new Intent(); + params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); + params.setAction(OpenPgpApi.ACTION_SIGN); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + InputStream is = new ByteArrayInputStream(status.getBytes()); + final OutputStream os = new ByteArrayOutputStream(); + api.executeApiAsync(params, is, os, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + StringBuilder signatureBuilder = new StringBuilder(); + try { + os.flush(); + String[] lines = os.toString().split("\n"); + boolean sig = false; + for (String line : lines) { + if (sig) { + if (line.contains("END PGP SIGNATURE")) { + sig = false; + } else { + if (!line.contains("Version")) { + signatureBuilder.append(line.trim()); + } + } + } + if (line.contains("BEGIN PGP SIGNATURE")) { + sig = true; + } + } + } catch (IOException e) { + callback.error(R.string.openpgp_error, account); + return; + } + account.setKey("pgp_signature", signatureBuilder.toString()); + callback.success(account); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + account); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, account); + return; + } + } + }); + } + + public void hasKey(final Contact contact, final UiCallback<Contact> callback) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); + api.executeApiAsync(params, null, null, new IOpenPgpCallback() { + + @Override + public void onReturn(Intent result) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) { + case OpenPgpApi.RESULT_CODE_SUCCESS: + callback.success(contact); + return; + case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: + callback.userInputRequried((PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT), + contact); + return; + case OpenPgpApi.RESULT_CODE_ERROR: + callback.error(R.string.openpgp_error, contact); + return; + } + } + }); + } + + public PendingIntent getIntentForKey(Contact contact) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId()); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, contact.getAccount() + .getJid()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } + + public PendingIntent getIntentForKey(Account account, long pgpKeyId) { + Intent params = new Intent(); + params.setAction(OpenPgpApi.ACTION_GET_KEY); + params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId); + params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, account.getJid()); + Intent result = api.executeApi(params, null, null); + return (PendingIntent) result + .getParcelableExtra(OpenPgpApi.RESULT_INTENT); + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java new file mode 100644 index 00000000..92b8a729 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java @@ -0,0 +1,21 @@ +package eu.siacs.conversations.entities; + +import android.content.ContentValues; + +public abstract class AbstractEntity { + + public static final String UUID = "uuid"; + + protected String uuid; + + public String getUuid() { + return this.uuid; + } + + public abstract ContentValues getContentValues(); + + public boolean equals(AbstractEntity entity) { + return this.getUuid().equals(entity.getUuid()); + } + +} diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java new file mode 100644 index 00000000..80a9d62f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -0,0 +1,399 @@ +package eu.siacs.conversations.entities; + +import java.security.interfaces.DSAPublicKey; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CopyOnWriteArrayList; + +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; + +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.OtrEngine; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.XmppConnection; +import android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; + +public class Account extends AbstractEntity { + + public static final String TABLENAME = "accounts"; + + public static final String USERNAME = "username"; + public static final String SERVER = "server"; + public static final String PASSWORD = "password"; + public static final String OPTIONS = "options"; + public static final String ROSTERVERSION = "rosterversion"; + public static final String KEYS = "keys"; + public static final String AVATAR = "avatar"; + + public static final int OPTION_USETLS = 0; + public static final int OPTION_DISABLED = 1; + public static final int OPTION_REGISTER = 2; + public static final int OPTION_USECOMPRESSION = 3; + + public static final int STATUS_CONNECTING = 0; + public static final int STATUS_DISABLED = -2; + public static final int STATUS_OFFLINE = -1; + public static final int STATUS_ONLINE = 1; + public static final int STATUS_NO_INTERNET = 2; + public static final int STATUS_UNAUTHORIZED = 3; + public static final int STATUS_SERVER_NOT_FOUND = 5; + + public static final int STATUS_REGISTRATION_FAILED = 7; + public static final int STATUS_REGISTRATION_CONFLICT = 8; + public static final int STATUS_REGISTRATION_SUCCESSFULL = 9; + public static final int STATUS_REGISTRATION_NOT_SUPPORTED = 10; + + protected String username; + protected String server; + protected String password; + protected int options = 0; + protected String rosterVersion; + protected String resource = "mobile"; + protected int status = -1; + protected JSONObject keys = new JSONObject(); + protected String avatar; + + protected boolean online = false; + + private OtrEngine otrEngine = null; + private XmppConnection xmppConnection = null; + private Presences presences = new Presences(); + private long mEndGracePeriod = 0L; + private String otrFingerprint; + private Roster roster = null; + + private List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>(); + public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<Conversation>(); + public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<Conversation>(); + + public Account() { + this.uuid = "0"; + } + + public Account(String username, String server, String password) { + this(java.util.UUID.randomUUID().toString(), username, server, + password, 0, null, "", null); + } + + public Account(String uuid, String username, String server, + String password, int options, String rosterVersion, String keys, + String avatar) { + this.uuid = uuid; + this.username = username; + this.server = server; + this.password = password; + this.options = options; + this.rosterVersion = rosterVersion; + try { + this.keys = new JSONObject(keys); + } catch (JSONException e) { + + } + this.avatar = avatar; + } + + public boolean isOptionSet(int option) { + return ((options & (1 << option)) != 0); + } + + public void setOption(int option, boolean value) { + if (value) { + this.options |= 1 << option; + } else { + this.options &= ~(1 << option); + } + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getServer() { + return server; + } + + public void setServer(String server) { + this.server = server; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getStatus() { + if (isOptionSet(OPTION_DISABLED)) { + return STATUS_DISABLED; + } else { + return this.status; + } + } + + public boolean errorStatus() { + int s = getStatus(); + return (s == STATUS_REGISTRATION_FAILED + || s == STATUS_REGISTRATION_CONFLICT + || s == STATUS_REGISTRATION_NOT_SUPPORTED + || s == STATUS_SERVER_NOT_FOUND || s == STATUS_UNAUTHORIZED); + } + + public boolean hasErrorStatus() { + if (getXmppConnection() == null) { + return false; + } else { + return getStatus() > STATUS_NO_INTERNET + && (getXmppConnection().getAttempt() >= 2); + } + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getResource() { + return this.resource; + } + + public String getJid() { + return username.toLowerCase(Locale.getDefault()) + "@" + + server.toLowerCase(Locale.getDefault()); + } + + public JSONObject getKeys() { + return keys; + } + + public String getSSLFingerprint() { + if (keys.has("ssl_cert")) { + try { + return keys.getString("ssl_cert"); + } catch (JSONException e) { + return null; + } + } else { + return null; + } + } + + public void setSSLCertFingerprint(String fingerprint) { + this.setKey("ssl_cert", fingerprint); + } + + public boolean setKey(String keyName, String keyValue) { + try { + this.keys.put(keyName, keyValue); + return true; + } catch (JSONException e) { + return false; + } + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(USERNAME, username); + values.put(SERVER, server); + values.put(PASSWORD, password); + values.put(OPTIONS, options); + values.put(KEYS, this.keys.toString()); + values.put(ROSTERVERSION, rosterVersion); + values.put(AVATAR, avatar); + return values; + } + + public static Account fromCursor(Cursor cursor) { + return new Account(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(USERNAME)), + cursor.getString(cursor.getColumnIndex(SERVER)), + cursor.getString(cursor.getColumnIndex(PASSWORD)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), + cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), + cursor.getString(cursor.getColumnIndex(KEYS)), + cursor.getString(cursor.getColumnIndex(AVATAR))); + } + + public OtrEngine getOtrEngine(XmppConnectionService context) { + if (otrEngine == null) { + otrEngine = new OtrEngine(context, this); + } + return this.otrEngine; + } + + public XmppConnection getXmppConnection() { + return this.xmppConnection; + } + + public void setXmppConnection(XmppConnection connection) { + this.xmppConnection = connection; + } + + public String getFullJid() { + return this.getJid() + "/" + this.resource; + } + + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + DSAPublicKey pubkey = (DSAPublicKey) this.otrEngine + .getPublicKey(); + if (pubkey == null) { + return null; + } + StringBuilder builder = new StringBuilder( + new OtrCryptoEngineImpl().getFingerprint(pubkey)); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + this.otrFingerprint = builder.toString(); + } catch (OtrCryptoException e) { + + } + } + return this.otrFingerprint; + } + + public String getRosterVersion() { + if (this.rosterVersion == null) { + return ""; + } else { + return this.rosterVersion; + } + } + + public void setRosterVersion(String version) { + this.rosterVersion = version; + } + + public String getOtrFingerprint(XmppConnectionService service) { + this.getOtrEngine(service); + return this.getOtrFingerprint(); + } + + public void updatePresence(String resource, int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(String resource) { + this.presences.removePresence(resource); + } + + public void clearPresences() { + this.presences = new Presences(); + } + + public int countPresences() { + return this.presences.size(); + } + + public String getPgpSignature() { + if (keys.has("pgp_signature")) { + try { + return keys.getString("pgp_signature"); + } catch (JSONException e) { + return null; + } + } else { + return null; + } + } + + public Roster getRoster() { + if (this.roster == null) { + this.roster = new Roster(this); + } + return this.roster; + } + + public void setBookmarks(List<Bookmark> bookmarks) { + this.bookmarks = bookmarks; + } + + public List<Bookmark> getBookmarks() { + return this.bookmarks; + } + + public boolean hasBookmarkFor(String conferenceJid) { + for (Bookmark bmark : this.bookmarks) { + if (bmark.getJid().equals(conferenceJid)) { + return true; + } + } + return false; + } + + public boolean setAvatar(String filename) { + if (this.avatar != null && this.avatar.equals(filename)) { + return false; + } else { + this.avatar = filename; + return true; + } + } + + public String getAvatar() { + return this.avatar; + } + + public int getReadableStatusId() { + switch (getStatus()) { + + case Account.STATUS_DISABLED: + return R.string.account_status_disabled; + case Account.STATUS_ONLINE: + return R.string.account_status_online; + case Account.STATUS_CONNECTING: + return R.string.account_status_connecting; + case Account.STATUS_OFFLINE: + return R.string.account_status_offline; + case Account.STATUS_UNAUTHORIZED: + return R.string.account_status_unauthorized; + case Account.STATUS_SERVER_NOT_FOUND: + return R.string.account_status_not_found; + case Account.STATUS_NO_INTERNET: + return R.string.account_status_no_internet; + case Account.STATUS_REGISTRATION_FAILED: + return R.string.account_status_regis_fail; + case Account.STATUS_REGISTRATION_CONFLICT: + return R.string.account_status_regis_conflict; + case Account.STATUS_REGISTRATION_SUCCESSFULL: + return R.string.account_status_regis_success; + case Account.STATUS_REGISTRATION_NOT_SUPPORTED: + return R.string.account_status_regis_not_sup; + default: + return R.string.account_status_unknown; + } + } + + public void activateGracePeriod() { + this.mEndGracePeriod = SystemClock.elapsedRealtime() + + (Config.CARBON_GRACE_PERIOD * 1000); + } + + public void deactivateGracePeriod() { + this.mEndGracePeriod = 0L; + } + + public boolean inGracePeriod() { + return SystemClock.elapsedRealtime() < this.mEndGracePeriod; + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java new file mode 100644 index 00000000..dd9e805c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -0,0 +1,137 @@ +package eu.siacs.conversations.entities; + +import java.util.Locale; + +import eu.siacs.conversations.xml.Element; + +public class Bookmark extends Element implements ListItem { + + private Account account; + private Conversation mJoinedConversation; + + public Bookmark(Account account, String jid) { + super("conference"); + this.setAttribute("jid", jid); + this.account = account; + } + + private Bookmark(Account account) { + super("conference"); + this.account = account; + } + + public static Bookmark parse(Element element, Account account) { + Bookmark bookmark = new Bookmark(account); + bookmark.setAttributes(element.getAttributes()); + bookmark.setChildren(element.getChildren()); + return bookmark; + } + + public void setAutojoin(boolean autojoin) { + if (autojoin) { + this.setAttribute("autojoin", "true"); + } else { + this.setAttribute("autojoin", "false"); + } + } + + public void setName(String name) { + this.name = name; + } + + public void setNick(String nick) { + Element element = this.findChild("nick"); + if (element == null) { + element = this.addChild("nick"); + } + element.setContent(nick); + } + + public void setPassword(String password) { + Element element = this.findChild("password"); + if (element != null) { + element.setContent(password); + } + } + + @Override + public int compareTo(ListItem another) { + return this.getDisplayName().compareToIgnoreCase( + another.getDisplayName()); + } + + @Override + public String getDisplayName() { + if (this.mJoinedConversation != null + && (this.mJoinedConversation.getMucOptions().getSubject() != null)) { + return this.mJoinedConversation.getMucOptions().getSubject(); + } else if (getName() != null) { + return getName(); + } else { + return this.getJid().split("@")[0]; + } + } + + @Override + public String getJid() { + String jid = this.getAttribute("jid"); + if (jid != null) { + return jid.toLowerCase(Locale.US); + } else { + return null; + } + } + + public String getNick() { + Element nick = this.findChild("nick"); + if (nick != null) { + return nick.getContent(); + } else { + return null; + } + } + + public boolean autojoin() { + String autojoin = this.getAttribute("autojoin"); + return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin + .equalsIgnoreCase("1"))); + } + + public String getPassword() { + Element password = this.findChild("password"); + if (password != null) { + return password.getContent(); + } else { + return null; + } + } + + public boolean match(String needle) { + return needle == null + || getJid().contains(needle.toLowerCase(Locale.US)) + || getDisplayName().toLowerCase(Locale.US).contains( + needle.toLowerCase(Locale.US)); + } + + public Account getAccount() { + return this.account; + } + + public void setConversation(Conversation conversation) { + this.mJoinedConversation = conversation; + } + + public Conversation getConversation() { + return this.mJoinedConversation; + } + + public String getName() { + return this.getAttribute("name"); + } + + public void unregisterConversation() { + if (this.mJoinedConversation != null) { + this.mJoinedConversation.deregisterWithBookmark(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java new file mode 100644 index 00000000..60c31a42 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -0,0 +1,367 @@ +package eu.siacs.conversations.entities; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.xml.Element; +import android.content.ContentValues; +import android.database.Cursor; + +public class Contact implements ListItem { + public static final String TABLENAME = "contacts"; + + public static final String SYSTEMNAME = "systemname"; + public static final String SERVERNAME = "servername"; + public static final String JID = "jid"; + public static final String OPTIONS = "options"; + public static final String SYSTEMACCOUNT = "systemaccount"; + public static final String PHOTOURI = "photouri"; + public static final String KEYS = "pgpkey"; + public static final String ACCOUNT = "accountUuid"; + public static final String AVATAR = "avatar"; + + protected String accountUuid; + protected String systemName; + protected String serverName; + protected String presenceName; + protected String jid; + protected int subscription = 0; + protected String systemAccount; + protected String photoUri; + protected String avatar; + protected JSONObject keys = new JSONObject(); + protected Presences presences = new Presences(); + + protected Account account; + + protected boolean inRoster = true; + + public Lastseen lastseen = new Lastseen(); + + public Contact(String account, String systemName, String serverName, + String jid, int subscription, String photoUri, + String systemAccount, String keys, String avatar) { + this.accountUuid = account; + this.systemName = systemName; + this.serverName = serverName; + this.jid = jid; + this.subscription = subscription; + this.photoUri = photoUri; + this.systemAccount = systemAccount; + if (keys == null) { + keys = ""; + } + try { + this.keys = new JSONObject(keys); + } catch (JSONException e) { + this.keys = new JSONObject(); + } + this.avatar = avatar; + } + + public Contact(String jid) { + this.jid = jid; + } + + public String getDisplayName() { + if (this.systemName != null) { + return this.systemName; + } else if (this.serverName != null) { + return this.serverName; + } else if (this.presenceName != null) { + return this.presenceName; + } else { + return this.jid.split("@")[0]; + } + } + + public String getProfilePhoto() { + return this.photoUri; + } + + public String getJid() { + return this.jid.toLowerCase(Locale.getDefault()); + } + + public boolean match(String needle) { + return needle == null + || jid.contains(needle.toLowerCase()) + || getDisplayName().toLowerCase() + .contains(needle.toLowerCase()); + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(ACCOUNT, accountUuid); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); + values.put(JID, jid); + values.put(OPTIONS, subscription); + values.put(SYSTEMACCOUNT, systemAccount); + values.put(PHOTOURI, photoUri); + values.put(KEYS, keys.toString()); + values.put(AVATAR, avatar); + return values; + } + + public static Contact fromCursor(Cursor cursor) { + return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(SYSTEMNAME)), + cursor.getString(cursor.getColumnIndex(SERVERNAME)), + cursor.getString(cursor.getColumnIndex(JID)), + cursor.getInt(cursor.getColumnIndex(OPTIONS)), + cursor.getString(cursor.getColumnIndex(PHOTOURI)), + cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)), + cursor.getString(cursor.getColumnIndex(KEYS)), + cursor.getString(cursor.getColumnIndex(AVATAR))); + } + + public int getSubscription() { + return this.subscription; + } + + public void setSystemAccount(String account) { + this.systemAccount = account; + } + + public void setAccount(Account account) { + this.account = account; + this.accountUuid = account.getUuid(); + } + + public Account getAccount() { + return this.account; + } + + public Presences getPresences() { + return this.presences; + } + + public void updatePresence(String resource, int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(String resource) { + this.presences.removePresence(resource); + } + + public void clearPresences() { + this.presences.clearPresences(); + this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); + } + + public int getMostAvailableStatus() { + return this.presences.getMostAvailableStatus(); + } + + public void setPresences(Presences pres) { + this.presences = pres; + } + + public void setPhotoUri(String uri) { + this.photoUri = uri; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public void setSystemName(String systemName) { + this.systemName = systemName; + } + + public void setPresenceName(String presenceName) { + this.presenceName = presenceName; + } + + public String getSystemAccount() { + return systemAccount; + } + + public Set<String> getOtrFingerprints() { + Set<String> set = new HashSet<String>(); + try { + if (this.keys.has("otr_fingerprints")) { + JSONArray fingerprints = this.keys + .getJSONArray("otr_fingerprints"); + for (int i = 0; i < fingerprints.length(); ++i) { + set.add(fingerprints.getString(i)); + } + } + } catch (JSONException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return set; + } + + public void addOtrFingerprint(String print) { + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + } catch (JSONException e) { + + } + } + + public void setPgpKeyId(long keyId) { + try { + this.keys.put("pgp_keyid", keyId); + } catch (JSONException e) { + + } + } + + public long getPgpKeyId() { + if (this.keys.has("pgp_keyid")) { + try { + return this.keys.getLong("pgp_keyid"); + } catch (JSONException e) { + return 0; + } + } else { + return 0; + } + } + + public void setOption(int option) { + this.subscription |= 1 << option; + } + + public void resetOption(int option) { + this.subscription &= ~(1 << option); + } + + public boolean getOption(int option) { + return ((this.subscription & (1 << option)) != 0); + } + + public boolean showInRoster() { + return (this.getOption(Contact.Options.IN_ROSTER) && (!this + .getOption(Contact.Options.DIRTY_DELETE))) + || (this.getOption(Contact.Options.DIRTY_PUSH)); + } + + public void parseSubscriptionFromElement(Element item) { + String ask = item.getAttribute("ask"); + String subscription = item.getAttribute("subscription"); + + if (subscription != null) { + if (subscription.equals("to")) { + this.resetOption(Contact.Options.FROM); + this.setOption(Contact.Options.TO); + } else if (subscription.equals("from")) { + this.resetOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); + this.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else if (subscription.equals("both")) { + this.setOption(Contact.Options.TO); + this.setOption(Contact.Options.FROM); + this.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else if (subscription.equals("none")) { + this.resetOption(Contact.Options.FROM); + this.resetOption(Contact.Options.TO); + } + } + + // do NOT override asking if pending push request + if (!this.getOption(Contact.Options.DIRTY_PUSH)) { + if ((ask != null) && (ask.equals("subscribe"))) { + this.setOption(Contact.Options.ASKING); + } else { + this.resetOption(Contact.Options.ASKING); + } + } + } + + public Element asElement() { + Element item = new Element("item"); + item.setAttribute("jid", this.jid); + if (this.serverName != null) { + item.setAttribute("name", this.serverName); + } + return item; + } + + public class Options { + public static final int TO = 0; + public static final int FROM = 1; + public static final int ASKING = 2; + public static final int PREEMPTIVE_GRANT = 3; + public static final int IN_ROSTER = 4; + public static final int PENDING_SUBSCRIPTION_REQUEST = 5; + public static final int DIRTY_PUSH = 6; + public static final int DIRTY_DELETE = 7; + } + + public class Lastseen { + public long time = 0; + public String presence = null; + } + + @Override + public int compareTo(ListItem another) { + return this.getDisplayName().compareToIgnoreCase( + another.getDisplayName()); + } + + public String getServer() { + String[] split = getJid().split("@"); + if (split.length >= 2) { + return split[1]; + } else { + return null; + } + } + + public boolean setAvatar(String filename) { + if (this.avatar != null && this.avatar.equals(filename)) { + return false; + } else { + this.avatar = filename; + return true; + } + } + + public String getAvatar() { + return this.avatar; + } + + public boolean deleteOtrFingerprint(String fingerprint) { + boolean success = false; + try { + if (this.keys.has("otr_fingerprints")) { + JSONArray newPrints = new JSONArray(); + JSONArray oldPrints = this.keys + .getJSONArray("otr_fingerprints"); + for (int i = 0; i < oldPrints.length(); ++i) { + if (!oldPrints.getString(i).equals(fingerprint)) { + newPrints.put(oldPrints.getString(i)); + } else { + success = true; + } + } + this.keys.put("otr_fingerprints", newPrints); + } + return success; + } catch (JSONException e) { + return false; + } + } + + public boolean trusted() { + return getOption(Options.FROM) && getOption(Options.TO); + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java new file mode 100644 index 00000000..9d4c36db --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -0,0 +1,500 @@ +package eu.siacs.conversations.entities; + +import java.security.interfaces.DSAPublicKey; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.services.XmppConnectionService; + +import net.java.otr4j.OtrException; +import net.java.otr4j.crypto.OtrCryptoEngineImpl; +import net.java.otr4j.crypto.OtrCryptoException; +import net.java.otr4j.session.SessionID; +import net.java.otr4j.session.SessionImpl; +import net.java.otr4j.session.SessionStatus; +import android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; + +public class Conversation extends AbstractEntity { + public static final String TABLENAME = "conversations"; + + public static final int STATUS_AVAILABLE = 0; + public static final int STATUS_ARCHIVED = 1; + public static final int STATUS_DELETED = 2; + + public static final int MODE_MULTI = 1; + public static final int MODE_SINGLE = 0; + + public static final String NAME = "name"; + public static final String ACCOUNT = "accountUuid"; + public static final String CONTACT = "contactUuid"; + public static final String CONTACTJID = "contactJid"; + public static final String STATUS = "status"; + public static final String CREATED = "created"; + public static final String MODE = "mode"; + public static final String ATTRIBUTES = "attributes"; + + public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; + public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; + public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; + + private String name; + private String contactUuid; + private String accountUuid; + private String contactJid; + private int status; + private long created; + private int mode; + + private JSONObject attributes = new JSONObject(); + + private String nextPresence; + + protected ArrayList<Message> messages = new ArrayList<Message>(); + protected Account account = null; + + private transient SessionImpl otrSession; + + private transient String otrFingerprint = null; + + private String nextMessage; + + private transient MucOptions mucOptions = null; + + // private transient String latestMarkableMessageId; + + private byte[] symmetricKey; + + private Bookmark bookmark; + + public Conversation(String name, Account account, String contactJid, + int mode) { + this(java.util.UUID.randomUUID().toString(), name, null, account + .getUuid(), contactJid, System.currentTimeMillis(), + STATUS_AVAILABLE, mode, ""); + this.account = account; + } + + public Conversation(String uuid, String name, String contactUuid, + String accountUuid, String contactJid, long created, int status, + int mode, String attributes) { + this.uuid = uuid; + this.name = name; + this.contactUuid = contactUuid; + this.accountUuid = accountUuid; + this.contactJid = contactJid; + this.created = created; + this.status = status; + this.mode = mode; + try { + if (attributes == null) { + attributes = new String(); + } + this.attributes = new JSONObject(attributes); + } catch (JSONException e) { + this.attributes = new JSONObject(); + } + } + + public List<Message> getMessages() { + return messages; + } + + public boolean isRead() { + if ((this.messages == null) || (this.messages.size() == 0)) + return true; + return this.messages.get(this.messages.size() - 1).isRead(); + } + + public void markRead() { + if (this.messages == null) { + return; + } + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (messages.get(i).isRead()) { + break; + } + this.messages.get(i).markRead(); + } + } + + public String getLatestMarkableMessageId() { + if (this.messages == null) { + return null; + } + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED + && this.messages.get(i).markable) { + if (this.messages.get(i).isRead()) { + return null; + } else { + return this.messages.get(i).getRemoteMsgId(); + } + } + } + return null; + } + + public Message getLatestMessage() { + if ((this.messages == null) || (this.messages.size() == 0)) { + Message message = new Message(this, "", Message.ENCRYPTION_NONE); + message.setTime(getCreated()); + return message; + } else { + Message message = this.messages.get(this.messages.size() - 1); + message.setConversation(this); + return message; + } + } + + public void setMessages(ArrayList<Message> msgs) { + this.messages = msgs; + } + + public String getName() { + if (getMode() == MODE_MULTI && getMucOptions().getSubject() != null) { + return getMucOptions().getSubject(); + } else if (getMode() == MODE_MULTI && bookmark != null + && bookmark.getName() != null) { + return bookmark.getName(); + } else { + return this.getContact().getDisplayName(); + } + } + + public String getProfilePhotoString() { + return this.getContact().getProfilePhoto(); + } + + public String getAccountUuid() { + return this.accountUuid; + } + + public Account getAccount() { + return this.account; + } + + public Contact getContact() { + return this.account.getRoster().getContact(this.contactJid); + } + + public void setAccount(Account account) { + this.account = account; + } + + public String getContactJid() { + return this.contactJid; + } + + public int getStatus() { + return this.status; + } + + public long getCreated() { + return this.created; + } + + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(NAME, name); + values.put(CONTACT, contactUuid); + values.put(ACCOUNT, accountUuid); + values.put(CONTACTJID, contactJid); + values.put(CREATED, created); + values.put(STATUS, status); + values.put(MODE, mode); + values.put(ATTRIBUTES, attributes.toString()); + return values; + } + + public static Conversation fromCursor(Cursor cursor) { + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(NAME)), + cursor.getString(cursor.getColumnIndex(CONTACT)), + cursor.getString(cursor.getColumnIndex(ACCOUNT)), + cursor.getString(cursor.getColumnIndex(CONTACTJID)), + cursor.getLong(cursor.getColumnIndex(CREATED)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(MODE)), + cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); + } + + public void setStatus(int status) { + this.status = status; + } + + public int getMode() { + return this.mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + + public SessionImpl startOtrSession(XmppConnectionService service, + String presence, boolean sendStart) { + if (this.otrSession != null) { + return this.otrSession; + } else { + SessionID sessionId = new SessionID(this.getContactJid().split("/", + 2)[0], presence, "xmpp"); + this.otrSession = new SessionImpl(sessionId, getAccount() + .getOtrEngine(service)); + try { + if (sendStart) { + this.otrSession.startSession(); + return this.otrSession; + } + return this.otrSession; + } catch (OtrException e) { + return null; + } + } + + } + + public SessionImpl getOtrSession() { + return this.otrSession; + } + + public void resetOtrSession() { + this.otrFingerprint = null; + this.otrSession = null; + } + + public void startOtrIfNeeded() { + if (this.otrSession != null + && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { + try { + this.otrSession.startSession(); + } catch (OtrException e) { + this.resetOtrSession(); + } + } + } + + public boolean endOtrIfNeeded() { + if (this.otrSession != null) { + if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { + try { + this.otrSession.endSession(); + this.resetOtrSession(); + return true; + } catch (OtrException e) { + this.resetOtrSession(); + return false; + } + } else { + this.resetOtrSession(); + return false; + } + } else { + return false; + } + } + + public boolean hasValidOtrSession() { + return this.otrSession != null; + } + + public String getOtrFingerprint() { + if (this.otrFingerprint == null) { + try { + if (getOtrSession() == null) { + return ""; + } + DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession() + .getRemotePublicKey(); + StringBuilder builder = new StringBuilder( + new OtrCryptoEngineImpl().getFingerprint(remotePubKey)); + builder.insert(8, " "); + builder.insert(17, " "); + builder.insert(26, " "); + builder.insert(35, " "); + this.otrFingerprint = builder.toString(); + } catch (OtrCryptoException e) { + + } + } + return this.otrFingerprint; + } + + public synchronized MucOptions getMucOptions() { + if (this.mucOptions == null) { + this.mucOptions = new MucOptions(this); + } + return this.mucOptions; + } + + public void resetMucOptions() { + this.mucOptions = null; + } + + public void setContactJid(String jid) { + this.contactJid = jid; + } + + public void setNextPresence(String presence) { + this.nextPresence = presence; + } + + public String getNextPresence() { + return this.nextPresence; + } + + public int getLatestEncryption() { + int latestEncryption = this.getLatestMessage().getEncryption(); + if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) + || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { + return Message.ENCRYPTION_PGP; + } else { + return latestEncryption; + } + } + + public int getNextEncryption(boolean force) { + int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); + if (next == -1) { + int latest = this.getLatestEncryption(); + if (latest == Message.ENCRYPTION_NONE) { + if (force && getMode() == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else if (getContact().getPresences().size() == 1) { + if (getContact().getOtrFingerprints().size() >= 1) { + return Message.ENCRYPTION_OTR; + } else { + return latest; + } + } else { + return latest; + } + } else { + return latest; + } + } + if (next == Message.ENCRYPTION_NONE && force + && getMode() == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else { + return next; + } + } + + public void setNextEncryption(int encryption) { + this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption)); + } + + public String getNextMessage() { + if (this.nextMessage == null) { + return ""; + } else { + return this.nextMessage; + } + } + + public void setNextMessage(String message) { + this.nextMessage = message; + } + + public void setSymmetricKey(byte[] key) { + this.symmetricKey = key; + } + + public byte[] getSymmetricKey() { + return this.symmetricKey; + } + + public void setBookmark(Bookmark bookmark) { + this.bookmark = bookmark; + this.bookmark.setConversation(this); + } + + public void deregisterWithBookmark() { + if (this.bookmark != null) { + this.bookmark.setConversation(null); + } + } + + public Bookmark getBookmark() { + return this.bookmark; + } + + public boolean hasDuplicateMessage(Message message) { + for (int i = this.getMessages().size() - 1; i >= 0; --i) { + if (this.messages.get(i).equals(message)) { + return true; + } + } + return false; + } + + public void setMutedTill(long value) { + this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); + } + + public boolean isMuted() { + return SystemClock.elapsedRealtime() < this.getLongAttribute( + ATTRIBUTE_MUTED_TILL, 0); + } + + public boolean setAttribute(String key, String value) { + try { + this.attributes.put(key, value); + return true; + } catch (JSONException e) { + return false; + } + } + + public String getAttribute(String key) { + try { + return this.attributes.getString(key); + } catch (JSONException e) { + return null; + } + } + + public int getIntAttribute(String key, int defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public long getLongAttribute(String key, long defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + } + + public void add(Message message) { + message.setConversation(this); + synchronized (this.messages) { + this.messages.add(message); + } + } + + public void addAll(int index, List<Message> messages) { + synchronized (this.messages) { + this.messages.addAll(index, messages); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Downloadable.java b/src/main/java/eu/siacs/conversations/entities/Downloadable.java new file mode 100644 index 00000000..70516b20 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Downloadable.java @@ -0,0 +1,21 @@ +package eu.siacs.conversations.entities; + +public interface Downloadable { + + public final String[] VALID_EXTENSIONS = { "webp", "jpeg", "jpg", "png" }; + public final String[] VALID_CRYPTO_EXTENSIONS = { "pgp", "gpg", "otr" }; + + public static final int STATUS_UNKNOWN = 0x200; + public static final int STATUS_CHECKING = 0x201; + public static final int STATUS_FAILED = 0x202; + public static final int STATUS_OFFER = 0x203; + public static final int STATUS_DOWNLOADING = 0x204; + public static final int STATUS_DELETED = 0x205; + public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206; + + public boolean start(); + + public int getStatus(); + + public long getFileSize(); +} diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java new file mode 100644 index 00000000..1605c75b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -0,0 +1,154 @@ +package eu.siacs.conversations.entities; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.Config; +import android.util.Log; + +public class DownloadableFile extends File { + + private static final long serialVersionUID = 2247012619505115863L; + + private long expectedSize = 0; + private String sha1sum; + private Key aeskey; + + private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; + + public DownloadableFile(String path) { + super(path); + } + + public long getSize() { + return super.length(); + } + + public long getExpectedSize() { + if (this.aeskey != null) { + if (this.expectedSize == 0) { + return 0; + } else { + return (this.expectedSize / 16 + 1) * 16; + } + } else { + return this.expectedSize; + } + } + + public void setExpectedSize(long size) { + this.expectedSize = size; + } + + public String getSha1Sum() { + return this.sha1sum; + } + + public void setSha1Sum(String sum) { + this.sha1sum = sum; + } + + public void setKey(byte[] key) { + if (key.length == 48) { + byte[] secretKey = new byte[32]; + byte[] iv = new byte[16]; + System.arraycopy(key, 0, iv, 0, 16); + System.arraycopy(key, 16, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + this.iv = iv; + } else if (key.length >= 32) { + byte[] secretKey = new byte[32]; + System.arraycopy(key, 0, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } else if (key.length >= 16) { + byte[] secretKey = new byte[16]; + System.arraycopy(key, 0, secretKey, 0, 16); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } + } + + public Key getKey() { + return this.aeskey; + } + + public InputStream createInputStream() { + if (this.getKey() == null) { + try { + return new FileInputStream(this); + } catch (FileNotFoundException e) { + return null; + } + } else { + try { + IvParameterSpec ips = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips); + Log.d(Config.LOGTAG, "opening encrypted input stream"); + return new CipherInputStream(new FileInputStream(this), cipher); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); + return null; + } catch (FileNotFoundException e) { + return null; + } + } + } + + public OutputStream createOutputStream() { + if (this.getKey() == null) { + try { + return new FileOutputStream(this); + } catch (FileNotFoundException e) { + return null; + } + } else { + try { + IvParameterSpec ips = new IvParameterSpec(this.iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips); + Log.d(Config.LOGTAG, "opening encrypted output stream"); + return new CipherOutputStream(new FileOutputStream(this), + cipher); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); + return null; + } catch (FileNotFoundException e) { + return null; + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java new file mode 100644 index 00000000..a1872d2f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/ListItem.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.entities; + +public interface ListItem extends Comparable<ListItem> { + public String getDisplayName(); + + public String getJid(); +} diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java new file mode 100644 index 00000000..8a83c465 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -0,0 +1,512 @@ +package eu.siacs.conversations.entities; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +import eu.siacs.conversations.Config; +import android.content.ContentValues; +import android.database.Cursor; + +public class Message extends AbstractEntity { + + public static final String TABLENAME = "messages"; + + public static final int STATUS_RECEIVED = 0; + public static final int STATUS_UNSEND = 1; + public static final int STATUS_SEND = 2; + public static final int STATUS_SEND_FAILED = 3; + public static final int STATUS_WAITING = 5; + public static final int STATUS_OFFERED = 6; + public static final int STATUS_SEND_RECEIVED = 7; + public static final int STATUS_SEND_DISPLAYED = 8; + + public static final int ENCRYPTION_NONE = 0; + public static final int ENCRYPTION_PGP = 1; + public static final int ENCRYPTION_OTR = 2; + public static final int ENCRYPTION_DECRYPTED = 3; + public static final int ENCRYPTION_DECRYPTION_FAILED = 4; + + public static final int TYPE_TEXT = 0; + public static final int TYPE_IMAGE = 1; + public static final int TYPE_AUDIO = 2; + public static final int TYPE_STATUS = 3; + public static final int TYPE_PRIVATE = 4; + + public static String CONVERSATION = "conversationUuid"; + public static String COUNTERPART = "counterpart"; + public static String TRUE_COUNTERPART = "trueCounterpart"; + public static String BODY = "body"; + public static String TIME_SENT = "timeSent"; + public static String ENCRYPTION = "encryption"; + public static String STATUS = "status"; + public static String TYPE = "type"; + public static String REMOTE_MSG_ID = "remoteMsgId"; + + protected String conversationUuid; + protected String counterpart; + protected String trueCounterpart; + protected String body; + protected String encryptedBody; + protected long timeSent; + protected int encryption; + protected int status; + protected int type; + protected boolean read = true; + protected String remoteMsgId = null; + + protected Conversation conversation = null; + protected Downloadable downloadable = null; + public boolean markable = false; + + private Message mNextMessage = null; + private Message mPreviousMessage = null; + + private Message() { + + } + + public Message(Conversation conversation, String body, int encryption) { + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), + conversation.getContactJid(), null, body, System + .currentTimeMillis(), encryption, + Message.STATUS_UNSEND, TYPE_TEXT, null); + this.conversation = conversation; + } + + public Message(Conversation conversation, String counterpart, String body, + int encryption, int status) { + this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), + counterpart, null, body, System.currentTimeMillis(), + encryption, status, TYPE_TEXT, null); + this.conversation = conversation; + } + + public Message(String uuid, String conversationUUid, String counterpart, + String trueCounterpart, String body, long timeSent, int encryption, + int status, int type, String remoteMsgId) { + this.uuid = uuid; + this.conversationUuid = conversationUUid; + this.counterpart = counterpart; + this.trueCounterpart = trueCounterpart; + this.body = body; + this.timeSent = timeSent; + this.encryption = encryption; + this.status = status; + this.type = type; + this.remoteMsgId = remoteMsgId; + } + + @Override + public ContentValues getContentValues() { + ContentValues values = new ContentValues(); + values.put(UUID, uuid); + values.put(CONVERSATION, conversationUuid); + values.put(COUNTERPART, counterpart); + values.put(TRUE_COUNTERPART, trueCounterpart); + values.put(BODY, body); + values.put(TIME_SENT, timeSent); + values.put(ENCRYPTION, encryption); + values.put(STATUS, status); + values.put(TYPE, type); + values.put(REMOTE_MSG_ID, remoteMsgId); + return values; + } + + public String getConversationUuid() { + return conversationUuid; + } + + public Conversation getConversation() { + return this.conversation; + } + + public String getCounterpart() { + return counterpart; + } + + public Contact getContact() { + if (this.conversation.getMode() == Conversation.MODE_SINGLE) { + return this.conversation.getContact(); + } else { + if (this.trueCounterpart == null) { + return null; + } else { + return this.conversation.getAccount().getRoster() + .getContactFromRoster(this.trueCounterpart); + } + } + } + + public String getBody() { + return body; + } + + public long getTimeSent() { + return timeSent; + } + + public int getEncryption() { + return encryption; + } + + public int getStatus() { + return status; + } + + public String getRemoteMsgId() { + return this.remoteMsgId; + } + + public void setRemoteMsgId(String id) { + this.remoteMsgId = id; + } + + public static Message fromCursor(Cursor cursor) { + return new Message(cursor.getString(cursor.getColumnIndex(UUID)), + cursor.getString(cursor.getColumnIndex(CONVERSATION)), + cursor.getString(cursor.getColumnIndex(COUNTERPART)), + cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)), + cursor.getString(cursor.getColumnIndex(BODY)), + cursor.getLong(cursor.getColumnIndex(TIME_SENT)), + cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), + cursor.getInt(cursor.getColumnIndex(STATUS)), + cursor.getInt(cursor.getColumnIndex(TYPE)), + cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID))); + } + + public void setConversation(Conversation conv) { + this.conversation = conv; + } + + public void setStatus(int status) { + this.status = status; + } + + public boolean isRead() { + return this.read; + } + + public void markRead() { + this.read = true; + } + + public void markUnread() { + this.read = false; + } + + public void setTime(long time) { + this.timeSent = time; + } + + public void setEncryption(int encryption) { + this.encryption = encryption; + } + + public void setBody(String body) { + this.body = body; + } + + public String getEncryptedBody() { + return this.encryptedBody; + } + + public void setEncryptedBody(String body) { + this.encryptedBody = body; + } + + public void setType(int type) { + this.type = type; + } + + public int getType() { + return this.type; + } + + public void setPresence(String presence) { + if (presence == null) { + this.counterpart = this.counterpart.split("/", 2)[0]; + } else { + this.counterpart = this.counterpart.split("/", 2)[0] + "/" + + presence; + } + } + + public void setTrueCounterpart(String trueCounterpart) { + this.trueCounterpart = trueCounterpart; + } + + public String getPresence() { + String[] counterparts = this.counterpart.split("/", 2); + if (counterparts.length == 2) { + return counterparts[1]; + } else { + if (this.counterpart.contains("/")) { + return ""; + } else { + return null; + } + } + } + + public void setDownloadable(Downloadable downloadable) { + this.downloadable = downloadable; + } + + public Downloadable getDownloadable() { + return this.downloadable; + } + + public static Message createStatusMessage(Conversation conversation) { + Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + return message; + } + + public void setCounterpart(String counterpart) { + this.counterpart = counterpart; + } + + public boolean equals(Message message) { + if ((this.remoteMsgId != null) && (this.body != null) + && (this.counterpart != null)) { + return this.remoteMsgId.equals(message.getRemoteMsgId()) + && this.body.equals(message.getBody()) + && this.counterpart.equals(message.getCounterpart()); + } else { + return false; + } + } + + public Message next() { + if (this.mNextMessage == null) { + synchronized (this.conversation.messages) { + int index = this.conversation.messages.indexOf(this); + if (index < 0 + || index >= this.conversation.getMessages().size() - 1) { + this.mNextMessage = null; + } else { + this.mNextMessage = this.conversation.messages + .get(index + 1); + } + } + } + return this.mNextMessage; + } + + public Message prev() { + if (this.mPreviousMessage == null) { + synchronized (this.conversation.messages) { + int index = this.conversation.messages.indexOf(this); + if (index <= 0 || index > this.conversation.messages.size()) { + this.mPreviousMessage = null; + } else { + this.mPreviousMessage = this.conversation.messages + .get(index - 1); + } + } + } + return this.mPreviousMessage; + } + + public boolean mergable(Message message) { + if (message == null) { + return false; + } + return (message.getType() == Message.TYPE_TEXT + && this.getDownloadable() == null + && message.getDownloadable() == null + && message.getEncryption() != Message.ENCRYPTION_PGP + && this.getType() == message.getType() + && this.getEncryption() == message.getEncryption() + && this.getCounterpart().equals(message.getCounterpart()) + && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && ((this + .getStatus() == message.getStatus() || ((this.getStatus() == Message.STATUS_SEND || this + .getStatus() == Message.STATUS_SEND_RECEIVED) && (message + .getStatus() == Message.STATUS_UNSEND + || message.getStatus() == Message.STATUS_SEND || message + .getStatus() == Message.STATUS_SEND_DISPLAYED)))) + && !message.bodyContainsDownloadable() + && !this.bodyContainsDownloadable()); + } + + public String getMergedBody() { + Message next = this.next(); + if (this.mergable(next)) { + return body.trim() + '\n' + next.getMergedBody(); + } + return body.trim(); + } + + public int getMergedStatus() { + Message next = this.next(); + if (this.mergable(next)) { + return next.getMergedStatus(); + } else { + return getStatus(); + } + } + + public long getMergedTimeSent() { + Message next = this.next(); + if (this.mergable(next)) { + return next.getMergedTimeSent(); + } else { + return getTimeSent(); + } + } + + public boolean wasMergedIntoPrevious() { + Message prev = this.prev(); + if (prev == null) { + return false; + } else { + return prev.mergable(this); + } + } + + public boolean trusted() { + Contact contact = this.getContact(); + return (status > STATUS_RECEIVED || (contact != null && contact.trusted())); + } + + public boolean bodyContainsDownloadable() { + try { + URL url = new URL(this.getBody()); + if (!url.getProtocol().equalsIgnoreCase("http") + && !url.getProtocol().equalsIgnoreCase("https")) { + return false; + } + if (url.getPath() == null) { + return false; + } + String[] pathParts = url.getPath().split("/"); + String filename; + if (pathParts.length > 0) { + filename = pathParts[pathParts.length - 1]; + } else { + filename = pathParts[0]; + } + String[] extensionParts = filename.split("\\."); + if (extensionParts.length == 2 + && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( + extensionParts[extensionParts.length - 1])) { + return true; + } else if (extensionParts.length == 3 + && Arrays + .asList(Downloadable.VALID_CRYPTO_EXTENSIONS) + .contains(extensionParts[extensionParts.length - 1]) + && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( + extensionParts[extensionParts.length - 2])) { + return true; + } else { + return false; + } + } catch (MalformedURLException e) { + return false; + } + } + + public ImageParams getImageParams() { + ImageParams params = getLegacyImageParams(); + if (params!=null) { + return params; + } + params = new ImageParams(); + if (this.downloadable != null) { + params.size = this.downloadable.getFileSize(); + } + if (body == null) { + return params; + } + String parts[] = body.split("\\|"); + if (parts.length == 1) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.origin = parts[0]; + try { + params.url = new URL(parts[0]); + } catch (MalformedURLException e1) { + params.url = null; + } + } + } else if (parts.length == 3) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.size = 0; + } + try { + params.width = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + params.height = 0; + } + } else if (parts.length == 4) { + params.origin = parts[0]; + try { + params.url = new URL(parts[0]); + } catch (MalformedURLException e1) { + params.url = null; + } + try { + params.size = Long.parseLong(parts[1]); + } catch (NumberFormatException e) { + params.size = 0; + } + try { + params.width = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + params.height = 0; + } + } + return params; + } + + public ImageParams getLegacyImageParams() { + ImageParams params = new ImageParams(); + if (body == null) { + return params; + } + String parts[] = body.split(","); + if (parts.length == 3) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + return null; + } + try { + params.width = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + return null; + } + try { + params.height = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + return null; + } + return params; + } else { + return null; + } + } + + public class ImageParams { + public URL url; + public long size = 0; + public int width = 0; + public int height = 0; + public String origin; + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java new file mode 100644 index 00000000..d7407cd5 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -0,0 +1,369 @@ +package eu.siacs.conversations.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import android.annotation.SuppressLint; + +@SuppressLint("DefaultLocale") +public class MucOptions { + public static final int ERROR_NO_ERROR = 0; + public static final int ERROR_NICK_IN_USE = 1; + public static final int ERROR_ROOM_NOT_FOUND = 2; + public static final int ERROR_PASSWORD_REQUIRED = 3; + public static final int ERROR_BANNED = 4; + public static final int ERROR_MEMBERS_ONLY = 5; + + public static final int KICKED_FROM_ROOM = 9; + + public static final String STATUS_CODE_BANNED = "301"; + public static final String STATUS_CODE_KICKED = "307"; + + public interface OnRenameListener { + public void onRename(boolean success); + } + + public class User { + public static final int ROLE_MODERATOR = 3; + public static final int ROLE_NONE = 0; + public static final int ROLE_PARTICIPANT = 2; + public static final int ROLE_VISITOR = 1; + public static final int AFFILIATION_ADMIN = 4; + public static final int AFFILIATION_OWNER = 3; + public static final int AFFILIATION_MEMBER = 2; + public static final int AFFILIATION_OUTCAST = 1; + public static final int AFFILIATION_NONE = 0; + + private int role; + private int affiliation; + private String name; + private String jid; + private long pgpKeyId = 0; + + public String getName() { + return name; + } + + public void setName(String user) { + this.name = user; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + + public int getRole() { + return this.role; + } + + public void setRole(String role) { + role = role.toLowerCase(); + if (role.equals("moderator")) { + this.role = ROLE_MODERATOR; + } else if (role.equals("participant")) { + this.role = ROLE_PARTICIPANT; + } else if (role.equals("visitor")) { + this.role = ROLE_VISITOR; + } else { + this.role = ROLE_NONE; + } + } + + public int getAffiliation() { + return this.affiliation; + } + + public void setAffiliation(String affiliation) { + if (affiliation.equalsIgnoreCase("admin")) { + this.affiliation = AFFILIATION_ADMIN; + } else if (affiliation.equalsIgnoreCase("owner")) { + this.affiliation = AFFILIATION_OWNER; + } else if (affiliation.equalsIgnoreCase("member")) { + this.affiliation = AFFILIATION_MEMBER; + } else if (affiliation.equalsIgnoreCase("outcast")) { + this.affiliation = AFFILIATION_OUTCAST; + } else { + this.affiliation = AFFILIATION_NONE; + } + } + + public void setPgpKeyId(long id) { + this.pgpKeyId = id; + } + + public long getPgpKeyId() { + return this.pgpKeyId; + } + + public Contact getContact() { + return account.getRoster().getContactFromRoster(getJid()); + } + } + + private Account account; + private List<User> users = new CopyOnWriteArrayList<User>(); + private Conversation conversation; + private boolean isOnline = false; + private int error = ERROR_ROOM_NOT_FOUND; + private OnRenameListener renameListener = null; + private boolean aboutToRename = false; + private User self = new User(); + private String subject = null; + private String joinnick; + private String password = null; + + public MucOptions(Conversation conversation) { + this.account = conversation.getAccount(); + this.conversation = conversation; + } + + public void deleteUser(String name) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(name)) { + users.remove(i); + return; + } + } + } + + public void addUser(User user) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(user.getName())) { + users.set(i, user); + return; + } + } + users.add(user); + } + + public void processPacket(PresencePacket packet, PgpEngine pgp) { + String[] fromParts = packet.getFrom().split("/", 2); + if (fromParts.length >= 2) { + String name = fromParts[1]; + String type = packet.getAttribute("type"); + if (type == null) { + User user = new User(); + Element item = packet.findChild("x", + "http://jabber.org/protocol/muc#user") + .findChild("item"); + user.setName(name); + user.setAffiliation(item.getAttribute("affiliation")); + user.setRole(item.getAttribute("role")); + user.setJid(item.getAttribute("jid")); + user.setName(name); + if (name.equals(this.joinnick)) { + this.isOnline = true; + this.error = ERROR_NO_ERROR; + self = user; + if (aboutToRename) { + if (renameListener != null) { + renameListener.onRename(true); + } + aboutToRename = false; + } + } else { + addUser(user); + } + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + user.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); + } + } + } else if (type.equals("unavailable") && name.equals(this.joinnick)) { + Element x = packet.findChild("x", + "http://jabber.org/protocol/muc#user"); + if (x != null) { + Element status = x.findChild("status"); + if (status != null) { + String code = status.getAttribute("code"); + if (STATUS_CODE_KICKED.equals(code)) { + this.isOnline = false; + this.error = KICKED_FROM_ROOM; + } else if (STATUS_CODE_BANNED.equals(code)) { + this.isOnline = false; + this.error = ERROR_BANNED; + } + } + } + } else if (type.equals("unavailable")) { + deleteUser(packet.getAttribute("from").split("/", 2)[1]); + } else if (type.equals("error")) { + Element error = packet.findChild("error"); + if (error != null && error.hasChild("conflict")) { + if (aboutToRename) { + if (renameListener != null) { + renameListener.onRename(false); + } + aboutToRename = false; + this.setJoinNick(getActualNick()); + } else { + this.error = ERROR_NICK_IN_USE; + } + } else if (error != null && error.hasChild("not-authorized")) { + this.error = ERROR_PASSWORD_REQUIRED; + } else if (error != null && error.hasChild("forbidden")) { + this.error = ERROR_BANNED; + } else if (error != null + && error.hasChild("registration-required")) { + this.error = ERROR_MEMBERS_ONLY; + } + } + } + } + + public List<User> getUsers() { + return this.users; + } + + public String getProposedNick() { + String[] mucParts = conversation.getContactJid().split("/", 2); + if (conversation.getBookmark() != null + && conversation.getBookmark().getNick() != null) { + return conversation.getBookmark().getNick(); + } else { + if (mucParts.length == 2) { + return mucParts[1]; + } else { + return account.getUsername(); + } + } + } + + public String getActualNick() { + if (this.self.getName() != null) { + return this.self.getName(); + } else { + return this.getProposedNick(); + } + } + + public void setJoinNick(String nick) { + this.joinnick = nick; + } + + public boolean online() { + return this.isOnline; + } + + public int getError() { + return this.error; + } + + public void setOnRenameListener(OnRenameListener listener) { + this.renameListener = listener; + } + + public OnRenameListener getOnRenameListener() { + return this.renameListener; + } + + public void setOffline() { + this.users.clear(); + this.error = 0; + this.isOnline = false; + } + + public User getSelf() { + return self; + } + + public void setSubject(String content) { + this.subject = content; + } + + public String getSubject() { + return this.subject; + } + + public void flagAboutToRename() { + this.aboutToRename = true; + } + + public long[] getPgpKeyIds() { + List<Long> ids = new ArrayList<Long>(); + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { + ids.add(user.getPgpKeyId()); + } + } + long[] primitivLongArray = new long[ids.size()]; + for (int i = 0; i < ids.size(); ++i) { + primitivLongArray[i] = ids.get(i); + } + return primitivLongArray; + } + + public boolean pgpKeysInUse() { + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { + return true; + } + } + return false; + } + + public boolean everybodyHasKeys() { + for (User user : getUsers()) { + if (user.getPgpKeyId() == 0) { + return false; + } + } + return true; + } + + public String getJoinJid() { + return this.conversation.getContactJid().split("/", 2)[0] + "/" + + this.joinnick; + } + + public String getTrueCounterpart(String counterpart) { + for (User user : this.getUsers()) { + if (user.getName().equals(counterpart)) { + return user.getJid(); + } + } + return null; + } + + public String getPassword() { + this.password = conversation + .getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD); + if (this.password == null && conversation.getBookmark() != null + && conversation.getBookmark().getPassword() != null) { + return conversation.getBookmark().getPassword(); + } else { + return this.password; + } + } + + public void setPassword(String password) { + if (conversation.getBookmark() != null) { + conversation.getBookmark().setPassword(password); + } else { + this.password = password; + } + conversation + .setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password); + } + + public Conversation getConversation() { + return this.conversation; + } +}
\ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java new file mode 100644 index 00000000..b5899847 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.entities; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; + +import eu.siacs.conversations.xml.Element; + +public class Presences { + + public static final int CHAT = -1; + public static final int ONLINE = 0; + public static final int AWAY = 1; + public static final int XA = 2; + public static final int DND = 3; + public static final int OFFLINE = 4; + + private Hashtable<String, Integer> presences = new Hashtable<String, Integer>(); + + public Hashtable<String, Integer> getPresences() { + return this.presences; + } + + public void updatePresence(String resource, int status) { + this.presences.put(resource, status); + } + + public void removePresence(String resource) { + this.presences.remove(resource); + } + + public void clearPresences() { + this.presences.clear(); + } + + public int getMostAvailableStatus() { + int status = OFFLINE; + Iterator<Entry<String, Integer>> it = presences.entrySet().iterator(); + while (it.hasNext()) { + Entry<String, Integer> entry = it.next(); + if (entry.getValue() < status) + status = entry.getValue(); + } + return status; + } + + public static int parseShow(Element show) { + if ((show == null) || (show.getContent() == null)) { + return Presences.ONLINE; + } else if (show.getContent().equals("away")) { + return Presences.AWAY; + } else if (show.getContent().equals("xa")) { + return Presences.XA; + } else if (show.getContent().equals("chat")) { + return Presences.CHAT; + } else if (show.getContent().equals("dnd")) { + return Presences.DND; + } else { + return Presences.OFFLINE; + } + } + + public int size() { + return presences.size(); + } + + public String[] asStringArray() { + final String[] presencesArray = new String[presences.size()]; + presences.keySet().toArray(presencesArray); + return presencesArray; + } + + public boolean has(String presence) { + return presences.containsKey(presence); + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java new file mode 100644 index 00000000..3267b15a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Roster.java @@ -0,0 +1,83 @@ +package eu.siacs.conversations.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +public class Roster { + Account account; + ConcurrentHashMap<String, Contact> contacts = new ConcurrentHashMap<String, Contact>(); + private String version = null; + + public Roster(Account account) { + this.account = account; + } + + public Contact getContactFromRoster(String jid) { + if (jid == null) { + return null; + } + String cleanJid = jid.split("/", 2)[0]; + Contact contact = contacts.get(cleanJid); + if (contact != null && contact.showInRoster()) { + return contact; + } else { + return null; + } + } + + public Contact getContact(String jid) { + String cleanJid = jid.split("/", 2)[0].toLowerCase(Locale.getDefault()); + if (contacts.containsKey(cleanJid)) { + return contacts.get(cleanJid); + } else { + Contact contact = new Contact(cleanJid); + contact.setAccount(account); + contacts.put(cleanJid, contact); + return contact; + } + } + + public void clearPresences() { + for (Contact contact : getContacts()) { + contact.clearPresences(); + } + } + + public void markAllAsNotInRoster() { + for (Contact contact : getContacts()) { + contact.resetOption(Contact.Options.IN_ROSTER); + } + } + + public void clearSystemAccounts() { + for (Contact contact : getContacts()) { + contact.setPhotoUri(null); + contact.setSystemName(null); + contact.setSystemAccount(null); + } + } + + public List<Contact> getContacts() { + return new ArrayList<Contact>(this.contacts.values()); + } + + public void initContact(Contact contact) { + contact.setAccount(account); + contact.setOption(Contact.Options.IN_ROSTER); + contacts.put(contact.getJid(), contact); + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return this.version; + } + + public Account getAccount() { + return this.account; + } +} diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java new file mode 100644 index 00000000..c96d116d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -0,0 +1,48 @@ +package eu.siacs.conversations.generator; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import eu.siacs.conversations.services.XmppConnectionService; + +import android.util.Base64; + +public abstract class AbstractGenerator { + public final String[] FEATURES = { "urn:xmpp:jingle:1", + "urn:xmpp:jingle:apps:file-transfer:3", + "urn:xmpp:jingle:transports:s5b:1", + "urn:xmpp:jingle:transports:ibb:1", "urn:xmpp:receipts", + "urn:xmpp:chat-markers:0", "http://jabber.org/protocol/muc", + "jabber:x:conference", "http://jabber.org/protocol/caps", + "http://jabber.org/protocol/disco#info", + "urn:xmpp:avatar:metadata+notify" }; + public final String IDENTITY_NAME = "Conversations 0.7"; + public final String IDENTITY_TYPE = "phone"; + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractGenerator(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public String getCapHash() { + StringBuilder s = new StringBuilder(); + s.append("client/" + IDENTITY_TYPE + "//" + IDENTITY_NAME + "<"); + MessageDigest md = null; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + List<String> features = Arrays.asList(FEATURES); + Collections.sort(features); + for (String feature : features) { + s.append(feature + "<"); + } + byte[] sha1 = md.digest(s.toString().getBytes()); + return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); + } +} diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java new file mode 100644 index 00000000..d44bf0ca --- /dev/null +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -0,0 +1,96 @@ +package eu.siacs.conversations.generator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class IqGenerator extends AbstractGenerator { + + public IqGenerator(XmppConnectionService service) { + super(service); + } + + public IqPacket discoResponse(IqPacket request) { + IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT); + packet.setId(request.getId()); + packet.setTo(request.getFrom()); + Element query = packet.addChild("query", + "http://jabber.org/protocol/disco#info"); + query.setAttribute("node", request.query().getAttribute("node")); + Element identity = query.addChild("identity"); + identity.setAttribute("category", "client"); + identity.setAttribute("type", this.IDENTITY_TYPE); + identity.setAttribute("name", IDENTITY_NAME); + List<String> features = Arrays.asList(FEATURES); + Collections.sort(features); + for (String feature : features) { + query.addChild("feature").setAttribute("var", feature); + } + return packet; + } + + protected IqPacket publish(String node, Element item) { + IqPacket packet = new IqPacket(IqPacket.TYPE_SET); + Element pubsub = packet.addChild("pubsub", + "http://jabber.org/protocol/pubsub"); + Element publish = pubsub.addChild("publish"); + publish.setAttribute("node", node); + publish.addChild(item); + return packet; + } + + protected IqPacket retrieve(String node, Element item) { + IqPacket packet = new IqPacket(IqPacket.TYPE_GET); + Element pubsub = packet.addChild("pubsub", + "http://jabber.org/protocol/pubsub"); + Element items = pubsub.addChild("items"); + items.setAttribute("node", node); + if (item != null) { + items.addChild(item); + } + return packet; + } + + public IqPacket publishAvatar(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + Element data = item.addChild("data", "urn:xmpp:avatar:data"); + data.setContent(avatar.image); + return publish("urn:xmpp:avatar:data", item); + } + + public IqPacket publishAvatarMetadata(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + Element metadata = item + .addChild("metadata", "urn:xmpp:avatar:metadata"); + Element info = metadata.addChild("info"); + info.setAttribute("bytes", avatar.size); + info.setAttribute("id", avatar.sha1sum); + info.setAttribute("height", avatar.height); + info.setAttribute("width", avatar.height); + info.setAttribute("type", avatar.type); + return publish("urn:xmpp:avatar:metadata", item); + } + + public IqPacket retrieveAvatar(Avatar avatar) { + Element item = new Element("item"); + item.setAttribute("id", avatar.sha1sum); + IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + packet.setTo(avatar.owner); + return packet; + } + + public IqPacket retrieveAvatarMetaData(String to) { + IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + if (to != null) { + packet.setTo(to); + } + return packet; + } +} diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java new file mode 100644 index 00000000..dd833e56 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -0,0 +1,178 @@ +package eu.siacs.conversations.generator; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public class MessageGenerator extends AbstractGenerator { + public MessageGenerator(XmppConnectionService service) { + super(service); + } + + private MessagePacket preparePacket(Message message, boolean addDelay) { + Conversation conversation = message.getConversation(); + Account account = conversation.getAccount(); + MessagePacket packet = new MessagePacket(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); + packet.addChild("markable", "urn:xmpp:chat-markers:0"); + if (this.mXmppConnectionService.indicateReceived()) { + packet.addChild("request", "urn:xmpp:receipts"); + } + } else if (message.getType() == Message.TYPE_PRIVATE) { + packet.setTo(message.getCounterpart()); + packet.setType(MessagePacket.TYPE_CHAT); + } else { + packet.setTo(message.getCounterpart().split("/", 2)[0]); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + } + packet.setFrom(account.getFullJid()); + packet.setId(message.getUuid()); + if (addDelay) { + addDelay(packet, message.getTimeSent()); + } + return packet; + } + + private void addDelay(MessagePacket packet, long timestamp) { + final SimpleDateFormat mDateFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + Element delay = packet.addChild("delay", "urn:xmpp:delay"); + Date date = new Date(timestamp); + delay.setAttribute("stamp", mDateFormat.format(date)); + } + + public MessagePacket generateOtrChat(Message message) { + return generateOtrChat(message, false); + } + + public MessagePacket generateOtrChat(Message message, boolean addDelay) { + Session otrSession = message.getConversation().getOtrSession(); + if (otrSession == null) { + return null; + } + MessagePacket packet = preparePacket(message, addDelay); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + try { + packet.setBody(otrSession.transformSending(message.getBody())); + return packet; + } catch (OtrException e) { + return null; + } + } + + public MessagePacket generateChat(Message message) { + return generateChat(message, false); + } + + public MessagePacket generateChat(Message message, boolean addDelay) { + MessagePacket packet = preparePacket(message, addDelay); + packet.setBody(message.getBody()); + return packet; + } + + public MessagePacket generatePgpChat(Message message) { + return generatePgpChat(message, false); + } + + public MessagePacket generatePgpChat(Message message, boolean addDelay) { + MessagePacket packet = preparePacket(message, addDelay); + packet.setBody("This is an XEP-0027 encryted message"); + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getEncryptedBody()); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + packet.addChild("x", "jabber:x:encrypted").setContent( + message.getBody()); + } + return packet; + } + + public MessagePacket generateNotAcceptable(MessagePacket origin) { + MessagePacket packet = generateError(origin); + Element error = packet.addChild("error"); + error.setAttribute("type", "modify"); + error.setAttribute("code", "406"); + error.addChild("not-acceptable"); + return packet; + } + + private MessagePacket generateError(MessagePacket origin) { + MessagePacket packet = new MessagePacket(); + packet.setId(origin.getId()); + packet.setTo(origin.getFrom()); + packet.setBody(origin.getBody()); + packet.setType(MessagePacket.TYPE_ERROR); + return packet; + } + + public MessagePacket confirm(Account account, String to, String id) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(to); + packet.setFrom(account.getFullJid()); + Element received = packet.addChild("displayed", + "urn:xmpp:chat-markers:0"); + received.setAttribute("id", id); + return packet; + } + + public MessagePacket conferenceSubject(Conversation conversation, + String subject) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_GROUPCHAT); + packet.setTo(conversation.getContactJid().split("/", 2)[0]); + Element subjectChild = new Element("subject"); + subjectChild.setContent(subject); + packet.addChild(subjectChild); + packet.setFrom(conversation.getAccount().getJid()); + return packet; + } + + public MessagePacket directInvite(Conversation conversation, String contact) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_NORMAL); + packet.setTo(contact); + packet.setFrom(conversation.getAccount().getFullJid()); + Element x = packet.addChild("x", "jabber:x:conference"); + x.setAttribute("jid", conversation.getContactJid().split("/", 2)[0]); + return packet; + } + + public MessagePacket invite(Conversation conversation, String contact) { + MessagePacket packet = new MessagePacket(); + packet.setTo(conversation.getContactJid().split("/", 2)[0]); + packet.setFrom(conversation.getAccount().getFullJid()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); + Element invite = new Element("invite"); + invite.setAttribute("to", contact); + x.addChild(invite); + packet.addChild(x); + return packet; + } + + public MessagePacket received(Account account, + MessagePacket originalMessage, String namespace) { + MessagePacket receivedPacket = new MessagePacket(); + receivedPacket.setType(MessagePacket.TYPE_NORMAL); + receivedPacket.setTo(originalMessage.getFrom()); + receivedPacket.setFrom(account.getFullJid()); + Element received = receivedPacket.addChild("received", namespace); + received.setAttribute("id", originalMessage.getId()); + return receivedPacket; + } +} diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java new file mode 100644 index 00000000..d896dd00 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -0,0 +1,57 @@ +package eu.siacs.conversations.generator; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public class PresenceGenerator extends AbstractGenerator { + + public PresenceGenerator(XmppConnectionService service) { + super(service); + } + + private PresencePacket subscription(String type, Contact contact) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("type", type); + packet.setAttribute("to", contact.getJid()); + packet.setAttribute("from", contact.getAccount().getJid()); + return packet; + } + + public PresencePacket requestPresenceUpdatesFrom(Contact contact) { + return subscription("subscribe", contact); + } + + public PresencePacket stopPresenceUpdatesFrom(Contact contact) { + return subscription("unsubscribe", contact); + } + + public PresencePacket stopPresenceUpdatesTo(Contact contact) { + return subscription("unsubscribed", contact); + } + + public PresencePacket sendPresenceUpdatesTo(Contact contact) { + return subscription("subscribed", contact); + } + + public PresencePacket sendPresence(Account account) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("from", account.getFullJid()); + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + String capHash = getCapHash(); + if (capHash != null) { + Element cap = packet.addChild("c", + "http://jabber.org/protocol/caps"); + cap.setAttribute("hash", "sha-1"); + cap.setAttribute("node", "http://conversions.siacs.eu"); + cap.setAttribute("ver", capHash); + } + return packet; + } +}
\ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/src/main/java/eu/siacs/conversations/http/HttpConnection.java new file mode 100644 index 00000000..0810e167 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/HttpConnection.java @@ -0,0 +1,270 @@ +package eu.siacs.conversations.http; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; + +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.net.Uri; + +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; + +public class HttpConnection implements Downloadable { + + private HttpConnectionManager mHttpConnectionManager; + private XmppConnectionService mXmppConnectionService; + + private URL mUrl; + private Message message; + private DownloadableFile file; + private int mStatus = Downloadable.STATUS_UNKNOWN; + private boolean acceptedAutomatically = false; + + public HttpConnection(HttpConnectionManager manager) { + this.mHttpConnectionManager = manager; + this.mXmppConnectionService = manager.getXmppConnectionService(); + } + + @Override + public boolean start() { + if (mXmppConnectionService.hasInternetConnection()) { + if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) { + checkFileSize(true); + } else { + new Thread(new FileDownloader(true)).start(); + } + return true; + } else { + return false; + } + } + + public void init(Message message) { + this.message = message; + this.message.setDownloadable(this); + try { + mUrl = new URL(message.getBody()); + String path = mUrl.getPath(); + if (path != null && (path.endsWith(".pgp") || path.endsWith(".gpg"))) { + this.message.setEncryption(Message.ENCRYPTION_PGP); + } else if (message.getEncryption() != Message.ENCRYPTION_OTR) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + this.file = mXmppConnectionService.getFileBackend().getFile( + message, false); + String reference = mUrl.getRef(); + if (reference != null && reference.length() == 96) { + this.file.setKey(CryptoHelper.hexToBytes(reference)); + } + + if (this.message.getEncryption() == Message.ENCRYPTION_OTR + && this.file.getKey() == null) { + this.message.setEncryption(Message.ENCRYPTION_NONE); + } + checkFileSize(false); + } catch (MalformedURLException e) { + this.cancel(); + } + } + + private void checkFileSize(boolean interactive) { + new Thread(new FileSizeChecker(interactive)).start(); + } + + public void cancel() { + mHttpConnectionManager.finishConnection(this); + message.setDownloadable(null); + mXmppConnectionService.updateConversationUi(); + } + + private void finish() { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); + message.setDownloadable(null); + mHttpConnectionManager.finishConnection(this); + mXmppConnectionService.updateConversationUi(); + if (acceptedAutomatically) { + mXmppConnectionService.getNotificationService().push(message); + } + } + + private void changeStatus(int status) { + this.mStatus = status; + mXmppConnectionService.updateConversationUi(); + } + + private void setupTrustManager(HttpsURLConnection connection, + boolean interactive) { + X509TrustManager trustManager; + HostnameVerifier hostnameVerifier; + if (interactive) { + trustManager = mXmppConnectionService.getMemorizingTrustManager(); + hostnameVerifier = mXmppConnectionService + .getMemorizingTrustManager().wrapHostnameVerifier( + new StrictHostnameVerifier()); + } else { + trustManager = mXmppConnectionService.getMemorizingTrustManager() + .getNonInteractive(); + hostnameVerifier = mXmppConnectionService + .getMemorizingTrustManager() + .wrapHostnameVerifierNonInteractive( + new StrictHostnameVerifier()); + } + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, new X509TrustManager[] { trustManager }, + mXmppConnectionService.getRNG()); + connection.setSSLSocketFactory(sc.getSocketFactory()); + connection.setHostnameVerifier(hostnameVerifier); + } catch (KeyManagementException e) { + return; + } catch (NoSuchAlgorithmException e) { + return; + } + } + + private class FileSizeChecker implements Runnable { + + private boolean interactive = false; + + public FileSizeChecker(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + long size; + try { + size = retrieveFileSize(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER_CHECK_FILESIZE); + HttpConnection.this.acceptedAutomatically = false; + HttpConnection.this.mXmppConnectionService.getNotificationService().push(message); + return; + } catch (IOException e) { + cancel(); + return; + } + file.setExpectedSize(size); + if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) { + HttpConnection.this.acceptedAutomatically = true; + new Thread(new FileDownloader(interactive)).start(); + } else { + changeStatus(STATUS_OFFER); + HttpConnection.this.acceptedAutomatically = false; + HttpConnection.this.mXmppConnectionService.getNotificationService().push(message); + } + } + + private long retrieveFileSize() throws IOException, + SSLHandshakeException { + changeStatus(STATUS_CHECKING); + HttpURLConnection connection = (HttpURLConnection) mUrl + .openConnection(); + connection.setRequestMethod("HEAD"); + if (connection instanceof HttpsURLConnection) { + setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + String contentLength = connection.getHeaderField("Content-Length"); + if (contentLength == null) { + throw new IOException(); + } + try { + return Long.parseLong(contentLength, 10); + } catch (NumberFormatException e) { + throw new IOException(); + } + } + + } + + private class FileDownloader implements Runnable { + + private boolean interactive = false; + + public FileDownloader(boolean interactive) { + this.interactive = interactive; + } + + @Override + public void run() { + try { + changeStatus(STATUS_DOWNLOADING); + download(); + updateImageBounds(); + finish(); + } catch (SSLHandshakeException e) { + changeStatus(STATUS_OFFER); + } catch (IOException e) { + cancel(); + } + } + + private void download() throws SSLHandshakeException, IOException { + HttpURLConnection connection = (HttpURLConnection) mUrl + .openConnection(); + if (connection instanceof HttpsURLConnection) { + setupTrustManager((HttpsURLConnection) connection, interactive); + } + connection.connect(); + BufferedInputStream is = new BufferedInputStream( + connection.getInputStream()); + OutputStream os = file.createOutputStream(); + int count = -1; + byte[] buffer = new byte[1024]; + while ((count = is.read(buffer)) != -1) { + os.write(buffer, 0, count); + } + os.flush(); + os.close(); + is.close(); + } + + private void updateImageBounds() { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + message.setBody(mUrl.toString() + "|" + file.getSize() + '|' + + imageWidth + '|' + imageHeight); + message.setType(Message.TYPE_IMAGE); + mXmppConnectionService.updateMessage(message); + } + + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); + } else { + return 0; + } + } +}
\ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java new file mode 100644 index 00000000..9a2a2405 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -0,0 +1,28 @@ +package eu.siacs.conversations.http; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; + +public class HttpConnectionManager extends AbstractConnectionManager { + + public HttpConnectionManager(XmppConnectionService service) { + super(service); + } + + private List<HttpConnection> connections = new CopyOnWriteArrayList<HttpConnection>(); + + public HttpConnection createNewConnection(Message message) { + HttpConnection connection = new HttpConnection(this); + connection.init(message); + this.connections.add(connection); + return connection; + } + + public void finishConnection(HttpConnection connection) { + this.connections.remove(connection); + } +} diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java new file mode 100644 index 00000000..5541c1c6 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -0,0 +1,92 @@ +package eu.siacs.conversations.parser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; + +public abstract class AbstractParser { + + protected XmppConnectionService mXmppConnectionService; + + protected AbstractParser(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + protected long getTimestamp(Element packet) { + long now = System.currentTimeMillis(); + ArrayList<String> stamps = new ArrayList<String>(); + for (Element child : packet.getChildren()) { + if (child.getName().equals("delay")) { + stamps.add(child.getAttribute("stamp").replace("Z", "+0000")); + } + } + Collections.sort(stamps); + if (stamps.size() >= 1) { + try { + String stamp = stamps.get(stamps.size() - 1); + if (stamp.contains(".")) { + Date date = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) + .parse(stamp); + if (now < date.getTime()) { + return now; + } else { + return date.getTime(); + } + } else { + Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", + Locale.US).parse(stamp); + if (now < date.getTime()) { + return now; + } else { + return date.getTime(); + } + } + } catch (ParseException e) { + return now; + } + } else { + return now; + } + } + + protected void updateLastseen(Element packet, Account account, + boolean presenceOverwrite) { + String[] fromParts = packet.getAttribute("from").split("/", 2); + String from = fromParts[0]; + String presence = null; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + Contact contact = account.getRoster().getContact(from); + long timestamp = getTimestamp(packet); + if (timestamp >= contact.lastseen.time) { + contact.lastseen.time = timestamp; + if ((presence != null) && (presenceOverwrite)) { + contact.lastseen.presence = presence; + } + } + } + + protected String avatarData(Element items) { + Element item = items.findChild("item"); + if (item == null) { + return null; + } + Element data = item.findChild("data", "urn:xmpp:avatar:data"); + if (data == null) { + return null; + } + return data.getContent(); + } +} diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java new file mode 100644 index 00000000..df6754f2 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -0,0 +1,92 @@ +package eu.siacs.conversations.parser; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class IqParser extends AbstractParser implements OnIqPacketReceived { + + public IqParser(XmppConnectionService service) { + super(service); + } + + public void rosterItems(Account account, Element query) { + String version = query.getAttribute("ver"); + if (version != null) { + account.getRoster().setVersion(version); + } + for (Element item : query.getChildren()) { + if (item.getName().equals("item")) { + String jid = item.getAttribute("jid"); + String name = item.getAttribute("name"); + String subscription = item.getAttribute("subscription"); + Contact contact = account.getRoster().getContact(jid); + if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { + contact.setServerName(name); + } + if (subscription != null) { + if (subscription.equals("remove")) { + contact.resetOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + } else { + contact.setOption(Contact.Options.IN_ROSTER); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.parseSubscriptionFromElement(item); + } + } + } + } + mXmppConnectionService.updateRosterUi(); + } + + public String avatarData(IqPacket packet) { + Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub == null) { + return null; + } + Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return super.avatarData(items); + } + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.hasChild("query", "jabber:iq:roster")) { + String from = packet.getFrom(); + if ((from == null) || (from.equals(account.getJid()))) { + Element query = packet.findChild("query"); + this.rosterItems(account, query); + } + } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") + || packet.hasChild("data", "http://jabber.org/protocol/ibb")) { + mXmppConnectionService.getJingleConnectionManager() + .deliverIbbPacket(account, packet); + } else if (packet.hasChild("query", + "http://jabber.org/protocol/disco#info")) { + IqPacket response = mXmppConnectionService.getIqGenerator() + .discoResponse(packet); + account.getXmppConnection().sendIqPacket(response, null); + } else if (packet.hasChild("ping", "urn:xmpp:ping")) { + IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT); + mXmppConnectionService.sendIqPacket(account, response, null); + } else { + if ((packet.getType() == IqPacket.TYPE_GET) + || (packet.getType() == IqPacket.TYPE_SET)) { + IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); + Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("feature-not-implemented", + "urn:ietf:params:xml:ns:xmpp-stanzas"); + account.getXmppConnection().sendIqPacket(response, null); + } + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java new file mode 100644 index 00000000..383ac89a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -0,0 +1,525 @@ +package eu.siacs.conversations.parser; + +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.NotificationService; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public class MessageParser extends AbstractParser implements + OnMessagePacketReceived { + public MessageParser(XmppConnectionService service) { + super(service); + } + + private Message parseChat(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + updateLastseen(packet, account, true); + String pgpBody = getPgpBody(packet); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, packet.getFrom(), + pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED); + } else { + finishedMessage = new Message(conversation, packet.getFrom(), + packet.getBody(), Message.ENCRYPTION_NONE, + Message.STATUS_RECEIVED); + } + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + if (conversation.getMode() == Conversation.MODE_MULTI + && fromParts.length >= 2) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setPresence(fromParts[1]); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(fromParts[1])); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + private Message parseOtrChat(MessagePacket packet, Account account) { + boolean properlyAddressed = (packet.getTo().split("/", 2).length == 2) + || (account.countPresences() == 1); + String[] fromParts = packet.getFrom().split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], false); + String presence; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + updateLastseen(packet, account, true); + String body = packet.getBody(); + if (body.matches("^\\?OTRv\\d*\\?")) { + conversation.endOtrIfNeeded(); + } + if (!conversation.hasValidOtrSession()) { + if (properlyAddressed) { + conversation.startOtrSession(mXmppConnectionService, presence, + false); + } else { + return null; + } + } else { + String foreignPresence = conversation.getOtrSession() + .getSessionID().getUserID(); + if (!foreignPresence.equals(presence)) { + conversation.endOtrIfNeeded(); + if (properlyAddressed) { + conversation.startOtrSession(mXmppConnectionService, + presence, false); + } else { + return null; + } + } + } + try { + Session otrSession = conversation.getOtrSession(); + SessionStatus before = otrSession.getSessionStatus(); + body = otrSession.transformReceiving(body); + SessionStatus after = otrSession.getSessionStatus(); + if ((before != after) && (after == SessionStatus.ENCRYPTED)) { + mXmppConnectionService.onOtrSessionEstablished(conversation); + } else if ((before != after) && (after == SessionStatus.FINISHED)) { + conversation.resetOtrSession(); + mXmppConnectionService.updateConversationUi(); + } + if ((body == null) || (body.isEmpty())) { + return null; + } + if (body.startsWith(CryptoHelper.FILETRANSFER)) { + String key = body.substring(CryptoHelper.FILETRANSFER.length()); + conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); + return null; + } + Message finishedMessage = new Message(conversation, + packet.getFrom(), body, Message.ENCRYPTION_OTR, + Message.STATUS_RECEIVED); + finishedMessage.setTime(getTimestamp(packet)); + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + return finishedMessage; + } catch (Exception e) { + String receivedId = packet.getId(); + if (receivedId != null) { + mXmppConnectionService.replyWithNotAcceptable(account, packet); + } + conversation.resetOtrSession(); + return null; + } + } + + private Message parseGroupchat(MessagePacket packet, Account account) { + int status; + String[] fromParts = packet.getFrom().split("/", 2); + if (mXmppConnectionService.find(account.pendingConferenceLeaves, + account, fromParts[0]) != null) { + return null; + } + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, fromParts[0], true); + if (packet.hasChild("subject")) { + conversation.getMucOptions().setSubject( + packet.findChild("subject").getContent()); + mXmppConnectionService.updateConversationUi(); + return null; + } + if ((fromParts.length == 1)) { + return null; + } + String counterPart = fromParts[1]; + if (counterPart.equals(conversation.getMucOptions().getActualNick())) { + if (mXmppConnectionService.markMessage(conversation, + packet.getId(), Message.STATUS_SEND)) { + return null; + } else { + status = Message.STATUS_SEND; + } + } else { + status = Message.STATUS_RECEIVED; + } + String pgpBody = getPgpBody(packet); + Message finishedMessage; + if (pgpBody == null) { + finishedMessage = new Message(conversation, counterPart, + packet.getBody(), Message.ENCRYPTION_NONE, status); + } else { + finishedMessage = new Message(conversation, counterPart, pgpBody, + Message.ENCRYPTION_PGP, status); + } + finishedMessage.setRemoteMsgId(packet.getId()); + finishedMessage.markable = isMarkable(packet); + if (status == Message.STATUS_RECEIVED) { + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(counterPart)); + } + if (packet.hasChild("delay") + && conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + finishedMessage.setTime(getTimestamp(packet)); + return finishedMessage; + } + + private Message parseCarbonMessage(MessagePacket packet, Account account) { + int status; + String fullJid; + Element forwarded; + if (packet.hasChild("received", "urn:xmpp:carbons:2")) { + forwarded = packet.findChild("received", "urn:xmpp:carbons:2") + .findChild("forwarded", "urn:xmpp:forward:0"); + status = Message.STATUS_RECEIVED; + } else if (packet.hasChild("sent", "urn:xmpp:carbons:2")) { + forwarded = packet.findChild("sent", "urn:xmpp:carbons:2") + .findChild("forwarded", "urn:xmpp:forward:0"); + status = Message.STATUS_SEND; + } else { + return null; + } + if (forwarded == null) { + return null; + } + Element message = forwarded.findChild("message"); + if (message == null) { + return null; + } + if (!message.hasChild("body")) { + if (status == Message.STATUS_RECEIVED + && message.getAttribute("from") != null) { + parseNonMessage(message, account); + } else if (status == Message.STATUS_SEND + && message.hasChild("displayed", "urn:xmpp:chat-markers:0")) { + String to = message.getAttribute("to"); + if (to != null) { + Conversation conversation = mXmppConnectionService.find( + mXmppConnectionService.getConversations(), account, + to.split("/")[0]); + if (conversation != null) { + mXmppConnectionService.markRead(conversation, false); + } + } + } + return null; + } + if (status == Message.STATUS_RECEIVED) { + fullJid = message.getAttribute("from"); + if (fullJid == null) { + return null; + } else { + updateLastseen(message, account, true); + } + } else { + fullJid = message.getAttribute("to"); + if (fullJid == null) { + return null; + } + } + String[] parts = fullJid.split("/", 2); + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, parts[0], false); + String pgpBody = getPgpBody(message); + Message finishedMessage; + if (pgpBody != null) { + finishedMessage = new Message(conversation, fullJid, pgpBody, + Message.ENCRYPTION_PGP, status); + } else { + String body = message.findChild("body").getContent(); + finishedMessage = new Message(conversation, fullJid, body, + Message.ENCRYPTION_NONE, status); + } + finishedMessage.setTime(getTimestamp(message)); + finishedMessage.setRemoteMsgId(message.getAttribute("id")); + finishedMessage.markable = isMarkable(message); + if (conversation.getMode() == Conversation.MODE_MULTI + && parts.length >= 2) { + finishedMessage.setType(Message.TYPE_PRIVATE); + finishedMessage.setPresence(parts[1]); + finishedMessage.setTrueCounterpart(conversation.getMucOptions() + .getTrueCounterpart(parts[1])); + if (conversation.hasDuplicateMessage(finishedMessage)) { + return null; + } + } + return finishedMessage; + } + + private void parseError(MessagePacket packet, Account account) { + String[] fromParts = packet.getFrom().split("/", 2); + mXmppConnectionService.markMessage(account, fromParts[0], + packet.getId(), Message.STATUS_SEND_FAILED); + } + + private void parseNonMessage(Element packet, Account account) { + String from = packet.getAttribute("from"); + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, packet.getAttribute("from"), account); + } else if (from != null + && packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) { + String id = packet + .findChild("displayed", "urn:xmpp:chat-markers:0") + .getAttribute("id"); + updateLastseen(packet, account, true); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_DISPLAYED); + } else if (from != null + && packet.hasChild("received", "urn:xmpp:chat-markers:0")) { + String id = packet.findChild("received", "urn:xmpp:chat-markers:0") + .getAttribute("id"); + updateLastseen(packet, account, false); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_RECEIVED); + } else if (from != null + && packet.hasChild("received", "urn:xmpp:receipts")) { + String id = packet.findChild("received", "urn:xmpp:receipts") + .getAttribute("id"); + updateLastseen(packet, account, false); + mXmppConnectionService.markMessage(account, from.split("/", 2)[0], + id, Message.STATUS_SEND_RECEIVED); + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Element x = packet.findChild("x", + "http://jabber.org/protocol/muc#user"); + if (x.hasChild("invite")) { + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, + packet.getAttribute("from"), true); + if (!conversation.getMucOptions().online()) { + if (x.hasChild("password")) { + Element password = x.findChild("password"); + conversation.getMucOptions().setPassword( + password.getContent()); + mXmppConnectionService.databaseBackend + .updateConversation(conversation); + } + mXmppConnectionService.joinMuc(conversation); + mXmppConnectionService.updateConversationUi(); + } + } + } else if (packet.hasChild("x", "jabber:x:conference")) { + Element x = packet.findChild("x", "jabber:x:conference"); + String jid = x.getAttribute("jid"); + String password = x.getAttribute("password"); + if (jid != null) { + Conversation conversation = mXmppConnectionService + .findOrCreateConversation(account, jid, true); + if (!conversation.getMucOptions().online()) { + if (password != null) { + conversation.getMucOptions().setPassword(password); + mXmppConnectionService.databaseBackend + .updateConversation(conversation); + } + mXmppConnectionService.joinMuc(conversation); + mXmppConnectionService.updateConversationUi(); + } + } + } + } + + private void parseEvent(Element event, String from, Account account) { + Element items = event.findChild("items"); + String node = items.getAttribute("node"); + if (node != null) { + if (node.equals("urn:xmpp:avatar:metadata")) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = from; + if (mXmppConnectionService.getFileBackend().isAvatarCached( + avatar)) { + if (account.getJid().equals(from)) { + if (account.setAvatar(avatar.getFilename())) { + mXmppConnectionService.databaseBackend + .updateAccount(account); + } + mXmppConnectionService.getAvatarService().clear( + account); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateAccountUi(); + } else { + Contact contact = account.getRoster().getContact( + from); + contact.setAvatar(avatar.getFilename()); + mXmppConnectionService.getAvatarService().clear( + contact); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); + } + } else { + mXmppConnectionService.fetchAvatar(account, avatar); + } + } + } else if (node.equals("http://jabber.org/protocol/nick")) { + Element item = items.findChild("item"); + if (item != null) { + Element nick = item.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + if (from != null) { + Contact contact = account.getRoster().getContact( + from); + contact.setPresenceName(nick.getContent()); + } + } + } + } + } + } + + private String getPgpBody(Element message) { + Element child = message.findChild("x", "jabber:x:encrypted"); + if (child == null) { + return null; + } else { + return child.getContent(); + } + } + + private boolean isMarkable(Element message) { + return message.hasChild("markable", "urn:xmpp:chat-markers:0"); + } + + @Override + public void onMessagePacketReceived(Account account, MessagePacket packet) { + Message message = null; + boolean notify = mXmppConnectionService.getPreferences().getBoolean( + "show_notification", true); + boolean alwaysNotifyInConference = notify + && mXmppConnectionService.getPreferences().getBoolean( + "always_notify_in_conference", false); + + this.parseNick(packet, account); + + if ((packet.getType() == MessagePacket.TYPE_CHAT || packet.getType() == MessagePacket.TYPE_NORMAL)) { + if ((packet.getBody() != null) + && (packet.getBody().startsWith("?OTR"))) { + message = this.parseOtrChat(packet, account); + if (message != null) { + message.markUnread(); + } + } else if (packet.hasChild("body") + && !(packet.hasChild("x", + "http://jabber.org/protocol/muc#user"))) { + message = this.parseChat(packet, account); + if (message != null) { + message.markUnread(); + } + } else if (packet.hasChild("received", "urn:xmpp:carbons:2") + || (packet.hasChild("sent", "urn:xmpp:carbons:2"))) { + message = this.parseCarbonMessage(packet, account); + if (message != null) { + if (message.getStatus() == Message.STATUS_SEND) { + account.activateGracePeriod(); + notify = false; + mXmppConnectionService.markRead( + message.getConversation(), false); + } else { + message.markUnread(); + } + } + } else { + parseNonMessage(packet, account); + } + } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { + message = this.parseGroupchat(packet, account); + if (message != null) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + message.markUnread(); + notify = alwaysNotifyInConference + || NotificationService + .wasHighlightedOrPrivate(message); + } else { + mXmppConnectionService.markRead(message.getConversation(), + false); + account.activateGracePeriod(); + notify = false; + } + } + } else if (packet.getType() == MessagePacket.TYPE_ERROR) { + this.parseError(packet, account); + return; + } else if (packet.getType() == MessagePacket.TYPE_HEADLINE) { + this.parseHeadline(packet, account); + return; + } + if ((message == null) || (message.getBody() == null)) { + return; + } + if ((mXmppConnectionService.confirmMessages()) + && ((packet.getId() != null))) { + if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, + "urn:xmpp:chat-markers:0"); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + if (packet.hasChild("request", "urn:xmpp:receipts")) { + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, + "urn:xmpp:receipts"); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + } + Conversation conversation = message.getConversation(); + conversation.add(message); + + if (message.getStatus() == Message.STATUS_RECEIVED + && conversation.getOtrSession() != null + && !conversation.getOtrSession().getSessionID().getUserID() + .equals(message.getPresence())) { + conversation.endOtrIfNeeded(); + } + + if (packet.getType() != MessagePacket.TYPE_ERROR) { + if (message.getEncryption() == Message.ENCRYPTION_NONE + || mXmppConnectionService.saveEncryptedMessages()) { + mXmppConnectionService.databaseBackend.createMessage(message); + } + } + if (message.trusted() && message.bodyContainsDownloadable()) { + this.mXmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + notify = false; + } + notify = notify && !conversation.isMuted(); + if (notify) { + mXmppConnectionService.getNotificationService().push(message); + } + mXmppConnectionService.updateConversationUi(); + } + + private void parseHeadline(MessagePacket packet, Account account) { + if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { + Element event = packet.findChild("event", + "http://jabber.org/protocol/pubsub#event"); + parseEvent(event, packet.getFrom(), account); + } + } + + private void parseNick(MessagePacket packet, Account account) { + Element nick = packet.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + if (packet.getFrom() != null) { + Contact contact = account.getRoster().getContact( + packet.getFrom()); + contact.setPresenceName(nick.getContent()); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java new file mode 100644 index 00000000..4e90cda8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -0,0 +1,133 @@ +package eu.siacs.conversations.parser; + +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.generator.PresenceGenerator; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnPresencePacketReceived; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public class PresenceParser extends AbstractParser implements + OnPresencePacketReceived { + + public PresenceParser(XmppConnectionService service) { + super(service); + } + + public void parseConferencePresence(PresencePacket packet, Account account) { + PgpEngine mPgpEngine = mXmppConnectionService.getPgpEngine(); + if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + Conversation muc = mXmppConnectionService.find(account, packet + .getAttribute("from").split("/", 2)[0]); + if (muc != null) { + boolean before = muc.getMucOptions().online(); + muc.getMucOptions().processPacket(packet, mPgpEngine); + if (before != muc.getMucOptions().online()) { + mXmppConnectionService.updateConversationUi(); + } + mXmppConnectionService.getAvatarService().clear(muc); + } + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + Conversation muc = mXmppConnectionService.find(account, packet + .getAttribute("from").split("/", 2)[0]); + if (muc != null) { + boolean before = muc.getMucOptions().online(); + muc.getMucOptions().processPacket(packet, mPgpEngine); + if (before != muc.getMucOptions().online()) { + mXmppConnectionService.updateConversationUi(); + } + mXmppConnectionService.getAvatarService().clear(muc); + } + } + } + + public void parseContactPresence(PresencePacket packet, Account account) { + PresenceGenerator mPresenceGenerator = mXmppConnectionService + .getPresenceGenerator(); + if (packet.getFrom() == null) { + return; + } + String[] fromParts = packet.getFrom().split("/", 2); + String type = packet.getAttribute("type"); + if (fromParts[0].equals(account.getJid())) { + if (fromParts.length == 2) { + if (type == null) { + account.updatePresence(fromParts[1], + Presences.parseShow(packet.findChild("show"))); + } else if (type.equals("unavailable")) { + account.removePresence(fromParts[1]); + account.deactivateGracePeriod(); + } + } + } else { + Contact contact = account.getRoster().getContact(packet.getFrom()); + if (type == null) { + String presence; + if (fromParts.length >= 2) { + presence = fromParts[1]; + } else { + presence = ""; + } + int sizeBefore = contact.getPresences().size(); + contact.updatePresence(presence, + Presences.parseShow(packet.findChild("show"))); + PgpEngine pgp = mXmppConnectionService.getPgpEngine(); + if (pgp != null) { + Element x = packet.findChild("x", "jabber:x:signed"); + if (x != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + contact.setPgpKeyId(pgp.fetchKeyId(account, msg, + x.getContent())); + } + } + boolean online = sizeBefore < contact.getPresences().size(); + updateLastseen(packet, account, true); + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact, online); + } else if (type.equals("unavailable")) { + if (fromParts.length != 2) { + contact.clearPresences(); + } else { + contact.removePresence(fromParts[1]); + } + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact, false); + } else if (type.equals("subscribe")) { + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + mXmppConnectionService.sendPresencePacket(account, + mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } else { + contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); + } + } + Element nick = packet.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + contact.setPresenceName(nick.getContent()); + } + } + mXmppConnectionService.updateRosterUi(); + } + + @Override + public void onPresencePacketReceived(Account account, PresencePacket packet) { + if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) { + this.parseConferencePresence(packet, account); + } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { + this.parseConferencePresence(packet, account); + } else { + this.parseContactPresence(packet, account); + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java new file mode 100644 index 00000000..12e5e251 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -0,0 +1,353 @@ +package eu.siacs.conversations.persistance; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Roster; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteCantOpenDatabaseException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +public class DatabaseBackend extends SQLiteOpenHelper { + + private static DatabaseBackend instance = null; + + private static final String DATABASE_NAME = "history"; + private static final int DATABASE_VERSION = 8; + + private static String CREATE_CONTATCS_STATEMENT = "create table " + + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " + + Contact.SERVERNAME + " TEXT, " + Contact.SYSTEMNAME + " TEXT," + + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," + + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," + + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " + + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + + Account.TABLENAME + "(" + Account.UUID + + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " + + Contact.JID + ") ON CONFLICT REPLACE);"; + + private DatabaseBackend(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("PRAGMA foreign_keys=ON;"); + db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID + + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT," + + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT," + + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS + + " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS + + " TEXT)"); + db.execSQL("create table " + Conversation.TABLENAME + " (" + + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME + + " TEXT, " + Conversation.CONTACT + " TEXT, " + + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID + + " TEXT, " + Conversation.CREATED + " NUMBER, " + + Conversation.STATUS + " NUMBER, " + Conversation.MODE + + " NUMBER, " + Conversation.ATTRIBUTES + " TEXT, FOREIGN KEY(" + + Conversation.ACCOUNT + ") REFERENCES " + Account.TABLENAME + + "(" + Account.UUID + ") ON DELETE CASCADE);"); + db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID + + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, " + + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART + + " TEXT, " + Message.TRUE_COUNTERPART + " TEXT," + + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " + + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + + Message.CONVERSATION + ") REFERENCES " + + Conversation.TABLENAME + "(" + Conversation.UUID + + ") ON DELETE CASCADE);"); + + db.execSQL(CREATE_CONTATCS_STATEMENT); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2 && newVersion >= 2) { + db.execSQL("update " + Account.TABLENAME + " set " + + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); + } + if (oldVersion < 3 && newVersion >= 3) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TYPE + " NUMBER"); + } + if (oldVersion < 5 && newVersion >= 5) { + db.execSQL("DROP TABLE " + Contact.TABLENAME); + db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL("UPDATE " + Account.TABLENAME + " SET " + + Account.ROSTERVERSION + " = NULL"); + } + if (oldVersion < 6 && newVersion >= 6) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.TRUE_COUNTERPART + " TEXT"); + } + if (oldVersion < 7 && newVersion >= 7) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.REMOTE_MSG_ID + " TEXT"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.AVATAR + " TEXT"); + db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + + Account.AVATAR + " TEXT"); + } + if (oldVersion < 8 && newVersion >= 8) { + db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " + + Conversation.ATTRIBUTES + " TEXT"); + } + } + + public static synchronized DatabaseBackend getInstance(Context context) { + if (instance == null) { + instance = new DatabaseBackend(context); + } + return instance; + } + + public void createConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Conversation.TABLENAME, null, conversation.getContentValues()); + } + + public void createMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Message.TABLENAME, null, message.getContentValues()); + } + + public void createAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Account.TABLENAME, null, account.getContentValues()); + } + + public void createContact(Contact contact) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } + + public int getConversationCount() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("select count(uuid) as count from " + + Conversation.TABLENAME + " where " + Conversation.STATUS + + "=" + Conversation.STATUS_AVAILABLE, null); + cursor.moveToFirst(); + return cursor.getInt(0); + } + + public CopyOnWriteArrayList<Conversation> getConversations(int status) { + CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<Conversation>(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { Integer.toString(status) }; + Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME + + " where " + Conversation.STATUS + " = ? order by " + + Conversation.CREATED + " desc", selectionArgs); + while (cursor.moveToNext()) { + list.add(Conversation.fromCursor(cursor)); + } + return list; + } + + public ArrayList<Message> getMessages(Conversation conversations, int limit) { + return getMessages(conversations, limit, -1); + } + + public ArrayList<Message> getMessages(Conversation conversation, int limit, + long timestamp) { + ArrayList<Message> list = new ArrayList<Message>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + if (timestamp == -1) { + String[] selectionArgs = { conversation.getUuid() }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=?", selectionArgs, null, null, Message.TIME_SENT + + " DESC", String.valueOf(limit)); + } else { + String[] selectionArgs = { conversation.getUuid(), + Long.toString(timestamp) }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? and " + Message.TIME_SENT + "<?", selectionArgs, + null, null, Message.TIME_SENT + " DESC", + String.valueOf(limit)); + } + if (cursor.getCount() > 0) { + cursor.moveToLast(); + do { + Message message = Message.fromCursor(cursor); + message.setConversation(conversation); + list.add(message); + } while (cursor.moveToPrevious()); + } + return list; + } + + public Conversation findConversation(Account account, String contactJid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { account.getUuid(), contactJid + "%" }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + + " like ?", selectionArgs, null, null, null); + if (cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + return Conversation.fromCursor(cursor); + } + + public void updateConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { conversation.getUuid() }; + db.update(Conversation.TABLENAME, conversation.getContentValues(), + Conversation.UUID + "=?", args); + } + + public List<Account> getAccounts() { + List<Account> list = new ArrayList<Account>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.query(Account.TABLENAME, null, null, null, null, + null, null); + while (cursor.moveToNext()) { + list.add(Account.fromCursor(cursor)); + } + cursor.close(); + return list; + } + + public void updateAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { account.getUuid() }; + db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + + "=?", args); + } + + public void deleteAccount(Account account) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { account.getUuid() }; + db.delete(Account.TABLENAME, Account.UUID + "=?", args); + } + + public boolean hasEnabledAccounts() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from " + + Account.TABLENAME + " where not options & (1 <<1)", null); + try { + cursor.moveToFirst(); + int count = cursor.getInt(0); + cursor.close(); + return (count > 0); + } catch (SQLiteCantOpenDatabaseException e) { + return true; // better safe than sorry + } + } + + @Override + public SQLiteDatabase getWritableDatabase() { + SQLiteDatabase db = super.getWritableDatabase(); + db.execSQL("PRAGMA foreign_keys=ON;"); + return db; + } + + public void updateMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { message.getUuid() }; + db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + + "=?", args); + } + + public void readRoster(Roster roster) { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + String args[] = { roster.getAccount().getUuid() }; + cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", + args, null, null, null); + while (cursor.moveToNext()) { + roster.initContact(Contact.fromCursor(cursor)); + } + cursor.close(); + } + + public void writeRoster(Roster roster) { + Account account = roster.getAccount(); + SQLiteDatabase db = this.getWritableDatabase(); + for (Contact contact : roster.getContacts()) { + if (contact.getOption(Contact.Options.IN_ROSTER)) { + db.insert(Contact.TABLENAME, null, contact.getContentValues()); + } else { + String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?"; + String[] whereArgs = { account.getUuid(), contact.getJid() }; + db.delete(Contact.TABLENAME, where, whereArgs); + } + } + account.setRosterVersion(roster.getVersion()); + updateAccount(account); + } + + public void deleteMessage(Message message) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { message.getUuid() }; + db.delete(Message.TABLENAME, Message.UUID + "=?", args); + } + + public void deleteMessagesInConversation(Conversation conversation) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = { conversation.getUuid() }; + db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); + } + + public Conversation findConversationByUuid(String conversationUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { conversationUuid }; + Cursor cursor = db.query(Conversation.TABLENAME, null, + Conversation.UUID + "=?", selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Conversation.fromCursor(cursor); + } + + public Message findMessageByUuid(String messageUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { messageUuid }; + Cursor cursor = db.query(Message.TABLENAME, null, Message.UUID + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Message.fromCursor(cursor); + } + + public Account findAccountByUuid(String accountUuid) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { accountUuid }; + Cursor cursor = db.query(Account.TABLENAME, null, Account.UUID + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + return Account.fromCursor(cursor); + } + + public List<Message> getImageMessages(Conversation conversation) { + ArrayList<Message> list = new ArrayList<Message>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + String[] selectionArgs = { conversation.getUuid(), String.valueOf(Message.TYPE_IMAGE) }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? AND "+Message.TYPE+"=?", selectionArgs, null, null,null); + if (cursor.getCount() > 0) { + cursor.moveToLast(); + do { + Message message = Message.fromCursor(cursor); + message.setConversation(conversation); + list.add(message); + } while (cursor.moveToPrevious()); + } + return list; + } +} diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java new file mode 100644 index 00000000..13daa27b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -0,0 +1,447 @@ +package eu.siacs.conversations.persistance; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.util.Base64; +import android.util.Base64OutputStream; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.ExifHelper; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class FileBackend { + + private static int IMAGE_SIZE = 1920; + + private SimpleDateFormat imageDateFormat = new SimpleDateFormat( + "yyyyMMdd_HHmmssSSS", Locale.US); + + private XmppConnectionService mXmppConnectionService; + + public FileBackend(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public DownloadableFile getFile(Message message) { + return getFile(message, true); + } + + public DownloadableFile getFile(Message message, boolean decrypted) { + StringBuilder filename = new StringBuilder(); + filename.append(getConversationsDirectory()); + filename.append(message.getUuid()); + if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { + filename.append(".webp"); + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + filename.append(".webp"); + } else { + filename.append(".webp.pgp"); + } + } + return new DownloadableFile(filename.toString()); + } + + public static String getConversationsDirectory() { + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES).getAbsolutePath() + + "/Conversations/"; + } + + public Bitmap resize(Bitmap originalBitmap, int size) { + int w = originalBitmap.getWidth(); + int h = originalBitmap.getHeight(); + if (Math.max(w, h) > size) { + int scalledW; + int scalledH; + if (w <= h) { + scalledW = (int) (w / ((double) h / size)); + scalledH = size; + } else { + scalledW = size; + scalledH = (int) (h / ((double) w / size)); + } + Bitmap scalledBitmap = Bitmap.createScaledBitmap(originalBitmap, + scalledW, scalledH, true); + return scalledBitmap; + } else { + return originalBitmap; + } + } + + public Bitmap rotate(Bitmap bitmap, int degree) { + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + Matrix mtx = new Matrix(); + mtx.postRotate(degree); + return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); + } + + public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) + throws ImageCopyException { + return this.copyImageToPrivateStorage(message, image, 0); + } + + private DownloadableFile copyImageToPrivateStorage(Message message, + Uri image, int sampleSize) throws ImageCopyException { + try { + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + DownloadableFile file = getFile(message); + file.getParentFile().mkdirs(); + file.createNewFile(); + Bitmap originalBitmap; + BitmapFactory.Options options = new BitmapFactory.Options(); + int inSampleSize = (int) Math.pow(2, sampleSize); + Log.d(Config.LOGTAG, "reading bitmap with sample size " + + inSampleSize); + options.inSampleSize = inSampleSize; + originalBitmap = BitmapFactory.decodeStream(is, null, options); + is.close(); + if (originalBitmap == null) { + throw new ImageCopyException(R.string.error_not_an_image_file); + } + Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE); + originalBitmap = null; + int rotation = getRotation(image); + if (rotation > 0) { + scalledBitmap = rotate(scalledBitmap, rotation); + } + OutputStream os = new FileOutputStream(file); + boolean success = scalledBitmap.compress( + Bitmap.CompressFormat.WEBP, 75, os); + if (!success) { + throw new ImageCopyException(R.string.error_compressing_image); + } + os.flush(); + os.close(); + long size = file.getSize(); + int width = scalledBitmap.getWidth(); + int height = scalledBitmap.getHeight(); + message.setBody(Long.toString(size) + ',' + width + ',' + height); + return file; + } catch (FileNotFoundException e) { + throw new ImageCopyException(R.string.error_file_not_found); + } catch (IOException e) { + throw new ImageCopyException(R.string.error_io_exception); + } catch (SecurityException e) { + throw new ImageCopyException( + R.string.error_security_exception_during_image_copy); + } catch (OutOfMemoryError e) { + ++sampleSize; + if (sampleSize <= 3) { + return copyImageToPrivateStorage(message, image, sampleSize); + } else { + throw new ImageCopyException(R.string.error_out_of_memory); + } + } + } + + private int getRotation(Uri image) { + if ("content".equals(image.getScheme())) { + try { + Cursor cursor = mXmppConnectionService + .getContentResolver() + .query(image, + new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, + null, null, null); + if (cursor.getCount() != 1) { + return -1; + } + cursor.moveToFirst(); + return cursor.getInt(0); + } catch (IllegalArgumentException e) { + return -1; + } + } else { + try { + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + return ExifHelper.getOrientation(is); + } catch (FileNotFoundException e) { + return 0; + } + } + } + + public Bitmap getImageFromMessage(Message message) { + return BitmapFactory.decodeFile(getFile(message).getAbsolutePath()); + } + + public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) + throws FileNotFoundException { + Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get( + message.getUuid()); + if ((thumbnail == null) && (!cacheOnly)) { + File file = getFile(message); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(file, size); + Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(), + options); + if (fullsize == null) { + throw new FileNotFoundException(); + } + thumbnail = resize(fullsize, size); + this.mXmppConnectionService.getBitmapCache().put(message.getUuid(), + thumbnail); + } + return thumbnail; + } + + public Uri getTakePhotoUri() { + StringBuilder pathBuilder = new StringBuilder(); + pathBuilder.append(Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)); + pathBuilder.append('/'); + pathBuilder.append("Camera"); + pathBuilder.append('/'); + pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + + ".jpg"); + Uri uri = Uri.parse("file://" + pathBuilder.toString()); + File file = new File(uri.toString()); + file.getParentFile().mkdirs(); + return uri; + } + + public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) { + try { + Avatar avatar = new Avatar(); + Bitmap bm = cropCenterSquare(image, size); + if (bm == null) { + return null; + } + ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream(); + Base64OutputStream mBase64OutputSttream = new Base64OutputStream( + mByteArrayOutputStream, Base64.DEFAULT); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mBase64OutputSttream, digest); + if (!bm.compress(format, 75, mDigestOutputStream)) { + return null; + } + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest()); + avatar.image = new String(mByteArrayOutputStream.toByteArray()); + return avatar; + } catch (NoSuchAlgorithmException e) { + return null; + } catch (IOException e) { + return null; + } + } + + public boolean isAvatarCached(Avatar avatar) { + File file = new File(getAvatarPath(avatar.getFilename())); + return file.exists(); + } + + public boolean save(Avatar avatar) { + if (isAvatarCached(avatar)) { + return true; + } + String filename = getAvatarPath(avatar.getFilename()); + File file = new File(filename + ".tmp"); + file.getParentFile().mkdirs(); + try { + file.createNewFile(); + FileOutputStream mFileOutputStream = new FileOutputStream(file); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mFileOutputStream, digest); + mDigestOutputStream.write(avatar.getImageAsBytes()); + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + avatar.size = file.length(); + String sha1sum = CryptoHelper.bytesToHex(digest.digest()); + if (sha1sum.equals(avatar.sha1sum)) { + file.renameTo(new File(filename)); + return true; + } else { + Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); + file.delete(); + return false; + } + } catch (FileNotFoundException e) { + return false; + } catch (IOException e) { + return false; + } catch (NoSuchAlgorithmException e) { + return false; + } + } + + public String getAvatarPath(String avatar) { + return mXmppConnectionService.getFilesDir().getAbsolutePath() + + "/avatars/" + avatar; + } + + public Uri getAvatarUri(String avatar) { + return Uri.parse("file:" + getAvatarPath(avatar)); + } + + public Bitmap cropCenterSquare(Uri image, int size) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, size); + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + Bitmap input = BitmapFactory.decodeStream(is, null, options); + if (input == null) { + return null; + } else { + int rotation = getRotation(image); + if (rotation > 0) { + input = rotate(input, rotation); + } + return cropCenterSquare(input, size); + } + } catch (FileNotFoundException e) { + return null; + } + } + + public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, + Math.max(newHeight, newWidth)); + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + Bitmap source = BitmapFactory.decodeStream(is, null, options); + + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + float xScale = (float) newWidth / sourceWidth; + float yScale = (float) newHeight / sourceHeight; + float scale = Math.max(xScale, yScale); + float scaledWidth = scale * sourceWidth; + float scaledHeight = scale * sourceHeight; + float left = (newWidth - scaledWidth) / 2; + float top = (newHeight - scaledHeight) / 2; + + RectF targetRect = new RectF(left, top, left + scaledWidth, top + + scaledHeight); + Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, + source.getConfig()); + Canvas canvas = new Canvas(dest); + canvas.drawBitmap(source, null, targetRect, null); + + return dest; + } catch (FileNotFoundException e) { + return null; + } + + } + + public Bitmap cropCenterSquare(Bitmap input, int size) { + int w = input.getWidth(); + int h = input.getHeight(); + + float scale = Math.max((float) size / h, (float) size / w); + + float outWidth = scale * w; + float outHeight = scale * h; + float left = (size - outWidth) / 2; + float top = (size - outHeight) / 2; + RectF target = new RectF(left, top, left + outWidth, top + outHeight); + + Bitmap output = Bitmap.createBitmap(size, size, input.getConfig()); + Canvas canvas = new Canvas(output); + canvas.drawBitmap(input, null, target, null); + return output; + } + + private int calcSampleSize(Uri image, int size) + throws FileNotFoundException { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver() + .openInputStream(image), null, options); + return calcSampleSize(options, size); + } + + private int calcSampleSize(File image, int size) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(image.getAbsolutePath(), options); + return calcSampleSize(options, size); + } + + private int calcSampleSize(BitmapFactory.Options options, int size) { + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + + if (height > size || width > size) { + int halfHeight = height / 2; + int halfWidth = width / 2; + + while ((halfHeight / inSampleSize) > size + && (halfWidth / inSampleSize) > size) { + inSampleSize *= 2; + } + } + return inSampleSize; + } + + public Uri getJingleFileUri(Message message) { + File file = getFile(message); + return Uri.parse("file://" + file.getAbsolutePath()); + } + + public class ImageCopyException extends Exception { + private static final long serialVersionUID = -1010013599132881427L; + private int resId; + + public ImageCopyException(int resId) { + this.resId = resId; + } + + public int getResId() { + return resId; + } + } + + public Bitmap getAvatar(String avatar, int size) { + if (avatar == null) { + return null; + } + Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); + if (bm == null) { + return null; + } + return bm; + } + + public boolean isFileAvailable(Message message) { + return getFile(message).exists(); + } +} diff --git a/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java new file mode 100644 index 00000000..6a457b17 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.persistance; + +public interface OnPhoneContactsMerged { + public void phoneContactsMerged(); +} diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java new file mode 100644 index 00000000..676a09c9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -0,0 +1,23 @@ +package eu.siacs.conversations.services; + +public class AbstractConnectionManager { + protected XmppConnectionService mXmppConnectionService; + + public AbstractConnectionManager(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public XmppConnectionService getXmppConnectionService() { + return this.mXmppConnectionService; + } + + public long getAutoAcceptFileSize() { + String config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size", "524288"); + try { + return Long.parseLong(config); + } catch (NumberFormatException e) { + return 524288; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java new file mode 100644 index 00000000..bd27b555 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -0,0 +1,292 @@ +package eu.siacs.conversations.services; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.entities.MucOptions; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.net.Uri; + +public class AvatarService { + + private static final int FG_COLOR = 0xFFFAFAFA; + private static final int TRANSPARENT = 0x00000000; + + private static final String PREFIX_CONTACT = "contact"; + private static final String PREFIX_CONVERSATION = "conversation"; + private static final String PREFIX_ACCOUNT = "account"; + private static final String PREFIX_GENERIC = "generic"; + + private ArrayList<Integer> sizes = new ArrayList<Integer>(); + + protected XmppConnectionService mXmppConnectionService = null; + + public AvatarService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public Bitmap get(Contact contact, int size) { + final String KEY = key(contact, size); + Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); + if (avatar != null) { + return avatar; + } + avatar = mXmppConnectionService.getFileBackend().getAvatar( + contact.getAvatar(), size); + if (avatar == null) { + if (contact.getProfilePhoto() != null) { + avatar = mXmppConnectionService.getFileBackend() + .cropCenterSquare(Uri.parse(contact.getProfilePhoto()), + size); + if (avatar == null) { + avatar = get(contact.getDisplayName(), size); + } + } else { + avatar = get(contact.getDisplayName(), size); + } + } + this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); + return avatar; + } + + public void clear(Contact contact) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(contact, size)); + } + } + + private String key(Contact contact, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_CONTACT + "_" + contact.getAccount().getJid() + "_" + + contact.getJid() + "_" + String.valueOf(size); + } + + public Bitmap get(ListItem item, int size) { + if (item instanceof Contact) { + return get((Contact) item, size); + } else if (item instanceof Bookmark) { + Bookmark bookmark = (Bookmark) item; + if (bookmark.getConversation() != null) { + return get(bookmark.getConversation(), size); + } else { + return get(bookmark.getDisplayName(), size); + } + } else { + return get(item.getDisplayName(), size); + } + } + + public Bitmap get(Conversation conversation, int size) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + return get(conversation.getContact(), size); + } else { + return get(conversation.getMucOptions(), size); + } + } + + public void clear(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + clear(conversation.getContact()); + } else { + clear(conversation.getMucOptions()); + } + } + + public Bitmap get(MucOptions mucOptions, int size) { + final String KEY = key(mucOptions, size); + Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); + if (bitmap != null) { + return bitmap; + } + List<MucOptions.User> users = mucOptions.getUsers(); + int count = users.size(); + bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + bitmap.eraseColor(TRANSPARENT); + + if (count == 0) { + String name = mucOptions.getConversation().getName(); + String letter = name.substring(0, 1); + int color = this.getColorForName(name); + drawTile(canvas, letter, color, 0, 0, size, size); + } else if (count == 1) { + drawTile(canvas, users.get(0), 0, 0, size, size); + } else if (count == 2) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); + drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size); + } else if (count == 3) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size); + drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size, + size); + } else if (count == 4) { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1); + drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size); + drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size, + size); + } else { + drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1); + drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size); + drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1); + drawTile(canvas, "\u2026", 0xFF202020, size / 2 + 1, size / 2 + 1, + size, size); + } + this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + return bitmap; + } + + public void clear(MucOptions options) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(options, size)); + } + } + + private String key(MucOptions options, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid() + + "_" + String.valueOf(size); + } + + public Bitmap get(Account account, int size) { + final String KEY = key(account, size); + Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY); + if (avatar != null) { + return avatar; + } + avatar = mXmppConnectionService.getFileBackend().getAvatar( + account.getAvatar(), size); + if (avatar == null) { + avatar = get(account.getJid(), size); + } + mXmppConnectionService.getBitmapCache().put(KEY, avatar); + return avatar; + } + + public void clear(Account account) { + for (Integer size : sizes) { + this.mXmppConnectionService.getBitmapCache().remove( + key(account, size)); + } + } + + private String key(Account account, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_ACCOUNT + "_" + account.getUuid() + "_" + + String.valueOf(size); + } + + public Bitmap get(String name, int size) { + final String KEY = key(name, size); + Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); + if (bitmap != null) { + return bitmap; + } + bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + String letter = name.substring(0, 1); + int color = this.getColorForName(name); + drawTile(canvas, letter, color, 0, 0, size, size); + mXmppConnectionService.getBitmapCache().put(KEY, bitmap); + return bitmap; + } + + private String key(String name, int size) { + synchronized (this.sizes) { + if (!this.sizes.contains(size)) { + this.sizes.add(size); + } + } + return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size); + } + + private void drawTile(Canvas canvas, String letter, int tileColor, + int left, int top, int right, int bottom) { + letter = letter.toUpperCase(Locale.getDefault()); + Paint tilePaint = new Paint(), textPaint = new Paint(); + tilePaint.setColor(tileColor); + textPaint.setFlags(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(FG_COLOR); + textPaint.setTypeface(Typeface.create("sans-serif-light", + Typeface.NORMAL)); + textPaint.setTextSize((float) ((right - left) * 0.8)); + Rect rect = new Rect(); + + canvas.drawRect(new Rect(left, top, right, bottom), tilePaint); + textPaint.getTextBounds(letter, 0, 1, rect); + float width = textPaint.measureText(letter); + canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom) + / 2 + rect.height() / 2, textPaint); + } + + private void drawTile(Canvas canvas, MucOptions.User user, int left, + int top, int right, int bottom) { + Contact contact = user.getContact(); + if (contact != null) { + Uri uri = null; + if (contact.getAvatar() != null) { + uri = mXmppConnectionService.getFileBackend().getAvatarUri( + contact.getAvatar()); + } else if (contact.getProfilePhoto() != null) { + uri = Uri.parse(contact.getProfilePhoto()); + } + if (uri != null) { + Bitmap bitmap = mXmppConnectionService.getFileBackend() + .cropCenter(uri, bottom - top, right - left); + if (bitmap != null) { + drawTile(canvas, bitmap, left, top, right, bottom); + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } else { + String letter = user.getName().substring(0, 1); + int color = this.getColorForName(user.getName()); + drawTile(canvas, letter, color, left, top, right, bottom); + } + } + + private void drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop, + int dstright, int dstbottom) { + Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom); + canvas.drawBitmap(bm, null, dst, null); + } + + private int getColorForName(String name) { + int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5, + 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722, + 0xFF795548, 0xFF607d8b }; + return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)]; + } + +} diff --git a/src/main/java/eu/siacs/conversations/services/EventReceiver.java b/src/main/java/eu/siacs/conversations/services/EventReceiver.java new file mode 100644 index 00000000..dfbe9db7 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/EventReceiver.java @@ -0,0 +1,24 @@ +package eu.siacs.conversations.services; + +import eu.siacs.conversations.persistance.DatabaseBackend; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class EventReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Intent mIntentForService = new Intent(context, + XmppConnectionService.class); + if (intent.getAction() != null) { + mIntentForService.setAction(intent.getAction()); + } else { + mIntentForService.setAction("other"); + } + if (intent.getAction().equals("ui") + || DatabaseBackend.getInstance(context).hasEnabledAccounts()) { + context.startService(mIntentForService); + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java new file mode 100644 index 00000000..7b2e16df --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -0,0 +1,333 @@ +package eu.siacs.conversations.services; + +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.PowerManager; +import android.os.SystemClock; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.BigPictureStyle; +import android.support.v4.app.NotificationCompat.Builder; +import android.support.v4.app.TaskStackBuilder; +import android.text.Html; +import android.util.DisplayMetrics; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.ConversationActivity; + +public class NotificationService { + + private XmppConnectionService mXmppConnectionService; + + private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>(); + + public static int NOTIFICATION_ID = 0x2342; + private Conversation mOpenConversation; + private boolean mIsInForeground; + private long mLastNotification; + + public NotificationService(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public void push(Message message) { + PowerManager pm = (PowerManager) mXmppConnectionService + .getSystemService(Context.POWER_SERVICE); + boolean isScreenOn = pm.isScreenOn(); + + if (this.mIsInForeground && isScreenOn + && this.mOpenConversation == message.getConversation()) { + return; + } + synchronized (notifications) { + String conversationUuid = message.getConversationUuid(); + if (notifications.containsKey(conversationUuid)) { + notifications.get(conversationUuid).add(message); + } else { + ArrayList<Message> mList = new ArrayList<Message>(); + mList.add(message); + notifications.put(conversationUuid, mList); + } + Account account = message.getConversation().getAccount(); + updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) + && !account.inGracePeriod() + && !this.inMiniGracePeriod(account)); + } + + } + + public void clear() { + synchronized (notifications) { + notifications.clear(); + updateNotification(false); + } + } + + public void clear(Conversation conversation) { + synchronized (notifications) { + notifications.remove(conversation.getUuid()); + updateNotification(false); + } + } + + private void updateNotification(boolean notify) { + NotificationManager notificationManager = (NotificationManager) mXmppConnectionService + .getSystemService(Context.NOTIFICATION_SERVICE); + SharedPreferences preferences = mXmppConnectionService.getPreferences(); + + String ringtone = preferences.getString("notification_ringtone", null); + boolean vibrate = preferences.getBoolean("vibrate_on_notification", + true); + + if (notifications.size() == 0) { + notificationManager.cancel(NOTIFICATION_ID); + } else { + if (notify) { + this.markLastNotification(); + } + Builder mBuilder; + if (notifications.size() == 1) { + mBuilder = buildSingleConversations(notify); + } else { + mBuilder = buildMultipleConversation(); + } + if (notify) { + if (vibrate) { + int dat = 70; + long[] pattern = { 0, 3 * dat, dat, dat }; + mBuilder.setVibrate(pattern); + } + if (ringtone != null) { + mBuilder.setSound(Uri.parse(ringtone)); + } + } + mBuilder.setSmallIcon(R.drawable.ic_notification); + mBuilder.setDeleteIntent(createDeleteIntent()); + mBuilder.setLights(0xffffffff, 2000, 4000); + Notification notification = mBuilder.build(); + notificationManager.notify(NOTIFICATION_ID, notification); + } + } + + private Builder buildMultipleConversation() { + Builder mBuilder = new NotificationCompat.Builder( + mXmppConnectionService); + NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); + style.setBigContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + StringBuilder names = new StringBuilder(); + Conversation conversation = null; + for (ArrayList<Message> messages : notifications.values()) { + if (messages.size() > 0) { + conversation = messages.get(0).getConversation(); + String name = conversation.getName(); + style.addLine(Html.fromHtml("<b>" + name + "</b> " + + getReadableBody(messages.get(0)))); + names.append(name); + names.append(", "); + } + } + if (names.length() >= 2) { + names.delete(names.length() - 2, names.length()); + } + mBuilder.setContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + mBuilder.setContentText(names.toString()); + mBuilder.setStyle(style); + if (conversation != null) { + mBuilder.setContentIntent(createContentIntent(conversation + .getUuid())); + } + return mBuilder; + } + + private Builder buildSingleConversations(boolean notify) { + Builder mBuilder = new NotificationCompat.Builder( + mXmppConnectionService); + ArrayList<Message> messages = notifications.values().iterator().next(); + if (messages.size() >= 1) { + Conversation conversation = messages.get(0).getConversation(); + mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService() + .get(conversation, getPixel(64))); + mBuilder.setContentTitle(conversation.getName()); + Message message; + if ((message = getImage(messages)) != null) { + modifyForImage(mBuilder, message, messages, notify); + } else { + modifyForTextOnly(mBuilder, messages, notify); + } + mBuilder.setContentIntent(createContentIntent(conversation + .getUuid())); + } + return mBuilder; + + } + + private void modifyForImage(Builder builder, Message message, + ArrayList<Message> messages, boolean notify) { + try { + Bitmap bitmap = mXmppConnectionService.getFileBackend() + .getThumbnail(message, getPixel(288), false); + ArrayList<Message> tmp = new ArrayList<Message>(); + for (Message msg : messages) { + if (msg.getType() == Message.TYPE_TEXT + && msg.getDownloadable() == null) { + tmp.add(msg); + } + } + BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); + bigPictureStyle.bigPicture(bitmap); + if (tmp.size() > 0) { + bigPictureStyle.setSummaryText(getMergedBodies(tmp)); + builder.setContentText(getReadableBody(tmp.get(0))); + } else { + builder.setContentText(mXmppConnectionService.getString(R.string.image_file)); + } + builder.setStyle(bigPictureStyle); + } catch (FileNotFoundException e) { + modifyForTextOnly(builder, messages, notify); + } + } + + private void modifyForTextOnly(Builder builder, + ArrayList<Message> messages, boolean notify) { + builder.setStyle(new NotificationCompat.BigTextStyle() + .bigText(getMergedBodies(messages))); + builder.setContentText(getReadableBody(messages.get(0))); + if (notify) { + builder.setTicker(getReadableBody(messages.get(messages.size() - 1))); + } + } + + private Message getImage(ArrayList<Message> messages) { + for (Message message : messages) { + if (message.getType() == Message.TYPE_IMAGE + && message.getDownloadable() == null + && message.getEncryption() != Message.ENCRYPTION_PGP) { + return message; + } + } + return null; + } + + private String getMergedBodies(ArrayList<Message> messages) { + StringBuilder text = new StringBuilder(); + for (int i = 0; i < messages.size(); ++i) { + text.append(getReadableBody(messages.get(i))); + if (i != messages.size() - 1) { + text.append("\n"); + } + } + return text.toString(); + } + + private String getReadableBody(Message message) { + if (message.getDownloadable() != null + && (message.getDownloadable().getStatus() == Downloadable.STATUS_OFFER || message + .getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) { + return mXmppConnectionService.getText( + R.string.image_offered_for_download).toString(); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + return mXmppConnectionService.getText( + R.string.encrypted_message_received).toString(); + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + return mXmppConnectionService.getText(R.string.decryption_failed) + .toString(); + } else if (message.getType() == Message.TYPE_IMAGE) { + return mXmppConnectionService.getText(R.string.image_file) + .toString(); + } else { + return message.getBody().trim(); + } + } + + private PendingIntent createContentIntent(String conversationUuid) { + TaskStackBuilder stackBuilder = TaskStackBuilder + .create(mXmppConnectionService); + stackBuilder.addParentStack(ConversationActivity.class); + + Intent viewConversationIntent = new Intent(mXmppConnectionService, + ConversationActivity.class); + viewConversationIntent.setAction(Intent.ACTION_VIEW); + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversationUuid); + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + + stackBuilder.addNextIntent(viewConversationIntent); + + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT); + return resultPendingIntent; + } + + private PendingIntent createDeleteIntent() { + Intent intent = new Intent(mXmppConnectionService, + XmppConnectionService.class); + intent.setAction("clear_notification"); + return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); + } + + public static boolean wasHighlightedOrPrivate(Message message) { + String nick = message.getConversation().getMucOptions().getActualNick(); + Pattern highlight = generateNickHighlightPattern(nick); + if (message.getBody() == null || nick == null) { + return false; + } + Matcher m = highlight.matcher(message.getBody()); + return (m.find() || message.getType() == Message.TYPE_PRIVATE); + } + + private static Pattern generateNickHighlightPattern(String nick) { + // We expect a word boundary, i.e. space or start of string, followed by + // the + // nick (matched in case-insensitive manner), followed by optional + // punctuation (for example "bob: i disagree" or "how are you alice?"), + // followed by another word boundary. + return Pattern.compile("\\b" + nick + "\\p{Punct}?\\b", + Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + } + + public void setOpenConversation(Conversation conversation) { + this.mOpenConversation = conversation; + } + + public void setIsInForeground(boolean foreground) { + this.mIsInForeground = foreground; + } + + private int getPixel(int dp) { + DisplayMetrics metrics = mXmppConnectionService.getResources() + .getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } + + private void markLastNotification() { + this.mLastNotification = SystemClock.elapsedRealtime(); + } + + private boolean inMiniGracePeriod(Account account) { + int miniGrace = account.getStatus() == Account.STATUS_ONLINE ? Config.MINI_GRACE_PERIOD + : Config.MINI_GRACE_PERIOD * 2; + return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); + } +} diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java new file mode 100644 index 00000000..be73e07f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -0,0 +1,1949 @@ +package eu.siacs.conversations.services; + +import java.security.SecureRandom; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; + +import de.duenndns.ssl.MemorizingTrustManager; + +import net.java.otr4j.OtrException; +import net.java.otr4j.session.Session; +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.generator.IqGenerator; +import eu.siacs.conversations.generator.MessageGenerator; +import eu.siacs.conversations.generator.PresenceGenerator; +import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.parser.IqParser; +import eu.siacs.conversations.parser.MessageParser; +import eu.siacs.conversations.parser.PresenceParser; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.ui.UiCallback; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; +import eu.siacs.conversations.utils.PRNGFixes; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnBindListener; +import eu.siacs.conversations.xmpp.OnContactStatusChanged; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.OnMessageAcknowledged; +import eu.siacs.conversations.xmpp.OnStatusChanged; +import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; +import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import android.annotation.SuppressLint; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.FileObserver; +import android.os.IBinder; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.provider.ContactsContract; +import android.util.Log; +import android.util.LruCache; + +public class XmppConnectionService extends Service { + + public DatabaseBackend databaseBackend; + private FileBackend fileBackend = new FileBackend(this); + + public long startDate; + + private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static String ACTION_CLEAR_NOTIFICATION = "clear_notification"; + + private MemorizingTrustManager mMemorizingTrustManager; + + private NotificationService mNotificationService = new NotificationService( + this); + + private MessageParser mMessageParser = new MessageParser(this); + private PresenceParser mPresenceParser = new PresenceParser(this); + private IqParser mIqParser = new IqParser(this); + private MessageGenerator mMessageGenerator = new MessageGenerator(this); + private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); + + private List<Account> accounts; + private CopyOnWriteArrayList<Conversation> conversations = null; + private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( + this); + private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( + this); + private AvatarService mAvatarService = new AvatarService(this); + + private OnConversationUpdate mOnConversationUpdate = null; + private Integer convChangedListenerCount = 0; + private OnAccountUpdate mOnAccountUpdate = null; + private Integer accountChangedListenerCount = 0; + private OnRosterUpdate mOnRosterUpdate = null; + private Integer rosterChangedListenerCount = 0; + public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { + + @Override + public void onContactStatusChanged(Contact contact, boolean online) { + Conversation conversation = find(getConversations(), contact); + if (conversation != null) { + if (online && contact.getPresences().size() > 1) { + conversation.endOtrIfNeeded(); + } else { + conversation.resetOtrSession(); + } + if (online && (contact.getPresences().size() == 1)) { + sendUnsendMessages(conversation); + } + } + } + }; + + private SecureRandom mRandom; + + private ContentObserver contactObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + Intent intent = new Intent(getApplicationContext(), + XmppConnectionService.class); + intent.setAction(ACTION_MERGE_PHONE_CONTACTS); + startService(intent); + } + }; + + private FileObserver fileObserver = new FileObserver( + FileBackend.getConversationsDirectory()) { + + @Override + public void onEvent(int event, String path) { + if (event == FileObserver.DELETE) { + markFileDeleted(path.split("\\.")[0]); + } + } + }; + + private final IBinder mBinder = new XmppConnectionBinder(); + private OnStatusChanged statusListener = new OnStatusChanged() { + + @Override + public void onStatusChanged(Account account) { + XmppConnection connection = account.getXmppConnection(); + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + ; + } + if (account.getStatus() == Account.STATUS_ONLINE) { + for (Conversation conversation : account.pendingConferenceLeaves) { + leaveMuc(conversation); + } + for (Conversation conversation : account.pendingConferenceJoins) { + joinMuc(conversation); + } + mJingleConnectionManager.cancelInTransmission(); + List<Conversation> conversations = getConversations(); + for (int i = 0; i < conversations.size(); ++i) { + if (conversations.get(i).getAccount() == account) { + conversations.get(i).startOtrIfNeeded(); + sendUnsendMessages(conversations.get(i)); + } + } + if (connection != null && connection.getFeatures().csi()) { + if (checkListeners()) { + Log.d(Config.LOGTAG, account.getJid() + + " sending csi//inactive"); + connection.sendInactive(); + } else { + Log.d(Config.LOGTAG, account.getJid() + + " sending csi//active"); + connection.sendActive(); + } + } + syncDirtyContacts(account); + scheduleWakeupCall(Config.PING_MAX_INTERVAL, true); + } else if (account.getStatus() == Account.STATUS_OFFLINE) { + resetSendingToWaiting(account); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + int timeToReconnect = mRandom.nextInt(50) + 10; + scheduleWakeupCall(timeToReconnect, false); + } + } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) { + databaseBackend.updateAccount(account); + reconnectAccount(account, true); + } else if ((account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_NO_INTERNET)) { + if (connection != null) { + int next = connection.getTimeToNextAttempt(); + Log.d(Config.LOGTAG, account.getJid() + + ": error connecting account. try again in " + + next + "s for the " + + (connection.getAttempt() + 1) + " time"); + scheduleWakeupCall((int) (next * 1.2), false); + } + } + UIHelper.showErrorNotification(getApplicationContext(), + getAccounts()); + } + }; + + private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { + + @Override + public void onJinglePacketReceived(Account account, JinglePacket packet) { + mJingleConnectionManager.deliverPacket(account, packet); + } + }; + + private OpenPgpServiceConnection pgpServiceConnection; + private PgpEngine mPgpEngine = null; + private Intent pingIntent; + private PendingIntent pendingPingIntent = null; + private WakeLock wakeLock; + private PowerManager pm; + private OnBindListener mOnBindListener = new OnBindListener() { + + @Override + public void onBind(final Account account) { + account.getRoster().clearPresences(); + account.clearPresences(); // self presences + account.pendingConferenceJoins.clear(); + account.pendingConferenceLeaves.clear(); + fetchRosterFromServer(account); + fetchBookmarks(account); + sendPresencePacket(account, + mPresenceGenerator.sendPresence(account)); + connectMultiModeConversations(account); + updateConversationUi(); + } + }; + + private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { + + @Override + public void onMessageAcknowledged(Account account, String uuid) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + for (Message message : conversation.getMessages()) { + if ((message.getStatus() == Message.STATUS_UNSEND || message + .getStatus() == Message.STATUS_WAITING) + && message.getUuid().equals(uuid)) { + markMessage(message, Message.STATUS_SEND); + return; + } + } + } + } + } + }; + private LruCache<String, Bitmap> mBitmapCache; + + public PgpEngine getPgpEngine() { + if (pgpServiceConnection.isBound()) { + if (this.mPgpEngine == null) { + this.mPgpEngine = new PgpEngine(new OpenPgpApi( + getApplicationContext(), + pgpServiceConnection.getService()), this); + } + return mPgpEngine; + } else { + return null; + } + + } + + public FileBackend getFileBackend() { + return this.fileBackend; + } + + public AvatarService getAvatarService() { + return this.mAvatarService; + } + + public Message attachImageToConversation(final Conversation conversation, + final Uri uri, final UiCallback<Message> callback) { + final Message message; + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + message = new Message(conversation, "", + Message.ENCRYPTION_DECRYPTED); + } else { + message = new Message(conversation, "", + conversation.getNextEncryption(forceEncryption())); + } + message.setPresence(conversation.getNextPresence()); + message.setType(Message.TYPE_IMAGE); + message.setStatus(Message.STATUS_OFFERED); + new Thread(new Runnable() { + + @Override + public void run() { + try { + getFileBackend().copyImageToPrivateStorage(message, uri); + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + getPgpEngine().encrypt(message, callback); + } else { + callback.success(message); + } + } catch (FileBackend.ImageCopyException e) { + callback.error(e.getResId(), message); + } + } + }).start(); + return message; + } + + public Conversation find(Bookmark bookmark) { + return find(bookmark.getAccount(), bookmark.getJid()); + } + + public Conversation find(Account account, String jid) { + return find(getConversations(), account, jid); + } + + public class XmppConnectionBinder extends Binder { + public XmppConnectionService getService() { + return XmppConnectionService.this; + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && intent.getAction() != null) { + if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) { + mergePhoneContactsWithRoster(); + return START_STICKY; + } else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) { + logoutAndSave(); + return START_NOT_STICKY; + } else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) { + mNotificationService.clear(); + } + } + this.wakeLock.acquire(); + + for (Account account : accounts) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (!hasInternetConnection()) { + account.setStatus(Account.STATUS_NO_INTERNET); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } else { + if (account.getStatus() == Account.STATUS_NO_INTERNET) { + account.setStatus(Account.STATUS_OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + if (account.getStatus() == Account.STATUS_ONLINE) { + long lastReceived = account.getXmppConnection() + .getLastPacketReceived(); + long lastSent = account.getXmppConnection() + .getLastPingSent(); + if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) { + Log.d(Config.LOGTAG, account.getJid() + + ": ping timeout"); + this.reconnectAccount(account, true); + } else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) { + account.getXmppConnection().sendPing(); + this.scheduleWakeupCall(2, false); + } + } else if (account.getStatus() == Account.STATUS_OFFLINE) { + if (account.getXmppConnection() == null) { + account.setXmppConnection(this + .createConnection(account)); + } + new Thread(account.getXmppConnection()).start(); + } else if ((account.getStatus() == Account.STATUS_CONNECTING) + && ((SystemClock.elapsedRealtime() - account + .getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) { + Log.d(Config.LOGTAG, account.getJid() + + ": time out during connect reconnecting"); + reconnectAccount(account, true); + } else { + if (account.getXmppConnection().getTimeToNextAttempt() <= 0) { + reconnectAccount(account, true); + } + } + // in any case. reschedule wakup call + this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true); + } + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + } + } + } + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return START_STICKY; + } + + public boolean hasInternetConnection() { + ConnectivityManager cm = (ConnectivityManager) getApplicationContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnected(); + } + + @SuppressLint("TrulyRandom") + @Override + public void onCreate() { + ExceptionHelper.init(getApplicationContext()); + PRNGFixes.apply(); + this.mRandom = new SecureRandom(); + this.mMemorizingTrustManager = new MemorizingTrustManager( + getApplicationContext()); + + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int cacheSize = maxMemory / 8; + this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + + this.databaseBackend = DatabaseBackend + .getInstance(getApplicationContext()); + this.accounts = databaseBackend.getAccounts(); + + for (Account account : this.accounts) { + this.databaseBackend.readRoster(account.getRoster()); + } + this.mergePhoneContactsWithRoster(); + this.getConversations(); + + getContentResolver().registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, true, contactObserver); + this.fileObserver.startWatching(); + this.pgpServiceConnection = new OpenPgpServiceConnection( + getApplicationContext(), "org.sufficientlysecure.keychain"); + this.pgpServiceConnection.bindToService(); + + this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "XmppConnectionService"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + this.logoutAndSave(); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + this.logoutAndSave(); + } + + private void logoutAndSave() { + for (Account account : accounts) { + databaseBackend.writeRoster(account.getRoster()); + if (account.getXmppConnection() != null) { + disconnect(account, false); + } + } + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(context, EventReceiver.class); + alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0)); + Log.d(Config.LOGTAG, "good bye"); + stopSelf(); + } + + protected void scheduleWakeupCall(int seconds, boolean ping) { + long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000; + Context context = getApplicationContext(); + AlarmManager alarmManager = (AlarmManager) context + .getSystemService(Context.ALARM_SERVICE); + + if (ping) { + if (this.pingIntent == null) { + this.pingIntent = new Intent(context, EventReceiver.class); + this.pingIntent.setAction("ping"); + this.pingIntent.putExtra("time", timeToWake); + this.pendingPingIntent = PendingIntent.getBroadcast(context, 0, + this.pingIntent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + timeToWake, pendingPingIntent); + } else { + long scheduledTime = this.pingIntent.getLongExtra("time", 0); + if (scheduledTime < SystemClock.elapsedRealtime() + || (scheduledTime > timeToWake)) { + this.pingIntent.putExtra("time", timeToWake); + alarmManager.cancel(this.pendingPingIntent); + this.pendingPingIntent = PendingIntent.getBroadcast( + context, 0, this.pingIntent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + timeToWake, pendingPingIntent); + } + } + } else { + Intent intent = new Intent(context, EventReceiver.class); + intent.setAction("ping_check"); + PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, + intent, 0); + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, + alarmIntent); + } + + } + + public XmppConnection createConnection(Account account) { + SharedPreferences sharedPref = getPreferences(); + account.setResource(sharedPref.getString("resource", "mobile") + .toLowerCase(Locale.getDefault())); + XmppConnection connection = new XmppConnection(account, this); + connection.setOnMessagePacketReceivedListener(this.mMessageParser); + connection.setOnStatusChangedListener(this.statusListener); + connection.setOnPresencePacketReceivedListener(this.mPresenceParser); + connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); + connection.setOnJinglePacketReceivedListener(this.jingleListener); + connection.setOnBindListener(this.mOnBindListener); + connection + .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); + return connection; + } + + public void sendMessage(Message message) { + Account account = message.getConversation().getAccount(); + account.deactivateGracePeriod(); + Conversation conv = message.getConversation(); + MessagePacket packet = null; + boolean saveInDb = true; + boolean send = false; + if (account.getStatus() == Account.STATUS_ONLINE + && account.getXmppConnection() != null) { + if (message.getType() == Message.TYPE_IMAGE) { + if (message.getPresence() != null) { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession() + && (message.getPresence() != null)) { + conv.startOtrSession(this, message.getPresence(), + true); + message.setStatus(Message.STATUS_WAITING); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + mJingleConnectionManager + .createNewConnection(message); + } + } else { + mJingleConnectionManager.createNewConnection(message); + } + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + conv.startOtrIfNeeded(); + } + message.setStatus(Message.STATUS_WAITING); + } + } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (!conv.hasValidOtrSession() + && (message.getPresence() != null)) { + conv.startOtrSession(this, message.getPresence(), true); + message.setStatus(Message.STATUS_WAITING); + } else if (conv.hasValidOtrSession() + && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { + message.setPresence(conv.getOtrSession().getSessionID() + .getUserID()); + packet = mMessageGenerator.generateOtrChat(message); + send = true; + + } else if (message.getPresence() == null) { + conv.startOtrIfNeeded(); + message.setStatus(Message.STATUS_WAITING); + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + message.getConversation().endOtrIfNeeded(); + failWaitingOtrMessages(message.getConversation()); + packet = mMessageGenerator.generatePgpChat(message); + send = true; + } else { + message.getConversation().endOtrIfNeeded(); + failWaitingOtrMessages(message.getConversation()); + packet = mMessageGenerator.generateChat(message); + send = true; + } + } + if (!account.getXmppConnection().getFeatures().sm() + && conv.getMode() != Conversation.MODE_MULTI) { + message.setStatus(Message.STATUS_SEND); + } + } else { + message.setStatus(Message.STATUS_WAITING); + if (message.getType() == Message.TYPE_TEXT) { + if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { + String pgpBody = message.getEncryptedBody(); + String decryptedBody = message.getBody(); + message.setBody(pgpBody); + message.setEncryption(Message.ENCRYPTION_PGP); + databaseBackend.createMessage(message); + saveInDb = false; + message.setBody(decryptedBody); + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + } else if (message.getEncryption() == Message.ENCRYPTION_OTR) { + if (conv.hasValidOtrSession()) { + message.setPresence(conv.getOtrSession().getSessionID() + .getUserID()); + } else if (!conv.hasValidOtrSession() + && message.getPresence() != null) { + conv.startOtrSession(this, message.getPresence(), false); + } + } + } + + } + conv.add(message); + if (saveInDb) { + if (message.getEncryption() == Message.ENCRYPTION_NONE + || saveEncryptedMessages()) { + databaseBackend.createMessage(message); + } + } + if ((send) && (packet != null)) { + sendMessagePacket(account, packet); + } + updateConversationUi(); + } + + private void sendUnsendMessages(Conversation conversation) { + for (int i = 0; i < conversation.getMessages().size(); ++i) { + int status = conversation.getMessages().get(i).getStatus(); + if (status == Message.STATUS_WAITING) { + resendMessage(conversation.getMessages().get(i)); + } + } + } + + private void resendMessage(Message message) { + Account account = message.getConversation().getAccount(); + MessagePacket packet = null; + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if (!message.getConversation().hasValidOtrSession()) { + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + message.getConversation().startOtrSession(this, + message.getPresence(), true); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.getConversation().startOtrSession(this, + presence, true); + } + } + } else { + if (message.getConversation().getOtrSession() + .getSessionStatus() == SessionStatus.ENCRYPTED) { + if (message.getType() == Message.TYPE_TEXT) { + packet = mMessageGenerator.generateOtrChat(message, + true); + } else if (message.getType() == Message.TYPE_IMAGE) { + mJingleConnectionManager.createNewConnection(message); + } + } + } + } else if (message.getType() == Message.TYPE_TEXT) { + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + packet = mMessageGenerator.generateChat(message, true); + } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED) + || (message.getEncryption() == Message.ENCRYPTION_PGP)) { + packet = mMessageGenerator.generatePgpChat(message, true); + } + } else if (message.getType() == Message.TYPE_IMAGE) { + Presences presences = message.getConversation().getContact() + .getPresences(); + if ((message.getPresence() != null) + && (presences.has(message.getPresence()))) { + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } else { + if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + message.setPresence(presence); + markMessage(message, Message.STATUS_OFFERED); + mJingleConnectionManager.createNewConnection(message); + } + } + } + if (packet != null) { + if (!account.getXmppConnection().getFeatures().sm() + && message.getConversation().getMode() != Conversation.MODE_MULTI) { + markMessage(message, Message.STATUS_SEND); + } else { + markMessage(message, Message.STATUS_UNSEND); + } + sendMessagePacket(account, packet); + } + } + + public void fetchRosterFromServer(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + if (!"".equals(account.getRosterVersion())) { + Log.d(Config.LOGTAG, account.getJid() + + ": fetching roster version " + account.getRosterVersion()); + } else { + Log.d(Config.LOGTAG, account.getJid() + ": fetching roster"); + } + iqPacket.query("jabber:iq:roster").setAttribute("ver", + account.getRosterVersion()); + account.getXmppConnection().sendIqPacket(iqPacket, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, + IqPacket packet) { + Element query = packet.findChild("query"); + if (query != null) { + account.getRoster().markAllAsNotInRoster(); + mIqParser.rosterItems(account, query); + } + } + }); + } + + public void fetchBookmarks(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + Element query = iqPacket.query("jabber:iq:private"); + query.addChild("storage", "storage:bookmarks"); + OnIqPacketReceived callback = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element query = packet.query(); + List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>(); + Element storage = query.findChild("storage", + "storage:bookmarks"); + if (storage != null) { + for (Element item : storage.getChildren()) { + if (item.getName().equals("conference")) { + Bookmark bookmark = Bookmark.parse(item, account); + bookmarks.add(bookmark); + Conversation conversation = find(bookmark); + if (conversation != null) { + conversation.setBookmark(bookmark); + } else { + if (bookmark.autojoin()) { + conversation = findOrCreateConversation( + account, bookmark.getJid(), true); + conversation.setBookmark(bookmark); + joinMuc(conversation); + } + } + } + } + } + account.setBookmarks(bookmarks); + } + }; + sendIqPacket(account, iqPacket, callback); + + } + + public void pushBookmarks(Account account) { + IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET); + Element query = iqPacket.query("jabber:iq:private"); + Element storage = query.addChild("storage", "storage:bookmarks"); + for (Bookmark bookmark : account.getBookmarks()) { + storage.addChild(bookmark); + } + sendIqPacket(account, iqPacket, null); + } + + private void mergePhoneContactsWithRoster() { + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new OnPhoneContactsLoadedListener() { + @Override + public void onPhoneContactsLoaded(List<Bundle> phoneContacts) { + for (Account account : accounts) { + account.getRoster().clearSystemAccounts(); + } + for (Bundle phoneContact : phoneContacts) { + for (Account account : accounts) { + String jid = phoneContact.getString("jid"); + Contact contact = account.getRoster() + .getContact(jid); + String systemAccount = phoneContact + .getInt("phoneid") + + "#" + + phoneContact.getString("lookup"); + contact.setSystemAccount(systemAccount); + contact.setPhotoUri(phoneContact + .getString("photouri")); + contact.setSystemName(phoneContact + .getString("displayname")); + getAvatarService().clear(contact); + } + } + } + }); + } + + public List<Conversation> getConversations() { + if (this.conversations == null) { + Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>(); + for (Account account : this.accounts) { + accountLookupTable.put(account.getUuid(), account); + } + this.conversations = databaseBackend + .getConversations(Conversation.STATUS_AVAILABLE); + for (Conversation conv : this.conversations) { + Account account = accountLookupTable.get(conv.getAccountUuid()); + conv.setAccount(account); + conv.setMessages(databaseBackend.getMessages(conv, 50)); + checkDeletedFiles(conv); + } + } + return this.conversations; + } + + private void checkDeletedFiles(Conversation conversation) { + for (Message message : conversation.getMessages()) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DeletedDownloadable()); + } + } + } + } + + private void markFileDeleted(String uuid) { + for (Conversation conversation : getConversations()) { + for (Message message : conversation.getMessages()) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DeletedDownloadable()); + updateConversationUi(); + } + return; + } + } + } + } + + public void populateWithOrderedConversations(List<Conversation> list) { + populateWithOrderedConversations(list, true); + } + + public void populateWithOrderedConversations(List<Conversation> list, + boolean includeConferences) { + list.clear(); + if (includeConferences) { + list.addAll(getConversations()); + } else { + for (Conversation conversation : getConversations()) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + list.add(conversation); + } + } + } + Collections.sort(list, new Comparator<Conversation>() { + @Override + public int compare(Conversation lhs, Conversation rhs) { + Message left = lhs.getLatestMessage(); + Message right = rhs.getLatestMessage(); + if (left.getTimeSent() > right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() < right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); + } + + public int loadMoreMessages(Conversation conversation, long timestamp) { + List<Message> messages = databaseBackend.getMessages(conversation, 50, + timestamp); + for (Message message : messages) { + message.setConversation(conversation); + } + conversation.addAll(0, messages); + return messages.size(); + } + + public List<Account> getAccounts() { + return this.accounts; + } + + public Conversation find(List<Conversation> haystack, Contact contact) { + for (Conversation conversation : haystack) { + if (conversation.getContact() == contact) { + return conversation; + } + } + return null; + } + + public Conversation find(List<Conversation> haystack, Account account, + String jid) { + for (Conversation conversation : haystack) { + if ((account == null || conversation.getAccount() == account) + && (conversation.getContactJid().split("/", 2)[0] + .equalsIgnoreCase(jid))) { + return conversation; + } + } + return null; + } + + public Conversation findOrCreateConversation(Account account, String jid, + boolean muc) { + Conversation conversation = find(account, jid); + if (conversation != null) { + return conversation; + } + conversation = databaseBackend.findConversation(account, jid); + if (conversation != null) { + conversation.setStatus(Conversation.STATUS_AVAILABLE); + conversation.setAccount(account); + if (muc) { + conversation.setMode(Conversation.MODE_MULTI); + } else { + conversation.setMode(Conversation.MODE_SINGLE); + } + conversation.setMessages(databaseBackend.getMessages(conversation, + 50)); + this.databaseBackend.updateConversation(conversation); + } else { + String conversationName; + Contact contact = account.getRoster().getContact(jid); + if (contact != null) { + conversationName = contact.getDisplayName(); + } else { + conversationName = jid.split("@")[0]; + } + if (muc) { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_MULTI); + } else { + conversation = new Conversation(conversationName, account, jid, + Conversation.MODE_SINGLE); + } + this.databaseBackend.createConversation(conversation); + } + this.conversations.add(conversation); + updateConversationUi(); + return conversation; + } + + public void archiveConversation(Conversation conversation) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null && bookmark.autojoin()) { + bookmark.setAutojoin(false); + pushBookmarks(bookmark.getAccount()); + } + } + leaveMuc(conversation); + } else { + conversation.endOtrIfNeeded(); + } + this.databaseBackend.updateConversation(conversation); + this.conversations.remove(conversation); + updateConversationUi(); + } + + public void clearConversationHistory(Conversation conversation) { + this.databaseBackend.deleteMessagesInConversation(conversation); + conversation.getMessages().clear(); + updateConversationUi(); + } + + public int getConversationCount() { + return this.databaseBackend.getConversationCount(); + } + + public void createAccount(Account account) { + databaseBackend.createAccount(account); + this.accounts.add(account); + this.reconnectAccount(account, false); + updateAccountUi(); + } + + public void updateAccount(Account account) { + this.statusListener.onStatusChanged(account); + databaseBackend.updateAccount(account); + reconnectAccount(account, false); + updateAccountUi(); + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); + } + + public void deleteAccount(Account account) { + for (Conversation conversation : conversations) { + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else if (conversation.getMode() == Conversation.MODE_SINGLE) { + conversation.endOtrIfNeeded(); + } + conversations.remove(conversation); + } + } + if (account.getXmppConnection() != null) { + this.disconnect(account, true); + } + databaseBackend.deleteAccount(account); + this.accounts.remove(account); + updateAccountUi(); + UIHelper.showErrorNotification(getApplicationContext(), getAccounts()); + } + + public void setOnConversationListChangedListener( + OnConversationUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, + "ignoring setOnConversationListChangedListener"); + return; + } + synchronized (this.convChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnConversationUpdate = listener; + this.mNotificationService.setIsInForeground(true); + this.convChangedListenerCount++; + } + } + + public void removeOnConversationListChangedListener() { + synchronized (this.convChangedListenerCount) { + this.convChangedListenerCount--; + if (this.convChangedListenerCount <= 0) { + this.convChangedListenerCount = 0; + this.mOnConversationUpdate = null; + this.mNotificationService.setIsInForeground(false); + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnAccountListChangedListener(OnAccountUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener"); + return; + } + synchronized (this.accountChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnAccountUpdate = listener; + this.accountChangedListenerCount++; + } + } + + public void removeOnAccountListChangedListener() { + synchronized (this.accountChangedListenerCount) { + this.accountChangedListenerCount--; + if (this.accountChangedListenerCount <= 0) { + this.mOnAccountUpdate = null; + this.accountChangedListenerCount = 0; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + public void setOnRosterUpdateListener(OnRosterUpdate listener) { + if (!isScreenOn()) { + Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener"); + return; + } + synchronized (this.rosterChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnRosterUpdate = listener; + this.rosterChangedListenerCount++; + } + } + + public void removeOnRosterUpdateListener() { + synchronized (this.rosterChangedListenerCount) { + this.rosterChangedListenerCount--; + if (this.rosterChangedListenerCount <= 0) { + this.rosterChangedListenerCount = 0; + this.mOnRosterUpdate = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + + private boolean checkListeners() { + return (this.mOnAccountUpdate == null + && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null); + } + + private void switchToForeground() { + for (Account account : getAccounts()) { + if (account.getStatus() == Account.STATUS_ONLINE) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null && connection.getFeatures().csi()) { + connection.sendActive(); + } + } + } + Log.d(Config.LOGTAG, "app switched into foreground"); + } + + private void switchToBackground() { + for (Account account : getAccounts()) { + if (account.getStatus() == Account.STATUS_ONLINE) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null && connection.getFeatures().csi()) { + connection.sendInactive(); + } + } + } + this.mNotificationService.setIsInForeground(false); + Log.d(Config.LOGTAG, "app switched into background"); + } + + private boolean isScreenOn() { + PowerManager pm = (PowerManager) this + .getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); + } + + public void connectMultiModeConversations(Account account) { + List<Conversation> conversations = getConversations(); + for (int i = 0; i < conversations.size(); i++) { + Conversation conversation = conversations.get(i); + if ((conversation.getMode() == Conversation.MODE_MULTI) + && (conversation.getAccount() == account)) { + joinMuc(conversation); + } + } + } + + public void joinMuc(Conversation conversation) { + Account account = conversation.getAccount(); + account.pendingConferenceJoins.remove(conversation); + account.pendingConferenceLeaves.remove(conversation); + if (account.getStatus() == Account.STATUS_ONLINE) { + Log.d(Config.LOGTAG, + "joining conversation " + conversation.getContactJid()); + String nick = conversation.getMucOptions().getProposedNick(); + conversation.getMucOptions().setJoinNick(nick); + PresencePacket packet = new PresencePacket(); + String joinJid = conversation.getMucOptions().getJoinJid(); + packet.setAttribute("to", conversation.getMucOptions().getJoinJid()); + Element x = new Element("x"); + x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); + if (conversation.getMucOptions().getPassword() != null) { + Element password = x.addChild("password"); + password.setContent(conversation.getMucOptions().getPassword()); + } + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + if (conversation.getMessages().size() != 0) { + final SimpleDateFormat mDateFormat = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + Date date = new Date(conversation.getLatestMessage() + .getTimeSent() + 1000); + x.addChild("history").setAttribute("since", + mDateFormat.format(date)); + } + packet.addChild(x); + sendPresencePacket(account, packet); + if (!joinJid.equals(conversation.getContactJid())) { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + } + } else { + account.pendingConferenceJoins.add(conversation); + } + } + + private OnRenameListener renameListener = null; + private IqGenerator mIqGenerator = new IqGenerator(this); + + public void setOnRenameListener(OnRenameListener listener) { + this.renameListener = listener; + } + + public void providePasswordForMuc(Conversation conversation, String password) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + conversation.getMucOptions().setPassword(password); + if (conversation.getBookmark() != null) { + conversation.getBookmark().setAutojoin(true); + pushBookmarks(conversation.getAccount()); + } + databaseBackend.updateConversation(conversation); + joinMuc(conversation); + } + } + + public void renameInMuc(final Conversation conversation, final String nick) { + final MucOptions options = conversation.getMucOptions(); + options.setJoinNick(nick); + if (options.online()) { + Account account = conversation.getAccount(); + options.setOnRenameListener(new OnRenameListener() { + + @Override + public void onRename(boolean success) { + if (renameListener != null) { + renameListener.onRename(success); + } + if (success) { + conversation.setContactJid(conversation.getMucOptions() + .getJoinJid()); + databaseBackend.updateConversation(conversation); + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setNick(nick); + pushBookmarks(bookmark.getAccount()); + } + } + } + }); + options.flagAboutToRename(); + PresencePacket packet = new PresencePacket(); + packet.setAttribute("to", options.getJoinJid()); + packet.setAttribute("from", conversation.getAccount().getFullJid()); + + String sig = account.getPgpSignature(); + if (sig != null) { + packet.addChild("status").setContent("online"); + packet.addChild("x", "jabber:x:signed").setContent(sig); + } + sendPresencePacket(account, packet); + } else { + conversation.setContactJid(options.getJoinJid()); + databaseBackend.updateConversation(conversation); + if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + bookmark.setNick(nick); + pushBookmarks(bookmark.getAccount()); + } + joinMuc(conversation); + } + } + } + + public void leaveMuc(Conversation conversation) { + Account account = conversation.getAccount(); + account.pendingConferenceJoins.remove(conversation); + account.pendingConferenceLeaves.remove(conversation); + if (account.getStatus() == Account.STATUS_ONLINE) { + PresencePacket packet = new PresencePacket(); + packet.setAttribute("to", conversation.getMucOptions().getJoinJid()); + packet.setAttribute("from", conversation.getAccount().getFullJid()); + packet.setAttribute("type", "unavailable"); + sendPresencePacket(conversation.getAccount(), packet); + conversation.getMucOptions().setOffline(); + conversation.deregisterWithBookmark(); + Log.d(Config.LOGTAG, conversation.getAccount().getJid() + + ": leaving muc " + conversation.getContactJid()); + } else { + account.pendingConferenceLeaves.add(conversation); + } + } + + public void disconnect(Account account, boolean force) { + if ((account.getStatus() == Account.STATUS_ONLINE) + || (account.getStatus() == Account.STATUS_DISABLED)) { + if (!force) { + List<Conversation> conversations = getConversations(); + for (int i = 0; i < conversations.size(); i++) { + Conversation conversation = conversations.get(i); + if (conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI) { + leaveMuc(conversation); + } else { + if (conversation.endOtrIfNeeded()) { + Log.d(Config.LOGTAG, account.getJid() + + ": ended otr session with " + + conversation.getContactJid()); + } + } + } + } + } + account.getXmppConnection().disconnect(force); + } + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + public void updateMessage(Message message) { + databaseBackend.updateMessage(message); + updateConversationUi(); + } + + protected void syncDirtyContacts(Account account) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.getOption(Contact.Options.DIRTY_PUSH)) { + pushContactToServer(contact); + } + if (contact.getOption(Contact.Options.DIRTY_DELETE)) { + deleteContactOnServer(contact); + } + } + } + + public void createContact(Contact contact) { + SharedPreferences sharedPref = getPreferences(); + boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true); + if (autoGrant) { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + contact.setOption(Contact.Options.ASKING); + } + pushContactToServer(contact); + } + + public void onOtrSessionEstablished(Conversation conversation) { + Account account = conversation.getAccount(); + List<Message> messages = conversation.getMessages(); + Session otrSession = conversation.getOtrSession(); + Log.d(Config.LOGTAG, + account.getJid() + " otr session established with " + + conversation.getContactJid() + "/" + + otrSession.getSessionID().getUserID()); + for (int i = 0; i < messages.size(); ++i) { + Message msg = messages.get(i); + if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING) + && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { + msg.setPresence(otrSession.getSessionID().getUserID()); + if (msg.getType() == Message.TYPE_TEXT) { + MessagePacket outPacket = mMessageGenerator + .generateOtrChat(msg, true); + if (outPacket != null) { + msg.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(msg); + sendMessagePacket(account, outPacket); + } + } else if (msg.getType() == Message.TYPE_IMAGE) { + mJingleConnectionManager.createNewConnection(msg); + } + } + } + updateConversationUi(); + } + + public boolean renewSymmetricKey(Conversation conversation) { + Account account = conversation.getAccount(); + byte[] symmetricKey = new byte[32]; + this.mRandom.nextBytes(symmetricKey); + Session otrSession = conversation.getOtrSession(); + if (otrSession != null) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_CHAT); + packet.setFrom(account.getFullJid()); + packet.addChild("private", "urn:xmpp:carbons:2"); + packet.addChild("no-copy", "urn:xmpp:hints"); + packet.setTo(otrSession.getSessionID().getAccountID() + "/" + + otrSession.getSessionID().getUserID()); + try { + packet.setBody(otrSession + .transformSending(CryptoHelper.FILETRANSFER + + CryptoHelper.bytesToHex(symmetricKey))); + sendMessagePacket(account, packet); + conversation.setSymmetricKey(symmetricKey); + return true; + } catch (OtrException e) { + return false; + } + } + return false; + } + + public void pushContactToServer(Contact contact) { + contact.resetOption(Contact.Options.DIRTY_DELETE); + contact.setOption(Contact.Options.DIRTY_PUSH); + Account account = contact.getAccount(); + if (account.getStatus() == Account.STATUS_ONLINE) { + boolean ask = contact.getOption(Contact.Options.ASKING); + boolean sendUpdates = contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) + && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.query("jabber:iq:roster").addChild(contact.asElement()); + account.getXmppConnection().sendIqPacket(iq, null); + if (sendUpdates) { + sendPresencePacket(account, + mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } + if (ask) { + sendPresencePacket(account, + mPresenceGenerator.requestPresenceUpdatesFrom(contact)); + } + } + } + + public void publishAvatar(Account account, Uri image, + final UiCallback<Avatar> callback) { + final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; + final int size = Config.AVATAR_SIZE; + final Avatar avatar = getFileBackend() + .getPepAvatar(image, size, format); + if (avatar != null) { + avatar.height = size; + avatar.width = size; + if (format.equals(Bitmap.CompressFormat.WEBP)) { + avatar.type = "image/webp"; + } else if (format.equals(Bitmap.CompressFormat.JPEG)) { + avatar.type = "image/jpeg"; + } else if (format.equals(Bitmap.CompressFormat.PNG)) { + avatar.type = "image/png"; + } + if (!getFileBackend().save(avatar)) { + callback.error(R.string.error_saving_avatar, avatar); + return; + } + IqPacket packet = this.mIqGenerator.publishAvatar(avatar); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + if (result.getType() == IqPacket.TYPE_RESULT) { + IqPacket packet = XmppConnectionService.this.mIqGenerator + .publishAvatarMetadata(avatar); + sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket result) { + if (result.getType() == IqPacket.TYPE_RESULT) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + callback.success(avatar); + } else { + callback.error( + R.string.error_publish_avatar_server_reject, + avatar); + } + } + }); + } else { + callback.error( + R.string.error_publish_avatar_server_reject, + avatar); + } + } + }); + } else { + callback.error(R.string.error_publish_avatar_converting, null); + } + } + + public void fetchAvatar(Account account, Avatar avatar) { + fetchAvatar(account, avatar, null); + } + + public void fetchAvatar(Account account, final Avatar avatar, + final UiCallback<Avatar> callback) { + IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar); + sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket result) { + final String ERROR = account.getJid() + + ": fetching avatar for " + avatar.owner + " failed "; + if (result.getType() == IqPacket.TYPE_RESULT) { + avatar.image = mIqParser.avatarData(result); + if (avatar.image != null) { + if (getFileBackend().save(avatar)) { + if (account.getJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + getAvatarService().clear(account); + updateConversationUi(); + updateAccountUi(); + } else { + Contact contact = account.getRoster() + .getContact(avatar.owner); + contact.setAvatar(avatar.getFilename()); + getAvatarService().clear(contact); + updateConversationUi(); + updateRosterUi(); + } + if (callback != null) { + callback.success(avatar); + } + Log.d(Config.LOGTAG, account.getJid() + + ": succesfully fetched avatar for " + + avatar.owner); + return; + } + } else { + + Log.d(Config.LOGTAG, ERROR + "(parsing error)"); + } + } else { + Element error = result.findChild("error"); + if (error == null) { + Log.d(Config.LOGTAG, ERROR + "(server error)"); + } else { + Log.d(Config.LOGTAG, ERROR + error.toString()); + } + } + if (callback != null) { + callback.error(0, null); + } + + } + }); + } + + public void checkForAvatar(Account account, + final UiCallback<Avatar> callback) { + IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(account, packet, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub != null) { + Element items = pubsub.findChild("items"); + if (items != null) { + Avatar avatar = Avatar.parseMetadata(items); + if (avatar != null) { + avatar.owner = account.getJid(); + if (fileBackend.isAvatarCached(avatar)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatar(account, avatar, callback); + } + return; + } + } + } + } + callback.error(0, null); + } + }); + } + + public void deleteContactOnServer(Contact contact) { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + contact.resetOption(Contact.Options.DIRTY_PUSH); + contact.setOption(Contact.Options.DIRTY_DELETE); + Account account = contact.getAccount(); + if (account.getStatus() == Account.STATUS_ONLINE) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + Element item = iq.query("jabber:iq:roster").addChild("item"); + item.setAttribute("jid", contact.getJid()); + item.setAttribute("subscription", "remove"); + account.getXmppConnection().sendIqPacket(iq, null); + } + } + + public void updateConversation(Conversation conversation) { + this.databaseBackend.updateConversation(conversation); + } + + public void reconnectAccount(final Account account, final boolean force) { + new Thread(new Runnable() { + + @Override + public void run() { + if (account.getXmppConnection() != null) { + disconnect(account, force); + } + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (account.getXmppConnection() == null) { + account.setXmppConnection(createConnection(account)); + } + Thread thread = new Thread(account.getXmppConnection()); + thread.start(); + scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2), + false); + } else { + account.getRoster().clearPresences(); + account.setXmppConnection(null); + } + } + }).start(); + } + + public void invite(Conversation conversation, String contact) { + MessagePacket packet = mMessageGenerator.invite(conversation, contact); + sendMessagePacket(conversation.getAccount(), packet); + } + + public void resetSendingToWaiting(Account account) { + for (Conversation conversation : getConversations()) { + if (conversation.getAccount() == account) { + for (Message message : conversation.getMessages()) { + if (message.getType() != Message.TYPE_IMAGE + && message.getStatus() == Message.STATUS_UNSEND) { + markMessage(message, Message.STATUS_WAITING); + } + } + } + } + } + + public boolean markMessage(Account account, String recipient, String uuid, + int status) { + if (uuid == null) { + return false; + } else { + for (Conversation conversation : getConversations()) { + if (conversation.getContactJid().equals(recipient) + && conversation.getAccount().equals(account)) { + return markMessage(conversation, uuid, status); + } + } + return false; + } + } + + public boolean markMessage(Conversation conversation, String uuid, + int status) { + if (uuid == null) { + return false; + } else { + for (Message message : conversation.getMessages()) { + if (uuid.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND && uuid + .equals(message.getRemoteMsgId()))) { + markMessage(message, status); + return true; + } + } + return false; + } + } + + public void markMessage(Message message, int status) { + if (status == Message.STATUS_SEND_FAILED + && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message + .getStatus() == Message.STATUS_SEND_DISPLAYED)) { + return; + } + message.setStatus(status); + databaseBackend.updateMessage(message); + updateConversationUi(); + } + + public SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + } + + public boolean forceEncryption() { + return getPreferences().getBoolean("force_encryption", false); + } + + public boolean confirmMessages() { + return getPreferences().getBoolean("confirm_messages", true); + } + + public boolean saveEncryptedMessages() { + return !getPreferences().getBoolean("dont_save_encrypted", false); + } + + public boolean indicateReceived() { + return getPreferences().getBoolean("indicate_received", false); + } + + public void updateConversationUi() { + if (mOnConversationUpdate != null) { + mOnConversationUpdate.onConversationUpdate(); + } + } + + public void updateAccountUi() { + if (mOnAccountUpdate != null) { + mOnAccountUpdate.onAccountUpdate(); + } + } + + public void updateRosterUi() { + if (mOnRosterUpdate != null) { + mOnRosterUpdate.onRosterUpdate(); + } + } + + public Account findAccountByJid(String accountJid) { + for (Account account : this.accounts) { + if (account.getJid().equals(accountJid)) { + return account; + } + } + return null; + } + + public Conversation findConversationByUuid(String uuid) { + for (Conversation conversation : getConversations()) { + if (conversation.getUuid().equals(uuid)) { + return conversation; + } + } + return null; + } + + public void markRead(Conversation conversation, boolean calledByUi) { + mNotificationService.clear(conversation); + String id = conversation.getLatestMarkableMessageId(); + conversation.markRead(); + if (confirmMessages() && id != null && calledByUi) { + Log.d(Config.LOGTAG, conversation.getAccount().getJid() + + ": sending read marker for " + conversation.getName()); + Account account = conversation.getAccount(); + String to = conversation.getContactJid(); + this.sendMessagePacket(conversation.getAccount(), + mMessageGenerator.confirm(account, to, id)); + } + if (!calledByUi) { + updateConversationUi(); + } + } + + public void failWaitingOtrMessages(Conversation conversation) { + for (Message message : conversation.getMessages()) { + if (message.getEncryption() == Message.ENCRYPTION_OTR + && message.getStatus() == Message.STATUS_WAITING) { + markMessage(message, Message.STATUS_SEND_FAILED); + } + } + } + + public SecureRandom getRNG() { + return this.mRandom; + } + + public MemorizingTrustManager getMemorizingTrustManager() { + return this.mMemorizingTrustManager; + } + + public PowerManager getPowerManager() { + return this.pm; + } + + public LruCache<String, Bitmap> getBitmapCache() { + return this.mBitmapCache; + } + + public void replyWithNotAcceptable(Account account, MessagePacket packet) { + if (account.getStatus() == Account.STATUS_ONLINE) { + MessagePacket error = this.mMessageGenerator + .generateNotAcceptable(packet); + sendMessagePacket(account, error); + } + } + + public void syncRosterToDisk(final Account account) { + new Thread(new Runnable() { + + @Override + public void run() { + databaseBackend.writeRoster(account.getRoster()); + } + }).start(); + + } + + public List<String> getKnownHosts() { + List<String> hosts = new ArrayList<String>(); + for (Account account : getAccounts()) { + if (!hosts.contains(account.getServer())) { + hosts.add(account.getServer()); + } + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster()) { + String server = contact.getServer(); + if (server != null && !hosts.contains(server)) { + hosts.add(server); + } + } + } + } + return hosts; + } + + public List<String> getKnownConferenceHosts() { + ArrayList<String> mucServers = new ArrayList<String>(); + for (Account account : accounts) { + if (account.getXmppConnection() != null) { + String server = account.getXmppConnection().getMucServer(); + if (server != null && !mucServers.contains(server)) { + mucServers.add(server); + } + } + } + return mucServers; + } + + public void sendMessagePacket(Account account, MessagePacket packet) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendMessagePacket(packet); + } + } + + public void sendPresencePacket(Account account, PresencePacket packet) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendPresencePacket(packet); + } + } + + public void sendIqPacket(Account account, IqPacket packet, + OnIqPacketReceived callback) { + XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.sendIqPacket(packet, callback); + } + } + + public MessageGenerator getMessageGenerator() { + return this.mMessageGenerator; + } + + public PresenceGenerator getPresenceGenerator() { + return this.mPresenceGenerator; + } + + public IqGenerator getIqGenerator() { + return this.mIqGenerator; + } + + public JingleConnectionManager getJingleConnectionManager() { + return this.mJingleConnectionManager; + } + + public interface OnConversationUpdate { + public void onConversationUpdate(); + } + + public interface OnAccountUpdate { + public void onAccountUpdate(); + } + + public interface OnRosterUpdate { + public void onRosterUpdate(); + } + + public List<Contact> findContacts(String jid) { + ArrayList<Contact> contacts = new ArrayList<Contact>(); + for (Account account : getAccounts()) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + Contact contact = account.getRoster().getContactFromRoster(jid); + if (contact != null) { + contacts.add(contact); + } + } + } + return contacts; + } + + public NotificationService getNotificationService() { + return this.mNotificationService; + } + + public HttpConnectionManager getHttpConnectionManager() { + return this.mHttpConnectionManager; + } + + private class DeletedDownloadable implements Downloadable { + + @Override + public boolean start() { + return false; + } + + @Override + public int getStatus() { + return Downloadable.STATUS_DELETED; + } + + @Override + public long getFileSize() { + return 0; + } + + } + + public void resendFailedMessages(Message message) { + List<Message> messages = new ArrayList<Message>(); + Message current = message; + while(current.getStatus() == Message.STATUS_SEND_FAILED) { + messages.add(current); + if (current.mergable(current.next())) { + current = current.next(); + } else { + break; + } + } + for(Message msg: messages) { + markMessage(msg, Message.STATUS_WAITING); + this.resendMessage(msg); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java new file mode 100644 index 00000000..f14da352 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -0,0 +1,145 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.Collections; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.ui.adapter.ListItemAdapter; + +public class ChooseContactActivity extends XmppActivity { + + private ListView mListView; + private ArrayList<ListItem> contacts = new ArrayList<ListItem>(); + private ArrayAdapter<ListItem> mContactsAdapter; + + private EditText mSearchEditText; + + private TextWatcher mSearchTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(Editable editable) { + filterContacts(editable.toString()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + } + }; + + private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + mSearchEditText.post(new Runnable() { + + @Override + public void run() { + mSearchEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, + InputMethodManager.SHOW_IMPLICIT); + } + }); + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + mSearchEditText.setText(""); + filterContacts(null); + return true; + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_choose_contact); + mListView = (ListView) findViewById(R.id.choose_contact_list); + mListView.setFastScrollEnabled(true); + mContactsAdapter = new ListItemAdapter(this, contacts); + mListView.setAdapter(mContactsAdapter); + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView<?> arg0, View arg1, + int position, long arg3) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + Intent request = getIntent(); + Intent data = new Intent(); + ListItem mListItem = contacts.get(position); + data.putExtra("contact", mListItem.getJid()); + String account = request.getStringExtra("account"); + if (account == null && mListItem instanceof Contact) { + account = ((Contact) mListItem).getAccount().getJid(); + } + data.putExtra("account", account); + data.putExtra("conversation", + request.getStringExtra("conversation")); + setResult(RESULT_OK, data); + finish(); + } + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.choose_contact, menu); + MenuItem menuSearchView = menu.findItem(R.id.action_search); + View mSearchView = menuSearchView.getActionView(); + mSearchEditText = (EditText) mSearchView + .findViewById(R.id.search_field); + mSearchEditText.addTextChangedListener(mSearchTextWatcher); + menuSearchView.setOnActionExpandListener(mOnActionExpandListener); + return true; + } + + @Override + void onBackendConnected() { + filterContacts(null); + } + + protected void filterContacts(String needle) { + this.contacts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && contact.match(needle)) { + this.contacts.add(contact); + } + } + } + } + Collections.sort(this.contacts); + mContactsAdapter.notifyDataSetChanged(); + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java new file mode 100644 index 00000000..52687c81 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -0,0 +1,280 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.entities.MucOptions.User; +import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import android.app.PendingIntent; +import android.content.Context; +import android.content.IntentSender.SendIntentException; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class ConferenceDetailsActivity extends XmppActivity { + public static final String ACTION_VIEW_MUC = "view_muc"; + private Conversation conversation; + private TextView mYourNick; + private ImageView mYourPhoto; + private ImageButton mEditNickButton; + private TextView mRoleAffiliaton; + private TextView mFullJid; + private TextView mAccountJid; + private LinearLayout membersView; + private LinearLayout mMoreDetails; + private Button mInviteButton; + private String uuid = null; + + private OnClickListener inviteListener = new OnClickListener() { + + @Override + public void onClick(View v) { + inviteToConversation(conversation); + } + }; + + private List<User> users = new ArrayList<MucOptions.User>(); + private OnConversationUpdate onConvChanged = new OnConversationUpdate() { + + @Override + public void onConversationUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_muc_details); + mYourNick = (TextView) findViewById(R.id.muc_your_nick); + mYourPhoto = (ImageView) findViewById(R.id.your_photo); + mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button); + mFullJid = (TextView) findViewById(R.id.muc_jabberid); + membersView = (LinearLayout) findViewById(R.id.muc_members); + mAccountJid = (TextView) findViewById(R.id.details_account); + mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details); + mMoreDetails.setVisibility(View.GONE); + mInviteButton = (Button) findViewById(R.id.invite); + mInviteButton.setOnClickListener(inviteListener); + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + mEditNickButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + quickEdit(conversation.getMucOptions().getActualNick(), + new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + xmppConnectionService.renameInMuc(conversation, + value); + } + }); + } + }); + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case android.R.id.home: + finish(); + break; + case R.id.action_edit_subject: + if (conversation != null) { + quickEdit(conversation.getName(), new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + MessagePacket packet = xmppConnectionService + .getMessageGenerator().conferenceSubject( + conversation, value); + xmppConnectionService.sendMessagePacket( + conversation.getAccount(), packet); + } + }); + } + break; + } + return super.onOptionsItemSelected(menuItem); + } + + public String getReadableRole(int role) { + switch (role) { + case User.ROLE_MODERATOR: + return getString(R.string.moderator); + case User.ROLE_PARTICIPANT: + return getString(R.string.participant); + case User.ROLE_VISITOR: + return getString(R.string.visitor); + default: + return ""; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.muc_details, menu); + return true; + } + + @Override + void onBackendConnected() { + registerListener(); + if (getIntent().getAction().equals(ACTION_VIEW_MUC)) { + this.uuid = getIntent().getExtras().getString("uuid"); + } + if (uuid != null) { + this.conversation = xmppConnectionService + .findConversationByUuid(uuid); + if (this.conversation != null) { + populateView(); + } + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnConversationListChangedListener(); + } + super.onStop(); + } + + protected void registerListener() { + xmppConnectionService + .setOnConversationListChangedListener(this.onConvChanged); + xmppConnectionService.setOnRenameListener(new OnRenameListener() { + + @Override + public void onRename(final boolean success) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + if (success) { + Toast.makeText( + ConferenceDetailsActivity.this, + getString(R.string.your_nick_has_been_changed), + Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(ConferenceDetailsActivity.this, + getString(R.string.nick_in_use), + Toast.LENGTH_SHORT).show(); + } + } + }); + } + }); + } + + private void populateView() { + mAccountJid.setText(getString(R.string.using_account, conversation + .getAccount().getJid())); + mYourPhoto.setImageBitmap(avatarService().get( + conversation.getAccount(), getPixel(48))); + setTitle(conversation.getName()); + mFullJid.setText(conversation.getContactJid().split("/", 2)[0]); + mYourNick.setText(conversation.getMucOptions().getActualNick()); + mRoleAffiliaton = (TextView) findViewById(R.id.muc_role); + if (conversation.getMucOptions().online()) { + mMoreDetails.setVisibility(View.VISIBLE); + User self = conversation.getMucOptions().getSelf(); + switch (self.getAffiliation()) { + case User.AFFILIATION_ADMIN: + mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " (" + + getString(R.string.admin) + ")"); + break; + case User.AFFILIATION_OWNER: + mRoleAffiliaton.setText(getReadableRole(self.getRole()) + " (" + + getString(R.string.owner) + ")"); + break; + default: + mRoleAffiliaton.setText(getReadableRole(self.getRole())); + break; + } + } + this.users.clear(); + this.users.addAll(conversation.getMucOptions().getUsers()); + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + membersView.removeAllViews(); + for (final User user : conversation.getMucOptions().getUsers()) { + View view = inflater.inflate(R.layout.contact, membersView, + false); + TextView name = (TextView) view + .findViewById(R.id.contact_display_name); + TextView key = (TextView) view.findViewById(R.id.key); + TextView role = (TextView) view.findViewById(R.id.contact_jid); + if (user.getPgpKeyId() != 0) { + key.setVisibility(View.VISIBLE); + key.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + viewPgpKey(user); + } + }); + key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); + } + Bitmap bm; + Contact contact = user.getContact(); + if (contact != null) { + bm = avatarService().get(contact, getPixel(48)); + name.setText(contact.getDisplayName()); + role.setText(user.getName() + " \u2022 " + + getReadableRole(user.getRole())); + } else { + bm = avatarService().get(user.getName(), getPixel(48)); + name.setText(user.getName()); + role.setText(getReadableRole(user.getRole())); + } + ImageView iv = (ImageView) view.findViewById(R.id.contact_photo); + iv.setImageBitmap(bm); + membersView.addView(view); + } + } + + private void viewPgpKey(User user) { + PgpEngine pgp = xmppConnectionService.getPgpEngine(); + if (pgp != null) { + PendingIntent intent = pgp.getIntentForKey( + conversation.getAccount(), user.getPgpKeyId()); + if (intent != null) { + try { + startIntentSenderForResult(intent.getIntentSender(), 0, + null, 0, 0, 0); + } catch (SendIntentException e) { + + } + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java new file mode 100644 index 00000000..4c52c609 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -0,0 +1,434 @@ +package eu.siacs.conversations.ui; + +import java.util.Iterator; + +import org.openintents.openpgp.util.OpenPgpUtils; + +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentSender.SendIntentException; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Intents; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.CheckBox; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.CompoundButton; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.utils.UIHelper; + +public class ContactDetailsActivity extends XmppActivity { + public static final String ACTION_VIEW_CONTACT = "view_contact"; + + private Contact contact; + + private String accountJid; + private String contactJid; + + private TextView contactJidTv; + private TextView accountJidTv; + private TextView status; + private TextView lastseen; + private CheckBox send; + private CheckBox receive; + private QuickContactBadge badge; + + private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + ContactDetailsActivity.this.xmppConnectionService + .deleteContactOnServer(contact); + ContactDetailsActivity.this.finish(); + } + }; + + private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid()); + intent.putExtra(Intents.Insert.IM_PROTOCOL, + CommonDataKinds.Im.PROTOCOL_JABBER); + intent.putExtra("finishActivityOnSaveCompleted", true); + ContactDetailsActivity.this.startActivityForResult(intent, 0); + } + }; + private OnClickListener onBadgeClick = new OnClickListener() { + + @Override + public void onClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder( + ContactDetailsActivity.this); + builder.setTitle(getString(R.string.action_add_phone_book)); + builder.setMessage(getString(R.string.add_phone_book_text, + contact.getJid())); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add), addToPhonebook); + builder.create().show(); + } + }; + + private LinearLayout keys; + + private OnRosterUpdate rosterUpdate = new OnRosterUpdate() { + + @Override + public void onRosterUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + if (contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + xmppConnectionService.sendPresencePacket(contact + .getAccount(), + xmppConnectionService.getPresenceGenerator() + .sendPresenceUpdatesTo(contact)); + } else { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + } + } else { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .stopPresenceUpdatesTo(contact)); + } + } + }; + + private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); + } else { + xmppConnectionService.sendPresencePacket(contact.getAccount(), + xmppConnectionService.getPresenceGenerator() + .stopPresenceUpdatesFrom(contact)); + } + } + }; + + private OnAccountUpdate accountUpdate = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { + this.accountJid = getIntent().getExtras().getString("account"); + this.contactJid = getIntent().getExtras().getString("contact"); + } + setContentView(R.layout.activity_contact_details); + + contactJidTv = (TextView) findViewById(R.id.details_contactjid); + accountJidTv = (TextView) findViewById(R.id.details_account); + status = (TextView) findViewById(R.id.details_contactstatus); + lastseen = (TextView) findViewById(R.id.details_lastseen); + send = (CheckBox) findViewById(R.id.details_send_presence); + receive = (CheckBox) findViewById(R.id.details_receive_presence); + badge = (QuickContactBadge) findViewById(R.id.details_contact_badge); + keys = (LinearLayout) findViewById(R.id.details_contact_keys); + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + + } + + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(getString(R.string.cancel), null); + switch (menuItem.getItemId()) { + case android.R.id.home: + finish(); + break; + case R.id.action_delete_contact: + builder.setTitle(getString(R.string.action_delete_contact)) + .setMessage( + getString(R.string.remove_contact_text, + contact.getJid())) + .setPositiveButton(getString(R.string.delete), + removeFromRoster).create().show(); + break; + case R.id.action_edit_contact: + if (contact.getSystemAccount() == null) { + quickEdit(contact.getDisplayName(), new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + contact.setServerName(value); + ContactDetailsActivity.this.xmppConnectionService + .pushContactToServer(contact); + populateView(); + } + }); + } else { + Intent intent = new Intent(Intent.ACTION_EDIT); + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + Uri uri = Contacts.getLookupUri(id, systemAccount[1]); + intent.setDataAndType(uri, Contacts.CONTENT_ITEM_TYPE); + intent.putExtra("finishActivityOnSaveCompleted", true); + startActivity(intent); + } + break; + } + return super.onOptionsItemSelected(menuItem); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.contact_details, menu); + return true; + } + + private void populateView() { + send.setOnCheckedChangeListener(null); + receive.setOnCheckedChangeListener(null); + setTitle(contact.getDisplayName()); + if (contact.getOption(Contact.Options.FROM)) { + send.setText(R.string.send_presence_updates); + send.setChecked(true); + } else if (contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + send.setChecked(false); + send.setText(R.string.send_presence_updates); + } else { + send.setText(R.string.preemptively_grant); + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + send.setChecked(true); + } else { + send.setChecked(false); + } + } + if (contact.getOption(Contact.Options.TO)) { + receive.setText(R.string.receive_presence_updates); + receive.setChecked(true); + } else { + receive.setText(R.string.ask_for_presence_updates); + if (contact.getOption(Contact.Options.ASKING)) { + receive.setChecked(true); + } else { + receive.setChecked(false); + } + } + if (contact.getAccount().getStatus() == Account.STATUS_ONLINE) { + receive.setEnabled(true); + send.setEnabled(true); + } else { + receive.setEnabled(false); + send.setEnabled(false); + } + + send.setOnCheckedChangeListener(this.mOnSendCheckedChange); + receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange); + + lastseen.setText(UIHelper.lastseen(getApplicationContext(), + contact.lastseen.time)); + + switch (contact.getMostAvailableStatus()) { + case Presences.CHAT: + status.setText(R.string.contact_status_free_to_chat); + status.setTextColor(mColorGreen); + break; + case Presences.ONLINE: + status.setText(R.string.contact_status_online); + status.setTextColor(mColorGreen); + break; + case Presences.AWAY: + status.setText(R.string.contact_status_away); + status.setTextColor(mColorOrange); + break; + case Presences.XA: + status.setText(R.string.contact_status_extended_away); + status.setTextColor(mColorOrange); + break; + case Presences.DND: + status.setText(R.string.contact_status_do_not_disturb); + status.setTextColor(mColorRed); + break; + case Presences.OFFLINE: + status.setText(R.string.contact_status_offline); + status.setTextColor(mSecondaryTextColor); + break; + default: + status.setText(R.string.contact_status_offline); + status.setTextColor(mSecondaryTextColor); + break; + } + if (contact.getPresences().size() > 1) { + contactJidTv.setText(contact.getJid() + " (" + + contact.getPresences().size() + ")"); + } else { + contactJidTv.setText(contact.getJid()); + } + accountJidTv.setText(getString(R.string.using_account, contact + .getAccount().getJid())); + prepareContactBadge(badge, contact); + if (contact.getSystemAccount() == null) { + badge.setOnClickListener(onBadgeClick); + } + + keys.removeAllViews(); + boolean hasKeys = false; + LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); + for (Iterator<String> iterator = contact.getOtrFingerprints() + .iterator(); iterator.hasNext();) { + hasKeys = true; + final String otrFingerprint = iterator.next(); + View view = inflater.inflate(R.layout.contact_key, keys, false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + ImageButton remove = (ImageButton) view + .findViewById(R.id.button_remove); + remove.setVisibility(View.VISIBLE); + keyType.setText("OTR Fingerprint"); + key.setText(otrFingerprint); + keys.addView(view); + remove.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + confirmToDeleteFingerprint(otrFingerprint); + } + }); + } + if (contact.getPgpKeyId() != 0) { + hasKeys = true; + View view = inflater.inflate(R.layout.contact_key, keys, false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + keyType.setText("PGP Key ID"); + key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId())); + view.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService + .getPgpEngine(); + if (pgp != null) { + PendingIntent intent = pgp.getIntentForKey(contact); + if (intent != null) { + try { + startIntentSenderForResult( + intent.getIntentSender(), 0, null, 0, + 0, 0); + } catch (SendIntentException e) { + + } + } + } + } + }); + keys.addView(view); + } + if (hasKeys) { + keys.setVisibility(View.VISIBLE); + } else { + keys.setVisibility(View.GONE); + } + } + + private void prepareContactBadge(QuickContactBadge badge, Contact contact) { + if (contact.getSystemAccount() != null) { + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1])); + } + badge.setImageBitmap(avatarService().get(contact, getPixel(72))); + } + + protected void confirmToDeleteFingerprint(final String fingerprint) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.delete_fingerprint); + builder.setMessage(R.string.sure_delete_fingerprint); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.delete, + new android.content.DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (contact.deleteOtrFingerprint(fingerprint)) { + populateView(); + xmppConnectionService.syncRosterToDisk(contact + .getAccount()); + } + } + + }); + builder.create().show(); + } + + @Override + public void onBackendConnected() { + xmppConnectionService.setOnRosterUpdateListener(this.rosterUpdate); + xmppConnectionService + .setOnAccountListChangedListener(this.accountUpdate); + if ((accountJid != null) && (contactJid != null)) { + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + return; + } + this.contact = account.getRoster().getContact(contactJid); + populateView(); + } + } + + @Override + protected void onStop() { + super.onStop(); + xmppConnectionService.removeOnRosterUpdateListener(); + xmppConnectionService.removeOnAccountListChangedListener(); + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java new file mode 100644 index 00000000..1d7364d6 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -0,0 +1,936 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.ui.adapter.ConversationAdapter; +import eu.siacs.conversations.utils.ExceptionHelper; +import android.net.Uri; +import android.os.Bundle; +import android.os.SystemClock; +import android.provider.MediaStore; +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.app.AlertDialog; +import android.app.FragmentTransaction; +import android.app.PendingIntent; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.IntentSender.SendIntentException; +import android.content.Intent; +import android.support.v4.widget.SlidingPaneLayout; +import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.PopupMenu.OnMenuItemClickListener; +import android.widget.Toast; + +public class ConversationActivity extends XmppActivity implements + OnAccountUpdate, OnConversationUpdate, OnRosterUpdate { + + public static final String VIEW_CONVERSATION = "viewConversation"; + public static final String CONVERSATION = "conversationUuid"; + public static final String TEXT = "text"; + public static final String PRESENCE = "eu.siacs.conversations.presence"; + + public static final int REQUEST_SEND_MESSAGE = 0x0201; + public static final int REQUEST_DECRYPT_PGP = 0x0202; + private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203; + private static final int REQUEST_IMAGE_CAPTURE = 0x0204; + private static final int REQUEST_RECORD_AUDIO = 0x0205; + private static final int REQUEST_SEND_PGP_IMAGE = 0x0206; + public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; + + private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; + private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; + private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303; + private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; + private static final String STATE_PANEL_OPEN = "state_panel_open"; + + private String mOpenConverstaion = null; + private boolean mPanelOpen = true; + + private View mContentView; + + private List<Conversation> conversationList = new ArrayList<Conversation>(); + private Conversation selectedConversation = null; + private ListView listView; + + private boolean paneShouldBeOpen = true; + private ArrayAdapter<Conversation> listAdapter; + + private Toast prepareImageToast; + + private Uri pendingImageUri = null; + + public List<Conversation> getConversationList() { + return this.conversationList; + } + + public Conversation getSelectedConversation() { + return this.selectedConversation; + } + + public void setSelectedConversation(Conversation conversation) { + this.selectedConversation = conversation; + } + + public ListView getConversationListView() { + return this.listView; + } + + public boolean shouldPaneBeOpen() { + return paneShouldBeOpen; + } + + public void showConversationsOverview() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.openPane(); + } + } + + public void hideConversationsOverview() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.closePane(); + } + } + + public boolean isConversationsOverviewHideable() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + return mSlidingPaneLayout.isSlideable(); + } else { + return false; + } + } + + public boolean isConversationsOverviewVisable() { + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + return mSlidingPaneLayout.isOpen(); + } else { + return true; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + mOpenConverstaion = savedInstanceState.getString( + STATE_OPEN_CONVERSATION, null); + mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true); + } + + setContentView(R.layout.fragment_conversations_overview); + + listView = (ListView) findViewById(R.id.list); + + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + + this.listAdapter = new ConversationAdapter(this, conversationList); + listView.setAdapter(this.listAdapter); + + listView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView<?> arg0, View clickedView, + int position, long arg3) { + paneShouldBeOpen = false; + if (getSelectedConversation() != conversationList.get(position)) { + setSelectedConversation(conversationList.get(position)); + swapConversationFragment(); + } else { + hideConversationsOverview(); + } + } + }); + mContentView = findViewById(R.id.content_view_spl); + if (mContentView == null) { + mContentView = findViewById(R.id.content_view_ll); + } + if (mContentView instanceof SlidingPaneLayout) { + SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mSlidingPaneLayout.setParallaxDistance(150); + mSlidingPaneLayout + .setShadowResource(R.drawable.es_slidingpane_shadow); + mSlidingPaneLayout.setSliderFadeColor(0); + mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() { + + @Override + public void onPanelOpened(View arg0) { + paneShouldBeOpen = true; + ActionBar ab = getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(false); + ab.setHomeButtonEnabled(false); + ab.setTitle(R.string.app_name); + } + invalidateOptionsMenu(); + hideKeyboard(); + if (xmppConnectionServiceBound) { + xmppConnectionService.getNotificationService() + .setOpenConversation(null); + } + closeContextMenu(); + } + + @Override + public void onPanelClosed(View arg0) { + paneShouldBeOpen = false; + if ((conversationList.size() > 0) + && (getSelectedConversation() != null)) { + openConversation(getSelectedConversation()); + if (!getSelectedConversation().isRead()) { + xmppConnectionService.markRead( + getSelectedConversation(), true); + listView.invalidateViews(); + } + } + } + + @Override + public void onPanelSlide(View arg0, float arg1) { + // TODO Auto-generated method stub + + } + }); + } + } + + public void openConversation(Conversation conversation) { + ActionBar ab = getActionBar(); + if (ab != null) { + ab.setDisplayHomeAsUpEnabled(true); + ab.setHomeButtonEnabled(true); + if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE + || ConversationActivity.this + .useSubjectToIdentifyConference()) { + ab.setTitle(getSelectedConversation().getName()); + } else { + ab.setTitle(getSelectedConversation().getContactJid() + .split("/")[0]); + } + } + invalidateOptionsMenu(); + if (xmppConnectionServiceBound) { + xmppConnectionService.getNotificationService().setOpenConversation( + conversation); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.conversations, menu); + MenuItem menuSecure = menu.findItem(R.id.action_security); + MenuItem menuArchive = menu.findItem(R.id.action_archive); + MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details); + MenuItem menuContactDetails = menu + .findItem(R.id.action_contact_details); + MenuItem menuAttach = menu.findItem(R.id.action_attach_file); + MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history); + MenuItem menuAdd = menu.findItem(R.id.action_add); + MenuItem menuInviteContact = menu.findItem(R.id.action_invite); + MenuItem menuMute = menu.findItem(R.id.action_mute); + + if (isConversationsOverviewVisable() + && isConversationsOverviewHideable()) { + menuArchive.setVisible(false); + menuMucDetails.setVisible(false); + menuContactDetails.setVisible(false); + menuSecure.setVisible(false); + menuInviteContact.setVisible(false); + menuAttach.setVisible(false); + menuClearHistory.setVisible(false); + menuMute.setVisible(false); + } else { + menuAdd.setVisible(!isConversationsOverviewHideable()); + if (this.getSelectedConversation() != null) { + if (this.getSelectedConversation().getLatestMessage() + .getEncryption() != Message.ENCRYPTION_NONE) { + menuSecure.setIcon(R.drawable.ic_action_secure); + } + if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { + menuContactDetails.setVisible(false); + menuAttach.setVisible(false); + } else { + menuMucDetails.setVisible(false); + menuInviteContact.setVisible(false); + } + } + } + return true; + } + + private void selectPresenceToAttachFile(final int attachmentChoice) { + selectPresence(getSelectedConversation(), new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) { + pendingImageUri = xmppConnectionService.getFileBackend() + .getTakePhotoUri(); + Intent takePictureIntent = new Intent( + MediaStore.ACTION_IMAGE_CAPTURE); + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, + pendingImageUri); + if (takePictureIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(takePictureIntent, + REQUEST_IMAGE_CAPTURE); + } + } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, + getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG); + } else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) { + Intent intent = new Intent( + MediaStore.Audio.Media.RECORD_SOUND_ACTION); + startActivityForResult(intent, REQUEST_RECORD_AUDIO); + } + } + }); + } + + private void attachFile(final int attachmentChoice) { + final Conversation conversation = getSelectedConversation(); + if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + if (hasPgp()) { + if (conversation.getContact().getPgpKeyId() != 0) { + xmppConnectionService.getPgpEngine().hasKey( + conversation.getContact(), + new UiCallback<Contact>() { + + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + ConversationActivity.this.runIntent(pi, + attachmentChoice); + } + + @Override + public void success(Contact contact) { + selectPresenceToAttachFile(attachmentChoice); + } + + @Override + public void error(int error, Contact contact) { + displayErrorDialog(error); + } + }); + } else { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + fragment.showNoPGPKeyDialog(false, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + xmppConnectionService.databaseBackend + .updateConversation(conversation); + selectPresenceToAttachFile(attachmentChoice); + } + }); + } + } + } else { + showInstallPgpDialog(); + } + } else if (getSelectedConversation().getNextEncryption( + forceEncryption()) == Message.ENCRYPTION_NONE) { + selectPresenceToAttachFile(attachmentChoice); + } else { + selectPresenceToAttachFile(attachmentChoice); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + showConversationsOverview(); + return true; + } else if (item.getItemId() == R.id.action_add) { + startActivity(new Intent(this, StartConversationActivity.class)); + return true; + } else if (getSelectedConversation() != null) { + switch (item.getItemId()) { + case R.id.action_attach_file: + attachFileDialog(); + break; + case R.id.action_archive: + this.endConversation(getSelectedConversation()); + break; + case R.id.action_contact_details: + Contact contact = this.getSelectedConversation().getContact(); + if (contact.showInRoster()) { + switchToContactDetails(contact); + } else { + showAddToRosterDialog(getSelectedConversation()); + } + break; + case R.id.action_muc_details: + Intent intent = new Intent(this, + ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", getSelectedConversation().getUuid()); + startActivity(intent); + break; + case R.id.action_invite: + inviteToConversation(getSelectedConversation()); + break; + case R.id.action_security: + selectEncryptionDialog(getSelectedConversation()); + break; + case R.id.action_clear_history: + clearHistoryDialog(getSelectedConversation()); + break; + case R.id.action_mute: + muteConversationDialog(getSelectedConversation()); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } else { + return super.onOptionsItemSelected(item); + } + } + + public void endConversation(Conversation conversation) { + conversation.setStatus(Conversation.STATUS_ARCHIVED); + paneShouldBeOpen = true; + showConversationsOverview(); + xmppConnectionService.archiveConversation(conversation); + if (conversationList.size() > 0) { + setSelectedConversation(conversationList.get(0)); + } else { + setSelectedConversation(null); + } + } + + @SuppressLint("InflateParams") + protected void clearHistoryDialog(final Conversation conversation) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.clear_conversation_history)); + View dialogView = getLayoutInflater().inflate( + R.layout.dialog_clear_history, null); + final CheckBox endConversationCheckBox = (CheckBox) dialogView + .findViewById(R.id.end_conversation_checkbox); + builder.setView(dialogView); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.delete_messages), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + ConversationActivity.this.xmppConnectionService + .clearConversationHistory(conversation); + if (endConversationCheckBox.isChecked()) { + endConversation(conversation); + } + } + }); + builder.create().show(); + } + + protected void attachFileDialog() { + View menuAttachFile = findViewById(R.id.action_attach_file); + if (menuAttachFile == null) { + return; + } + PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile); + attachFilePopup.inflate(R.menu.attachment_choices); + attachFilePopup + .setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.attach_choose_picture: + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + break; + case R.id.attach_take_picture: + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + break; + case R.id.attach_record_voice: + attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); + break; + } + return false; + } + }); + attachFilePopup.show(); + } + + protected void selectEncryptionDialog(final Conversation conversation) { + View menuItemView = findViewById(R.id.action_security); + if (menuItemView == null) { + return; + } + PopupMenu popup = new PopupMenu(this, menuItemView); + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.encryption_choice_none: + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + item.setChecked(true); + break; + case R.id.encryption_choice_otr: + conversation.setNextEncryption(Message.ENCRYPTION_OTR); + item.setChecked(true); + break; + case R.id.encryption_choice_pgp: + if (hasPgp()) { + if (conversation.getAccount().getKeys() + .has("pgp_signature")) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + item.setChecked(true); + } else { + announcePgp(conversation.getAccount(), + conversation); + } + } else { + showInstallPgpDialog(); + } + break; + default: + conversation.setNextEncryption(Message.ENCRYPTION_NONE); + break; + } + xmppConnectionService.databaseBackend + .updateConversation(conversation); + fragment.updateChatMsgHint(); + return true; + } + }); + popup.inflate(R.menu.encryption_choices); + MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr); + MenuItem none = popup.getMenu().findItem( + R.id.encryption_choice_none); + if (conversation.getMode() == Conversation.MODE_MULTI) { + otr.setEnabled(false); + } else { + if (forceEncryption()) { + none.setVisible(false); + } + } + switch (conversation.getNextEncryption(forceEncryption())) { + case Message.ENCRYPTION_NONE: + none.setChecked(true); + break; + case Message.ENCRYPTION_OTR: + otr.setChecked(true); + break; + case Message.ENCRYPTION_PGP: + popup.getMenu().findItem(R.id.encryption_choice_pgp) + .setChecked(true); + break; + default: + popup.getMenu().findItem(R.id.encryption_choice_none) + .setChecked(true); + break; + } + popup.show(); + } + } + + protected void muteConversationDialog(final Conversation conversation) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.disable_notifications_for_this_conversation); + final int[] durations = getResources().getIntArray( + R.array.mute_options_durations); + builder.setItems(R.array.mute_options_descriptions, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + long till; + if (durations[which] == -1) { + till = Long.MAX_VALUE; + } else { + till = SystemClock.elapsedRealtime() + + (durations[which] * 1000); + } + conversation.setMutedTill(till); + ConversationActivity.this.xmppConnectionService.databaseBackend + .updateConversation(conversation); + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.updateMessages(); + } + } + }); + builder.create().show(); + } + + protected ConversationFragment swapConversationFragment() { + ConversationFragment selectedFragment = new ConversationFragment(); + if (!isFinishing()) { + + FragmentTransaction transaction = getFragmentManager() + .beginTransaction(); + transaction.replace(R.id.selected_conversation, selectedFragment, + "conversation"); + try { + transaction.commitAllowingStateLoss(); + } catch (IllegalStateException e) { + return selectedFragment; + } + } + return selectedFragment; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (!isConversationsOverviewVisable()) { + showConversationsOverview(); + return false; + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onNewIntent(Intent intent) { + if (xmppConnectionServiceBound) { + if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) { + handleViewConversationIntent(intent); + } + } else { + setIntent(intent); + } + } + + @Override + public void onStart() { + super.onStart(); + if (this.xmppConnectionServiceBound) { + this.onBackendConnected(); + } + if (conversationList.size() >= 1) { + this.onConversationUpdate(); + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnConversationListChangedListener(); + xmppConnectionService.removeOnAccountListChangedListener(); + xmppConnectionService.removeOnRosterUpdateListener(); + xmppConnectionService.getNotificationService().setOpenConversation( + null); + } + super.onStop(); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + Conversation conversation = getSelectedConversation(); + if (conversation != null) { + savedInstanceState.putString(STATE_OPEN_CONVERSATION, + conversation.getUuid()); + } + savedInstanceState.putBoolean(STATE_PANEL_OPEN, + isConversationsOverviewVisable()); + super.onSaveInstanceState(savedInstanceState); + } + + @Override + void onBackendConnected() { + this.registerListener(); + updateConversationList(); + + if (xmppConnectionService.getAccounts().size() == 0) { + startActivity(new Intent(this, EditAccountActivity.class)); + } else if (conversationList.size() <= 0) { + startActivity(new Intent(this, StartConversationActivity.class)); + finish(); + } else if (getIntent() != null + && VIEW_CONVERSATION.equals(getIntent().getType())) { + handleViewConversationIntent(getIntent()); + setIntent(null); + } else if (mOpenConverstaion != null) { + selectConversationByUuid(mOpenConverstaion); + paneShouldBeOpen = mPanelOpen; + if (paneShouldBeOpen) { + showConversationsOverview(); + } + swapConversationFragment(); + mOpenConverstaion = null; + } else { + showConversationsOverview(); + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.onBackendConnected(); + } else { + pendingImageUri = null; + setSelectedConversation(conversationList.get(0)); + swapConversationFragment(); + } + } + + if (pendingImageUri != null) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + ExceptionHelper.checkForCrash(this, this.xmppConnectionService); + } + + private void handleViewConversationIntent(Intent intent) { + String uuid = (String) intent.getExtras().get(CONVERSATION); + String text = intent.getExtras().getString(TEXT, null); + selectConversationByUuid(uuid); + paneShouldBeOpen = false; + swapConversationFragment().setText(text); + } + + private void selectConversationByUuid(String uuid) { + for (int i = 0; i < conversationList.size(); ++i) { + if (conversationList.get(i).getUuid().equals(uuid)) { + setSelectedConversation(conversationList.get(i)); + } + } + } + + public void registerListener() { + xmppConnectionService.setOnConversationListChangedListener(this); + xmppConnectionService.setOnAccountListChangedListener(this); + xmppConnectionService.setOnRosterUpdateListener(this); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_DECRYPT_PGP) { + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.hideSnackbar(); + selectedFragment.updateMessages(); + } + } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { + pendingImageUri = data.getData(); + if (xmppConnectionServiceBound) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + } else if (requestCode == REQUEST_SEND_PGP_IMAGE) { + + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + } else if (requestCode == REQUEST_ANNOUNCE_PGP) { + announcePgp(getSelectedConversation().getAccount(), + getSelectedConversation()); + } else if (requestCode == REQUEST_ENCRYPT_MESSAGE) { + // encryptTextMessage(); + } else if (requestCode == REQUEST_IMAGE_CAPTURE) { + if (xmppConnectionServiceBound) { + attachImageToConversation(getSelectedConversation(), + pendingImageUri); + pendingImageUri = null; + } + Intent intent = new Intent( + Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(pendingImageUri); + sendBroadcast(intent); + } else if (requestCode == REQUEST_RECORD_AUDIO) { + attachAudioToConversation(getSelectedConversation(), + data.getData()); + } + } else { + if (requestCode == REQUEST_IMAGE_CAPTURE) { + pendingImageUri = null; + } + } + } + + private void attachAudioToConversation(Conversation conversation, Uri uri) { + + } + + private void attachImageToConversation(Conversation conversation, Uri uri) { + prepareImageToast = Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), Toast.LENGTH_LONG); + prepareImageToast.show(); + xmppConnectionService.attachImageToConversation(conversation, uri, + new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, + Message object) { + hidePrepareImageToast(); + ConversationActivity.this.runIntent(pi, + ConversationActivity.REQUEST_SEND_PGP_IMAGE); + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + hidePrepareImageToast(); + displayErrorDialog(error); + } + }); + } + + private void hidePrepareImageToast() { + if (prepareImageToast != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + prepareImageToast.cancel(); + } + }); + } + } + + public void updateConversationList() { + xmppConnectionService + .populateWithOrderedConversations(conversationList); + listAdapter.notifyDataSetChanged(); + } + + public void runIntent(PendingIntent pi, int requestCode) { + try { + this.startIntentSenderForResult(pi.getIntentSender(), requestCode, + null, 0, 0, 0); + } catch (SendIntentException e1) { + } + } + + public void encryptTextMessage(Message message) { + xmppConnectionService.getPgpEngine().encrypt(message, + new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, + Message message) { + ConversationActivity.this.runIntent(pi, + ConversationActivity.REQUEST_SEND_MESSAGE); + } + + @Override + public void success(Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int error, Message message) { + + } + }); + } + + public boolean forceEncryption() { + return getPreferences().getBoolean("force_encryption", false); + } + + public boolean useSendButtonToIndicateStatus() { + return getPreferences().getBoolean("send_button_status", false); + } + + public boolean indicateReceived() { + return getPreferences().getBoolean("indicate_received", false); + } + + @Override + public void onAccountUpdate() { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + fragment.updateMessages(); + } + }); + } + } + + @Override + public void onConversationUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + updateConversationList(); + if (paneShouldBeOpen) { + if (conversationList.size() >= 1) { + swapConversationFragment(); + } else { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + finish(); + } + } + ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (selectedFragment != null) { + selectedFragment.updateMessages(); + } + } + }); + } + + @Override + public void onRosterUpdate() { + final ConversationFragment fragment = (ConversationFragment) getFragmentManager() + .findFragmentByTag("conversation"); + if (fragment != null) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + fragment.updateMessages(); + } + }); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java new file mode 100644 index 00000000..20eeeb30 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -0,0 +1,892 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.EditMessage.OnEnterPressed; +import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; +import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; +import eu.siacs.conversations.ui.adapter.MessageAdapter; +import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; +import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import eu.siacs.conversations.utils.UIHelper; +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; +import android.os.Bundle; +import android.text.Editable; +import android.text.Selection; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView.OnScrollListener; +import android.widget.TextView.OnEditorActionListener; +import android.widget.AbsListView; + +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.ListView; +import android.widget.ImageButton; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class ConversationFragment extends Fragment { + + protected Conversation conversation; + protected ListView messagesView; + protected LayoutInflater inflater; + protected List<Message> messageList = new ArrayList<Message>(); + protected MessageAdapter messageListAdapter; + protected Contact contact; + + protected String queuedPqpMessage = null; + + private EditMessage mEditMessage; + private ImageButton mSendButton; + private String pastedText = null; + private RelativeLayout snackbar; + private TextView snackbarMessage; + private TextView snackbarAction; + + private boolean messagesLoaded = false; + + private IntentSender askForPassphraseIntent = null; + + private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<Message>(); + private boolean mDecryptJobRunning = false; + + private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() { + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_SEND) { + InputMethodManager imm = (InputMethodManager) v.getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + sendMessage(); + return true; + } else { + return false; + } + } + }; + + private OnClickListener mSendButtonListener = new OnClickListener() { + + @Override + public void onClick(View v) { + sendMessage(); + } + }; + protected OnClickListener clickToDecryptListener = new OnClickListener() { + + @Override + public void onClick(View v) { + if (activity.hasPgp() && askForPassphraseIntent != null) { + try { + getActivity().startIntentSenderForResult( + askForPassphraseIntent, + ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, + 0, 0); + } catch (SendIntentException e) { + // + } + } + } + }; + + private OnClickListener clickToMuc = new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(getActivity(), + ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", conversation.getUuid()); + startActivity(intent); + } + }; + + private OnClickListener leaveMuc = new OnClickListener() { + + @Override + public void onClick(View v) { + activity.endConversation(conversation); + } + }; + + private OnClickListener joinMuc = new OnClickListener() { + + @Override + public void onClick(View v) { + activity.xmppConnectionService.joinMuc(conversation); + } + }; + + private OnClickListener enterPassword = new OnClickListener() { + + @Override + public void onClick(View v) { + MucOptions muc = conversation.getMucOptions(); + String password = muc.getPassword(); + if (password == null) { + password = ""; + } + activity.quickPasswordEdit(password, new OnValueEdited() { + + @Override + public void onValueEdited(String value) { + activity.xmppConnectionService.providePasswordForMuc( + conversation, value); + } + }); + } + }; + + private OnScrollListener mOnScrollListener = new OnScrollListener() { + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // TODO Auto-generated method stub + + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (firstVisibleItem == 0 && messagesLoaded) { + long timestamp = messageList.get(0).getTimeSent(); + messagesLoaded = false; + int size = activity.xmppConnectionService.loadMoreMessages( + conversation, timestamp); + messageList.clear(); + messageList.addAll(conversation.getMessages()); + updateStatusMessages(); + messageListAdapter.notifyDataSetChanged(); + if (size != 0) { + messagesLoaded = true; + } + messagesView.setSelectionFromTop(size + 1, 0); + } + } + }; + + private ConversationActivity activity; + private Message selectedMessage; + + private void sendMessage() { + if (this.conversation == null) { + return; + } + if (mEditMessage.getText().length() < 1) { + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextPresence(null); + updateChatMsgHint(); + } + return; + } + Message message = new Message(conversation, mEditMessage.getText() + .toString(), conversation.getNextEncryption(activity + .forceEncryption())); + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getNextPresence() != null) { + message.setPresence(conversation.getNextPresence()); + message.setType(Message.TYPE_PRIVATE); + conversation.setNextPresence(null); + } + } + if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) { + sendOtrMessage(message); + } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { + sendPgpMessage(message); + } else { + sendPlainTextMessage(message); + } + } + + public void updateChatMsgHint() { + if (conversation.getMode() == Conversation.MODE_MULTI + && conversation.getNextPresence() != null) { + this.mEditMessage.setHint(getString( + R.string.send_private_message_to, + conversation.getNextPresence())); + } else { + switch (conversation.getNextEncryption(activity.forceEncryption())) { + case Message.ENCRYPTION_NONE: + mEditMessage + .setHint(getString(R.string.send_plain_text_message)); + break; + case Message.ENCRYPTION_OTR: + mEditMessage.setHint(getString(R.string.send_otr_message)); + break; + case Message.ENCRYPTION_PGP: + mEditMessage.setHint(getString(R.string.send_pgp_message)); + break; + default: + break; + } + } + } + + @Override + public View onCreateView(final LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.fragment_conversation, + container, false); + mEditMessage = (EditMessage) view.findViewById(R.id.textinput); + mEditMessage.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.hideConversationsOverview(); + } + }); + mEditMessage.setOnEditorActionListener(mEditorActionListener); + mEditMessage.setOnEnterPressedListener(new OnEnterPressed() { + + @Override + public void onEnterPressed() { + sendMessage(); + } + }); + + mSendButton = (ImageButton) view.findViewById(R.id.textSendButton); + mSendButton.setOnClickListener(this.mSendButtonListener); + + snackbar = (RelativeLayout) view.findViewById(R.id.snackbar); + snackbarMessage = (TextView) view.findViewById(R.id.snackbar_message); + snackbarAction = (TextView) view.findViewById(R.id.snackbar_action); + + messagesView = (ListView) view.findViewById(R.id.messages_view); + messagesView.setOnScrollListener(mOnScrollListener); + messagesView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL); + messageListAdapter = new MessageAdapter( + (ConversationActivity) getActivity(), this.messageList); + messageListAdapter + .setOnContactPictureClicked(new OnContactPictureClicked() { + + @Override + public void onContactPictureClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getPresence() != null) { + highlightInConference(message.getPresence()); + } else { + highlightInConference(message + .getCounterpart()); + } + } else { + Contact contact = message.getConversation() + .getContact(); + if (contact.showInRoster()) { + activity.switchToContactDetails(contact); + } else { + activity.showAddToRosterDialog(message + .getConversation()); + } + } + } + } + }); + messageListAdapter + .setOnContactPictureLongClicked(new OnContactPictureLongClicked() { + + @Override + public void onContactPictureLongClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getPresence() != null) { + privateMessageWith(message.getPresence()); + } else { + privateMessageWith(message.getCounterpart()); + } + } + } + } + }); + messagesView.setAdapter(messageListAdapter); + + registerForContextMenu(messagesView); + + return view; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + this.selectedMessage = this.messageList.get(acmi.position); + populateContextMenu(menu); + } + + private void populateContextMenu(ContextMenu menu) { + if (this.selectedMessage.getType() != Message.TYPE_STATUS) { + activity.getMenuInflater().inflate(R.menu.message_context, menu); + menu.setHeaderTitle(R.string.message_options); + MenuItem copyText = menu.findItem(R.id.copy_text); + MenuItem shareImage = menu.findItem(R.id.share_image); + MenuItem sendAgain = menu.findItem(R.id.send_again); + MenuItem copyUrl = menu.findItem(R.id.copy_url); + MenuItem downloadImage = menu.findItem(R.id.download_image); + if (this.selectedMessage.getType() != Message.TYPE_TEXT + || this.selectedMessage.getDownloadable() != null) { + copyText.setVisible(false); + } + if (this.selectedMessage.getType() != Message.TYPE_IMAGE + || this.selectedMessage.getDownloadable() != null) { + shareImage.setVisible(false); + } + if (this.selectedMessage.getStatus() != Message.STATUS_SEND_FAILED) { + sendAgain.setVisible(false); + } + if ((this.selectedMessage.getType() != Message.TYPE_IMAGE && this.selectedMessage + .getDownloadable() == null) + || this.selectedMessage.getImageParams().url == null) { + copyUrl.setVisible(false); + } + + if (this.selectedMessage.getType() != Message.TYPE_TEXT + || this.selectedMessage.getDownloadable() != null + || !this.selectedMessage.bodyContainsDownloadable()) { + downloadImage.setVisible(false); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.share_image: + shareImage(selectedMessage); + return true; + case R.id.copy_text: + copyText(selectedMessage); + return true; + case R.id.send_again: + resendMessage(selectedMessage); + return true; + case R.id.copy_url: + copyUrl(selectedMessage); + return true; + case R.id.download_image: + downloadImage(selectedMessage); + return true; + default: + return super.onContextItemSelected(item); + } + } + + private void shareImage(Message message) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, + activity.xmppConnectionService.getFileBackend() + .getJingleFileUri(message)); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + shareIntent.setType("image/webp"); + activity.startActivity(Intent.createChooser(shareIntent, + getText(R.string.share_with))); + } + + private void copyText(Message message) { + if (activity.copyTextToClipboard(message.getMergedBody(), + R.string.message_text)) { + Toast.makeText(activity, R.string.message_copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + } + } + + private void resendMessage(Message message) { + activity.xmppConnectionService.resendFailedMessages(message); + } + + private void copyUrl(Message message) { + if (activity.copyTextToClipboard( + message.getImageParams().url.toString(), R.string.image_url)) { + Toast.makeText(activity, R.string.url_copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + } + } + + private void downloadImage(Message message) { + activity.xmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + } + + protected void privateMessageWith(String counterpart) { + this.mEditMessage.setText(""); + this.conversation.setNextPresence(counterpart); + updateChatMsgHint(); + } + + protected void highlightInConference(String nick) { + String oldString = mEditMessage.getText().toString().trim(); + if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) { + mEditMessage.getText().insert(0, nick + ": "); + } else { + if (mEditMessage.getText().charAt( + mEditMessage.getSelectionStart() - 1) != ' ') { + nick = " " + nick; + } + mEditMessage.getText().insert(mEditMessage.getSelectionStart(), + nick + " "); + } + } + + @Override + public void onStart() { + super.onStart(); + this.activity = (ConversationActivity) getActivity(); + if (activity.xmppConnectionServiceBound) { + this.onBackendConnected(); + } + } + + @Override + public void onStop() { + mDecryptJobRunning = false; + super.onStop(); + if (this.conversation != null) { + this.conversation.setNextMessage(mEditMessage.getText().toString()); + } + } + + public void onBackendConnected() { + this.activity = (ConversationActivity) getActivity(); + this.conversation = activity.getSelectedConversation(); + if (this.conversation == null) { + return; + } + String oldString = conversation.getNextMessage().trim(); + if (this.pastedText == null) { + this.mEditMessage.setText(oldString); + } else { + + if (oldString.isEmpty()) { + mEditMessage.setText(pastedText); + } else { + mEditMessage.setText(oldString + " " + pastedText); + } + pastedText = null; + } + int position = mEditMessage.length(); + Editable etext = mEditMessage.getText(); + Selection.setSelection(etext, position); + if (activity.isConversationsOverviewHideable()) { + if (!activity.shouldPaneBeOpen()) { + activity.hideConversationsOverview(); + activity.openConversation(conversation); + } + } + if (this.conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextPresence(null); + } + updateMessages(); + } + + public void updateMessages() { + if (getView() == null) { + return; + } + hideSnackbar(); + final ConversationActivity activity = (ConversationActivity) getActivity(); + if (this.conversation != null) { + final Contact contact = this.conversation.getContact(); + if (this.conversation.isMuted()) { + showSnackbar(R.string.notifications_disabled, R.string.enable, + new OnClickListener() { + + @Override + public void onClick(View v) { + conversation.setMutedTill(0); + activity.xmppConnectionService.databaseBackend + .updateConversation(conversation); + updateMessages(); + } + }); + } else if (!contact.showInRoster() + && contact + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar(R.string.contact_added_you, R.string.add_back, + new OnClickListener() { + + @Override + public void onClick(View v) { + activity.xmppConnectionService + .createContact(contact); + activity.switchToContactDetails(contact); + } + }); + } + for (Message message : this.conversation.getMessages()) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + && (message.getStatus() == Message.STATUS_RECEIVED || message + .getStatus() >= Message.STATUS_SEND) + && message.getDownloadable() == null) { + if (!mEncryptedMessages.contains(message)) { + mEncryptedMessages.add(message); + } + } + } + decryptNext(); + this.messageList.clear(); + if (this.conversation.getMessages().size() == 0) { + messagesLoaded = false; + } else { + this.messageList.addAll(this.conversation.getMessages()); + messagesLoaded = true; + updateStatusMessages(); + } + this.messageListAdapter.notifyDataSetChanged(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (messageList.size() >= 1) { + makeFingerprintWarning(); + } + } else { + if (!conversation.getMucOptions().online() + && conversation.getAccount().getStatus() == Account.STATUS_ONLINE) { + int error = conversation.getMucOptions().getError(); + switch (error) { + case MucOptions.ERROR_NICK_IN_USE: + showSnackbar(R.string.nick_in_use, R.string.edit, + clickToMuc); + break; + case MucOptions.ERROR_ROOM_NOT_FOUND: + showSnackbar(R.string.conference_not_found, + R.string.leave, leaveMuc); + break; + case MucOptions.ERROR_PASSWORD_REQUIRED: + showSnackbar(R.string.conference_requires_password, + R.string.enter_password, enterPassword); + break; + case MucOptions.ERROR_BANNED: + showSnackbar(R.string.conference_banned, + R.string.leave, leaveMuc); + break; + case MucOptions.ERROR_MEMBERS_ONLY: + showSnackbar(R.string.conference_members_only, + R.string.leave, leaveMuc); + break; + case MucOptions.KICKED_FROM_ROOM: + showSnackbar(R.string.conference_kicked, R.string.join, + joinMuc); + break; + default: + break; + } + } + } + getActivity().invalidateOptionsMenu(); + updateChatMsgHint(); + if (!activity.shouldPaneBeOpen()) { + activity.xmppConnectionService.markRead(conversation, true); + activity.updateConversationList(); + } + this.updateSendButton(); + } + } + + private void decryptNext() { + Message next = this.mEncryptedMessages.peek(); + PgpEngine engine = activity.xmppConnectionService.getPgpEngine(); + + if (next != null && engine != null && !mDecryptJobRunning) { + mDecryptJobRunning = true; + engine.decrypt(next, new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, Message message) { + mDecryptJobRunning = false; + askForPassphraseIntent = pi.getIntentSender(); + showSnackbar(R.string.openpgp_messages_found, + R.string.decrypt, clickToDecryptListener); + } + + @Override + public void success(Message message) { + mDecryptJobRunning = false; + mEncryptedMessages.remove(); + activity.xmppConnectionService.updateMessage(message); + } + + @Override + public void error(int error, Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); + mDecryptJobRunning = false; + mEncryptedMessages.remove(); + activity.xmppConnectionService.updateConversationUi(); + } + }); + } + } + + private void messageSent() { + int size = this.messageList.size(); + messagesView.setSelection(size - 1); + mEditMessage.setText(""); + updateChatMsgHint(); + } + + public void updateSendButton() { + Conversation c = this.conversation; + if (activity.useSendButtonToIndicateStatus() && c != null + && c.getAccount().getStatus() == Account.STATUS_ONLINE) { + if (c.getMode() == Conversation.MODE_SINGLE) { + switch (c.getContact().getMostAvailableStatus()) { + case Presences.CHAT: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + break; + case Presences.ONLINE: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + break; + case Presences.AWAY: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_away); + break; + case Presences.XA: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_away); + break; + case Presences.DND: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_dnd); + break; + default: + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + break; + } + } else if (c.getMode() == Conversation.MODE_MULTI) { + if (c.getMucOptions().online()) { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_online); + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } else { + this.mSendButton + .setImageResource(R.drawable.ic_action_send_now_offline); + } + } + + protected void updateStatusMessages() { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + for (int i = this.messageList.size() - 1; i >= 0; --i) { + if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) { + return; + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { + this.messageList.add(i + 1, + Message.createStatusMessage(conversation)); + return; + } + } + } + } + } + + protected void makeFingerprintWarning() { + Set<String> knownFingerprints = conversation.getContact() + .getOtrFingerprints(); + if (conversation.hasValidOtrSession() + && (!conversation.isMuted()) + && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints + .contains(conversation.getOtrFingerprint()))) { + showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, + new OnClickListener() { + + @Override + public void onClick(View v) { + if (conversation.getOtrFingerprint() != null) { + AlertDialog dialog = UIHelper + .getVerifyFingerprintDialog( + (ConversationActivity) getActivity(), + conversation, snackbar); + dialog.show(); + } + } + }); + } + } + + protected void showSnackbar(int message, int action, + OnClickListener clickListener) { + snackbar.setVisibility(View.VISIBLE); + snackbar.setOnClickListener(null); + snackbarMessage.setText(message); + snackbarMessage.setOnClickListener(null); + snackbarAction.setText(action); + snackbarAction.setOnClickListener(clickListener); + } + + protected void hideSnackbar() { + snackbar.setVisibility(View.GONE); + } + + protected void sendPlainTextMessage(Message message) { + ConversationActivity activity = (ConversationActivity) getActivity(); + activity.xmppConnectionService.sendMessage(message); + messageSent(); + } + + protected void sendPgpMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + final Contact contact = message.getConversation().getContact(); + if (activity.hasPgp()) { + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (contact.getPgpKeyId() != 0) { + xmppService.getPgpEngine().hasKey(contact, + new UiCallback<Contact>() { + + @Override + public void userInputRequried(PendingIntent pi, + Contact contact) { + activity.runIntent( + pi, + ConversationActivity.REQUEST_ENCRYPT_MESSAGE); + } + + @Override + public void success(Contact contact) { + messageSent(); + activity.encryptTextMessage(message); + } + + @Override + public void error(int error, Contact contact) { + + } + }); + + } else { + showNoPGPKeyDialog(false, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + xmppService.databaseBackend + .updateConversation(conversation); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } else { + if (conversation.getMucOptions().pgpKeysInUse()) { + if (!conversation.getMucOptions().everybodyHasKeys()) { + Toast warning = Toast + .makeText(getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); + warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); + warning.show(); + } + activity.encryptTextMessage(message); + messageSent(); + } else { + showNoPGPKeyDialog(true, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + conversation + .setNextEncryption(Message.ENCRYPTION_NONE); + message.setEncryption(Message.ENCRYPTION_NONE); + xmppService.databaseBackend + .updateConversation(conversation); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } + } else { + activity.showInstallPgpDialog(); + } + } + + public void showNoPGPKeyDialog(boolean plural, + DialogInterface.OnClickListener listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + if (plural) { + builder.setTitle(getString(R.string.no_pgp_keys)); + builder.setMessage(getText(R.string.contacts_have_no_pgp_keys)); + } else { + builder.setTitle(getString(R.string.no_pgp_key)); + builder.setMessage(getText(R.string.contact_has_no_pgp_key)); + } + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.send_unencrypted), + listener); + builder.create().show(); + } + + protected void sendOtrMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + if (conversation.hasValidOtrSession()) { + activity.xmppConnectionService.sendMessage(message); + messageSent(); + } else { + activity.selectPresence(message.getConversation(), + new OnPresenceSelected() { + + @Override + public void onPresenceSelected() { + message.setPresence(conversation.getNextPresence()); + xmppService.sendMessage(message); + messageSent(); + } + }); + } + } + + public void setText(String text) { + this.pastedText = text; + } + + public void clearInputField() { + this.mEditMessage.setText(""); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java new file mode 100644 index 00000000..58ca49cc --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -0,0 +1,410 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AutoCompleteTextView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.utils.Validator; +import eu.siacs.conversations.xmpp.XmppConnection.Features; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class EditAccountActivity extends XmppActivity { + + private AutoCompleteTextView mAccountJid; + private EditText mPassword; + private EditText mPasswordConfirm; + private CheckBox mRegisterNew; + private Button mCancelButton; + private Button mSaveButton; + + private LinearLayout mStats; + private TextView mServerInfoSm; + private TextView mServerInfoCarbons; + private TextView mServerInfoPep; + private TextView mSessionEst; + private TextView mOtrFingerprint; + private RelativeLayout mOtrFingerprintBox; + private ImageButton mOtrFingerprintToClipboardButton; + + private String jidToEdit; + private Account mAccount; + + private boolean mFetchingAvatar = false; + + private OnClickListener mSaveButtonClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_DISABLED) { + mAccount.setOption(Account.OPTION_DISABLED, false); + xmppConnectionService.updateAccount(mAccount); + return; + } + if (!Validator.isValidJid(mAccountJid.getText().toString())) { + mAccountJid.setError(getString(R.string.invalid_jid)); + mAccountJid.requestFocus(); + return; + } + boolean registerNewAccount = mRegisterNew.isChecked(); + String[] jidParts = mAccountJid.getText().toString().split("@"); + String username = jidParts[0]; + String server; + if (jidParts.length >= 2) { + server = jidParts[1]; + } else { + server = ""; + } + String password = mPassword.getText().toString(); + String passwordConfirm = mPasswordConfirm.getText().toString(); + if (registerNewAccount) { + if (!password.equals(passwordConfirm)) { + mPasswordConfirm + .setError(getString(R.string.passwords_do_not_match)); + mPasswordConfirm.requestFocus(); + return; + } + } + if (mAccount != null) { + mAccount.setPassword(password); + mAccount.setUsername(username); + mAccount.setServer(server); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.updateAccount(mAccount); + } else { + if (xmppConnectionService.findAccountByJid(mAccountJid + .getText().toString()) != null) { + mAccountJid + .setError(getString(R.string.account_already_exists)); + mAccountJid.requestFocus(); + return; + } + mAccount = new Account(username, server, password); + mAccount.setOption(Account.OPTION_USETLS, true); + mAccount.setOption(Account.OPTION_USECOMPRESSION, true); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.createAccount(mAccount); + } + if (jidToEdit != null) { + finish(); + } else { + updateSaveButton(); + updateAccountInformation(); + } + + } + }; + private OnClickListener mCancelButtonClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + finish(); + } + }; + private OnAccountUpdate mOnAccountUpdateListener = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mAccount != null + && mAccount.getStatus() != Account.STATUS_ONLINE + && mFetchingAvatar) { + startActivity(new Intent(getApplicationContext(), + ManageAccountActivity.class)); + finish(); + } else if (jidToEdit == null && mAccount != null + && mAccount.getStatus() == Account.STATUS_ONLINE) { + if (!mFetchingAvatar) { + mFetchingAvatar = true; + xmppConnectionService.checkForAvatar(mAccount, + mAvatarFetchCallback); + } + } else { + updateSaveButton(); + } + if (mAccount != null) { + updateAccountInformation(); + } + } + }); + } + }; + private UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() { + + @Override + public void userInputRequried(PendingIntent pi, Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void success(Avatar avatar) { + finishInitialSetup(avatar); + } + + @Override + public void error(int errorCode, Avatar avatar) { + finishInitialSetup(avatar); + } + }; + private KnownHostsAdapter mKnownHostsAdapter; + private TextWatcher mTextWatcher = new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + updateSaveButton(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + + } + + @Override + public void afterTextChanged(Editable s) { + + } + }; + + protected void finishInitialSetup(final Avatar avatar) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + Intent intent; + if (avatar != null) { + intent = new Intent(getApplicationContext(), + StartConversationActivity.class); + } else { + intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra("account", mAccount.getJid()); + intent.putExtra("setup", true); + } + startActivity(intent); + finish(); + } + }); + } + + protected boolean inputDataDiffersFromAccount() { + if (mAccount == null) { + return true; + } else { + return (!mAccount.getJid().equals(mAccountJid.getText().toString())) + || (!mAccount.getPassword().equals( + mPassword.getText().toString()) || mAccount + .isOptionSet(Account.OPTION_REGISTER) != mRegisterNew + .isChecked()); + } + } + + protected void updateSaveButton() { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_CONNECTING) { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + this.mSaveButton.setText(R.string.account_status_connecting); + } else if (mAccount != null + && mAccount.getStatus() == Account.STATUS_DISABLED) { + this.mSaveButton.setEnabled(true); + this.mSaveButton.setTextColor(getPrimaryTextColor()); + this.mSaveButton.setText(R.string.enable); + } else { + this.mSaveButton.setEnabled(true); + this.mSaveButton.setTextColor(getPrimaryTextColor()); + if (jidToEdit != null) { + if (mAccount != null + && mAccount.getStatus() == Account.STATUS_ONLINE) { + this.mSaveButton.setText(R.string.save); + if (!accountInfoEdited()) { + this.mSaveButton.setEnabled(false); + this.mSaveButton.setTextColor(getSecondaryTextColor()); + } + } else { + this.mSaveButton.setText(R.string.connect); + } + } else { + this.mSaveButton.setText(R.string.next); + } + } + } + + protected boolean accountInfoEdited() { + return (!this.mAccount.getJid().equals( + this.mAccountJid.getText().toString())) + || (!this.mAccount.getPassword().equals( + this.mPassword.getText().toString())); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_account); + this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid); + this.mAccountJid.addTextChangedListener(this.mTextWatcher); + this.mPassword = (EditText) findViewById(R.id.account_password); + this.mPassword.addTextChangedListener(this.mTextWatcher); + this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm); + this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new); + this.mStats = (LinearLayout) findViewById(R.id.stats); + this.mSessionEst = (TextView) findViewById(R.id.session_est); + this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons); + this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); + this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); + this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); + this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); + this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mSaveButton = (Button) findViewById(R.id.save_button); + this.mCancelButton = (Button) findViewById(R.id.cancel_button); + this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); + this.mCancelButton.setOnClickListener(this.mCancelButtonClickListener); + this.mRegisterNew + .setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + if (isChecked) { + mPasswordConfirm.setVisibility(View.VISIBLE); + } else { + mPasswordConfirm.setVisibility(View.GONE); + } + updateSaveButton(); + } + }); + } + + @Override + protected void onStart() { + super.onStart(); + if (getIntent() != null) { + this.jidToEdit = getIntent().getStringExtra("jid"); + if (this.jidToEdit != null) { + this.mRegisterNew.setVisibility(View.GONE); + getActionBar().setTitle(jidToEdit); + } else { + getActionBar().setTitle(R.string.action_add_account); + } + } + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnAccountListChangedListener(); + } + super.onStop(); + } + + @Override + protected void onBackendConnected() { + this.mKnownHostsAdapter = new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, + xmppConnectionService.getKnownHosts()); + this.xmppConnectionService + .setOnAccountListChangedListener(this.mOnAccountUpdateListener); + if (this.jidToEdit != null) { + this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); + updateAccountInformation(); + } else if (this.xmppConnectionService.getAccounts().size() == 0) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setDisplayShowHomeEnabled(false); + this.mCancelButton.setEnabled(false); + this.mCancelButton.setTextColor(getSecondaryTextColor()); + } + this.mAccountJid.setAdapter(this.mKnownHostsAdapter); + updateSaveButton(); + } + + private void updateAccountInformation() { + this.mAccountJid.setText(this.mAccount.getJid()); + this.mPassword.setText(this.mAccount.getPassword()); + if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { + this.mRegisterNew.setVisibility(View.VISIBLE); + this.mRegisterNew.setChecked(true); + this.mPasswordConfirm.setText(this.mAccount.getPassword()); + } else { + this.mRegisterNew.setVisibility(View.GONE); + this.mRegisterNew.setChecked(false); + } + if (this.mAccount.getStatus() == Account.STATUS_ONLINE + && !this.mFetchingAvatar) { + this.mStats.setVisibility(View.VISIBLE); + this.mSessionEst.setText(UIHelper.readableTimeDifference( + getApplicationContext(), this.mAccount.getXmppConnection() + .getLastSessionEstablished())); + Features features = this.mAccount.getXmppConnection().getFeatures(); + if (features.carbons()) { + this.mServerInfoCarbons.setText(R.string.server_info_available); + } else { + this.mServerInfoCarbons + .setText(R.string.server_info_unavailable); + } + if (features.sm()) { + this.mServerInfoSm.setText(R.string.server_info_available); + } else { + this.mServerInfoSm.setText(R.string.server_info_unavailable); + } + if (features.pubsub()) { + this.mServerInfoPep.setText(R.string.server_info_available); + } else { + this.mServerInfoPep.setText(R.string.server_info_unavailable); + } + final String fingerprint = this.mAccount + .getOtrFingerprint(xmppConnectionService); + if (fingerprint != null) { + this.mOtrFingerprintBox.setVisibility(View.VISIBLE); + this.mOtrFingerprint.setText(fingerprint); + this.mOtrFingerprintToClipboardButton + .setVisibility(View.VISIBLE); + this.mOtrFingerprintToClipboardButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + + if (copyTextToClipboard(fingerprint,R.string.otr_fingerprint)) { + Toast.makeText( + EditAccountActivity.this, + R.string.toast_message_otr_fingerprint, + Toast.LENGTH_SHORT).show(); + } + } + }); + } else { + this.mOtrFingerprintBox.setVisibility(View.GONE); + } + } else { + if (this.mAccount.errorStatus()) { + this.mAccountJid.setError(getString(this.mAccount + .getReadableStatusId())); + this.mAccountJid.requestFocus(); + } + this.mStats.setVisibility(View.GONE); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java new file mode 100644 index 00000000..f8302050 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java @@ -0,0 +1,39 @@ +package eu.siacs.conversations.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.EditText; + +public class EditMessage extends EditText { + + public EditMessage(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public EditMessage(Context context) { + super(context); + } + + protected OnEnterPressed mOnEnterPressed; + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + if (mOnEnterPressed != null) { + mOnEnterPressed.onEnterPressed(); + } + return true; + } + return super.onKeyDown(keyCode, event); + } + + public void setOnEnterPressedListener(OnEnterPressed listener) { + this.mOnEnterPressed = listener; + } + + public interface OnEnterPressed { + public void onEnterPressed(); + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java new file mode 100644 index 00000000..77f8b68a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -0,0 +1,218 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; +import eu.siacs.conversations.ui.adapter.AccountAdapter; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; + +public class ManageAccountActivity extends XmppActivity { + + protected Account selectedAccount = null; + + protected List<Account> accountList = new ArrayList<Account>(); + protected ListView accountListView; + protected AccountAdapter mAccountAdapter; + protected OnAccountUpdate accountChanged = new OnAccountUpdate() { + + @Override + public void onAccountUpdate() { + accountList.clear(); + accountList.addAll(xmppConnectionService.getAccounts()); + runOnUiThread(new Runnable() { + + @Override + public void run() { + mAccountAdapter.notifyDataSetChanged(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + setContentView(R.layout.manage_accounts); + + accountListView = (ListView) findViewById(R.id.account_list); + this.mAccountAdapter = new AccountAdapter(this, accountList); + accountListView.setAdapter(this.mAccountAdapter); + accountListView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView<?> arg0, View view, + int position, long arg3) { + switchToAccount(accountList.get(position)); + } + }); + registerForContextMenu(accountListView); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + ManageAccountActivity.this.getMenuInflater().inflate( + R.menu.manageaccounts_context, menu); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + this.selectedAccount = accountList.get(acmi.position); + if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) { + menu.findItem(R.id.mgmt_account_disable).setVisible(false); + menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false); + menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false); + } else { + menu.findItem(R.id.mgmt_account_enable).setVisible(false); + } + menu.setHeaderTitle(this.selectedAccount.getJid()); + } + + @Override + protected void onStop() { + if (xmppConnectionServiceBound) { + xmppConnectionService.removeOnAccountListChangedListener(); + } + super.onStop(); + } + + @Override + void onBackendConnected() { + xmppConnectionService.setOnAccountListChangedListener(accountChanged); + this.accountList.clear(); + this.accountList.addAll(xmppConnectionService.getAccounts()); + mAccountAdapter.notifyDataSetChanged(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.manageaccounts, menu); + return true; + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.mgmt_account_publish_avatar: + publishAvatar(selectedAccount); + return true; + case R.id.mgmt_account_disable: + disableAccount(selectedAccount); + return true; + case R.id.mgmt_account_enable: + enableAccount(selectedAccount); + return true; + case R.id.mgmt_account_delete: + deleteAccount(selectedAccount); + return true; + case R.id.mgmt_account_announce_pgp: + publishOpenPGPPublicKey(selectedAccount); + return true; + default: + return super.onContextItemSelected(item); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_add_account: + startActivity(new Intent(getApplicationContext(), + EditAccountActivity.class)); + break; + default: + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onNavigateUp() { + if (xmppConnectionService.getConversations().size() == 0) { + Intent contactsIntent = new Intent(this, + StartConversationActivity.class); + contactsIntent.setFlags( + // if activity exists in stack, pop the stack and go back to it + Intent.FLAG_ACTIVITY_CLEAR_TOP | + // otherwise, make a new task for it + Intent.FLAG_ACTIVITY_NEW_TASK | + // don't use the new activity animation; finish + // animation runs instead + Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(contactsIntent); + finish(); + return true; + } else { + return super.onNavigateUp(); + } + } + + private void publishAvatar(Account account) { + Intent intent = new Intent(getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra("account", account.getJid()); + startActivity(intent); + } + + private void disableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, true); + xmppConnectionService.updateAccount(account); + } + + private void enableAccount(Account account) { + account.setOption(Account.OPTION_DISABLED, false); + xmppConnectionService.updateAccount(account); + } + + private void publishOpenPGPPublicKey(Account account) { + if (ManageAccountActivity.this.hasPgp()) { + announcePgp(account, null); + } else { + this.showInstallPgpDialog(); + } + } + + private void deleteAccount(final Account account) { + AlertDialog.Builder builder = new AlertDialog.Builder( + ManageAccountActivity.this); + builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); + builder.setPositiveButton(getString(R.string.delete), + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteAccount(account); + selectedAccount = null; + } + }); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.create().show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_ANNOUNCE_PGP) { + announcePgp(selectedAccount, null); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java new file mode 100644 index 00000000..6aa40c41 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -0,0 +1,242 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.xmpp.pep.Avatar; + +public class PublishProfilePictureActivity extends XmppActivity { + + private static final int REQUEST_CHOOSE_FILE = 0xac23; + + private ImageView avatar; + private TextView accountTextView; + private TextView hintOrWarning; + private TextView secondaryHint; + private Button cancelButton; + private Button publishButton; + + private Uri avatarUri; + private Uri defaultUri; + + private Account account; + + private boolean support = false; + + private boolean mInitialAccountSetup; + + private UiCallback<Avatar> avatarPublication = new UiCallback<Avatar>() { + + @Override + public void success(Avatar object) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mInitialAccountSetup) { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + } + finish(); + } + }); + } + + @Override + public void error(final int errorCode, Avatar object) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + hintOrWarning.setText(errorCode); + hintOrWarning.setTextColor(getWarningTextColor()); + publishButton.setText(R.string.publish); + enablePublishButton(); + } + }); + + } + + @Override + public void userInputRequried(PendingIntent pi, Avatar object) { + } + }; + + private OnLongClickListener backToDefaultListener = new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + avatarUri = defaultUri; + loadImageIntoPreview(defaultUri); + return true; + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_publish_profile_picture); + this.avatar = (ImageView) findViewById(R.id.account_image); + this.cancelButton = (Button) findViewById(R.id.cancel_button); + this.publishButton = (Button) findViewById(R.id.publish_button); + this.accountTextView = (TextView) findViewById(R.id.account); + this.hintOrWarning = (TextView) findViewById(R.id.hint_or_warning); + this.secondaryHint = (TextView) findViewById(R.id.secondary_hint); + this.publishButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (avatarUri != null) { + publishButton.setText(R.string.publishing); + disablePublishButton(); + xmppConnectionService.publishAvatar(account, avatarUri, + avatarPublication); + } + } + }); + this.cancelButton.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mInitialAccountSetup) { + startActivity(new Intent(getApplicationContext(), + StartConversationActivity.class)); + } + finish(); + } + }); + this.avatar.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent attachFileIntent = new Intent(); + attachFileIntent.setType("image/*"); + attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); + Intent chooser = Intent.createChooser(attachFileIntent, + getString(R.string.attach_file)); + startActivityForResult(chooser, REQUEST_CHOOSE_FILE); + } + }); + this.defaultUri = PhoneHelper.getSefliUri(getApplicationContext()); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + if (requestCode == REQUEST_CHOOSE_FILE) { + this.avatarUri = data.getData(); + if (xmppConnectionServiceBound) { + loadImageIntoPreview(this.avatarUri); + } + } + } + } + + @Override + protected void onBackendConnected() { + if (getIntent() != null) { + String jid = getIntent().getStringExtra("account"); + if (jid != null) { + this.account = xmppConnectionService.findAccountByJid(jid); + if (this.account.getXmppConnection() != null) { + this.support = this.account.getXmppConnection() + .getFeatures().pubsub(); + } + if (this.avatarUri == null) { + if (this.account.getAvatar() != null + || this.defaultUri == null) { + this.avatar.setImageBitmap(avatarService().get(account, + getPixel(194))); + if (this.defaultUri != null) { + this.avatar + .setOnLongClickListener(this.backToDefaultListener); + } else { + this.secondaryHint.setVisibility(View.INVISIBLE); + } + if (!support) { + this.hintOrWarning + .setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_no_server_support); + } + } else { + this.avatarUri = this.defaultUri; + loadImageIntoPreview(this.defaultUri); + this.secondaryHint.setVisibility(View.INVISIBLE); + } + } else { + loadImageIntoPreview(avatarUri); + } + this.accountTextView.setText(this.account.getJid()); + } + } + + } + + @Override + protected void onStart() { + super.onStart(); + if (getIntent() != null) { + this.mInitialAccountSetup = getIntent().getBooleanExtra("setup", + false); + } + if (this.mInitialAccountSetup) { + this.cancelButton.setText(R.string.skip); + } + } + + protected void loadImageIntoPreview(Uri uri) { + Bitmap bm = xmppConnectionService.getFileBackend().cropCenterSquare( + uri, 384); + if (bm == null) { + disablePublishButton(); + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_converting); + return; + } + this.avatar.setImageBitmap(bm); + if (support) { + enablePublishButton(); + this.publishButton.setText(R.string.publish); + this.hintOrWarning.setText(R.string.publish_avatar_explanation); + this.hintOrWarning.setTextColor(getPrimaryTextColor()); + } else { + disablePublishButton(); + this.hintOrWarning.setTextColor(getWarningTextColor()); + this.hintOrWarning + .setText(R.string.error_publish_avatar_no_server_support); + } + if (this.defaultUri != null && uri.equals(this.defaultUri)) { + this.secondaryHint.setVisibility(View.INVISIBLE); + this.avatar.setOnLongClickListener(null); + } else if (this.defaultUri != null) { + this.secondaryHint.setVisibility(View.VISIBLE); + this.avatar.setOnLongClickListener(this.backToDefaultListener); + } + } + + protected void enablePublishButton() { + this.publishButton.setEnabled(true); + this.publishButton.setTextColor(getPrimaryTextColor()); + } + + protected void disablePublishButton() { + this.publishButton.setEnabled(false); + this.publishButton.setTextColor(getSecondaryTextColor()); + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java new file mode 100644 index 00000000..fc6308fc --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -0,0 +1,74 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +import eu.siacs.conversations.entities.Account; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.os.Build; +import android.os.Bundle; +import android.preference.ListPreference; +import android.preference.PreferenceManager; + +public class SettingsActivity extends XmppActivity implements + OnSharedPreferenceChangeListener { + private SettingsFragment mSettingsFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSettingsFragment = new SettingsFragment(); + getFragmentManager().beginTransaction() + .replace(android.R.id.content, mSettingsFragment).commit(); + } + + @Override + void onBackendConnected() { + + } + + @Override + public void onStart() { + super.onStart(); + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(this); + ListPreference resources = (ListPreference) mSettingsFragment + .findPreference("resource"); + if (resources != null) { + ArrayList<CharSequence> entries = new ArrayList<CharSequence>( + Arrays.asList(resources.getEntries())); + entries.add(0, Build.MODEL); + resources.setEntries(entries.toArray(new CharSequence[entries + .size()])); + resources.setEntryValues(entries.toArray(new CharSequence[entries + .size()])); + } + } + + @Override + public void onStop() { + super.onStop(); + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences preferences, + String name) { + if (name.equals("resource")) { + String resource = preferences.getString("resource", "mobile") + .toLowerCase(Locale.US); + if (xmppConnectionServiceBound) { + for (Account account : xmppConnectionService.getAccounts()) { + account.setResource(resource); + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + xmppConnectionService.reconnectAccount(account, false); + } + } + } + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java new file mode 100644 index 00000000..7e1c3698 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/SettingsFragment.java @@ -0,0 +1,15 @@ +package eu.siacs.conversations.ui; + +import eu.siacs.conversations.R; +import android.os.Bundle; +import android.preference.PreferenceFragment; + +public class SettingsFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java new file mode 100644 index 00000000..9fbc3db1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -0,0 +1,185 @@ +package eu.siacs.conversations.ui; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.adapter.ConversationAdapter; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.Toast; + +public class ShareWithActivity extends XmppActivity { + + private class Share { + public Uri uri; + public String account; + public String contact; + public String text; + } + + private Share share; + + private static final int REQUEST_START_NEW_CONVERSATION = 0x0501; + private ListView mListView; + private List<Conversation> mConversations = new ArrayList<Conversation>(); + + private UiCallback<Message> attachImageCallback = new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, Message object) { + // TODO Auto-generated method stub + + } + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message object) { + // TODO Auto-generated method stub + + } + }; + + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_START_NEW_CONVERSATION + && resultCode == RESULT_OK) { + share.contact = data.getStringExtra("contact"); + share.account = data.getStringExtra("account"); + Log.d(Config.LOGTAG, "contact: " + share.contact + " account:" + + share.account); + } + if (xmppConnectionServiceBound && share != null + && share.contact != null && share.account != null) { + share(); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + + setContentView(R.layout.share_with); + setTitle(getString(R.string.title_activity_sharewith)); + + mListView = (ListView) findViewById(R.id.choose_conversation_list); + ConversationAdapter mAdapter = new ConversationAdapter(this, + this.mConversations); + mListView.setAdapter(mAdapter); + mListView.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView<?> arg0, View arg1, + int position, long arg3) { + Conversation conversation = mConversations.get(position); + if (conversation.getMode() == Conversation.MODE_SINGLE + || share.uri == null) { + share(mConversations.get(position)); + } + } + }); + + this.share = new Share(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.share_with, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_add: + Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onStart() { + if (getIntent().getType() != null + && getIntent().getType().startsWith("image/")) { + this.share.uri = (Uri) getIntent().getParcelableExtra( + Intent.EXTRA_STREAM); + } else { + this.share.text = getIntent().getStringExtra(Intent.EXTRA_TEXT); + } + if (xmppConnectionServiceBound) { + xmppConnectionService.populateWithOrderedConversations( + mConversations, this.share.uri == null); + } + super.onStart(); + } + + @Override + void onBackendConnected() { + if (xmppConnectionServiceBound && share != null + && share.contact != null && share.account != null) { + share(); + return; + } + xmppConnectionService.populateWithOrderedConversations(mConversations, + this.share != null && this.share.uri == null); + } + + private void share() { + Account account = xmppConnectionService.findAccountByJid(share.account); + if (account == null) { + return; + } + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, share.contact, false); + share(conversation); + } + + private void share(final Conversation conversation) { + if (share.uri != null) { + selectPresence(conversation, new OnPresenceSelected() { + @Override + public void onPresenceSelected() { + Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), + Toast.LENGTH_LONG).show(); + ShareWithActivity.this.xmppConnectionService + .attachImageToConversation(conversation, share.uri, + attachImageCallback); + switchToConversation(conversation, null, true); + finish(); + } + }); + + } else { + switchToConversation(conversation, this.share.text, true); + finish(); + } + + } + +}
\ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java new file mode 100644 index 00000000..416e926a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -0,0 +1,677 @@ +package eu.siacs.conversations.ui; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import android.annotation.SuppressLint; +import android.app.ActionBar; +import android.app.ActionBar.Tab; +import android.app.ActionBar.TabListener; +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.app.ListFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Spinner; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Bookmark; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; +import eu.siacs.conversations.ui.adapter.ListItemAdapter; +import eu.siacs.conversations.utils.Validator; + +public class StartConversationActivity extends XmppActivity { + + private Tab mContactsTab; + private Tab mConferencesTab; + private ViewPager mViewPager; + + private MyListFragment mContactsListFragment = new MyListFragment(); + private List<ListItem> contacts = new ArrayList<ListItem>(); + private ArrayAdapter<ListItem> mContactsAdapter; + + private MyListFragment mConferenceListFragment = new MyListFragment(); + private List<ListItem> conferences = new ArrayList<ListItem>(); + private ArrayAdapter<ListItem> mConferenceAdapter; + + private List<String> mActivatedAccounts = new ArrayList<String>(); + private List<String> mKnownHosts; + private List<String> mKnownConferenceHosts; + + private Menu mOptionsMenu; + private EditText mSearchEditText; + + public int conference_context_id; + public int contact_context_id; + + private TabListener mTabListener = new TabListener() { + + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + return; + } + + @Override + public void onTabSelected(Tab tab, FragmentTransaction ft) { + mViewPager.setCurrentItem(tab.getPosition()); + onTabChanged(); + } + + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) { + return; + } + }; + + private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + getActionBar().setSelectedNavigationItem(position); + onTabChanged(); + } + }; + + private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + mSearchEditText.post(new Runnable() { + + @Override + public void run() { + mSearchEditText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, + InputMethodManager.SHOW_IMPLICIT); + } + }); + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + mSearchEditText.setText(""); + filter(null); + return true; + } + }; + private TextWatcher mSearchTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(Editable editable) { + filter(editable.toString()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + } + }; + private OnRosterUpdate onRosterUpdate = new OnRosterUpdate() { + + @Override + public void onRosterUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } + } + }); + } + }; + private MenuItem mMenuSearchView; + private String mInitialJid; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_start_conversation); + mViewPager = (ViewPager) findViewById(R.id.start_conversation_view_pager); + ActionBar actionBar = getActionBar(); + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + + mContactsTab = actionBar.newTab().setText(R.string.contacts) + .setTabListener(mTabListener); + mConferencesTab = actionBar.newTab().setText(R.string.conferences) + .setTabListener(mTabListener); + actionBar.addTab(mContactsTab); + actionBar.addTab(mConferencesTab); + + mViewPager.setOnPageChangeListener(mOnPageChangeListener); + mViewPager.setAdapter(new FragmentPagerAdapter(getFragmentManager()) { + + @Override + public int getCount() { + return 2; + } + + @Override + public Fragment getItem(int position) { + if (position == 0) { + return mContactsListFragment; + } else { + return mConferenceListFragment; + } + } + }); + + mConferenceAdapter = new ListItemAdapter(this, conferences); + mConferenceListFragment.setListAdapter(mConferenceAdapter); + mConferenceListFragment.setContextMenu(R.menu.conference_context); + mConferenceListFragment + .setOnListItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView<?> arg0, View arg1, + int position, long arg3) { + openConversationForBookmark(position); + } + }); + + mContactsAdapter = new ListItemAdapter(this, contacts); + mContactsListFragment.setListAdapter(mContactsAdapter); + mContactsListFragment.setContextMenu(R.menu.contact_context); + mContactsListFragment + .setOnListItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView<?> arg0, View arg1, + int position, long arg3) { + openConversationForContact(position); + } + }); + + } + + @Override + public void onStop() { + super.onStop(); + xmppConnectionService.removeOnRosterUpdateListener(); + } + + protected void openConversationForContact(int position) { + Contact contact = (Contact) contacts.get(position); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(contact.getAccount(), + contact.getJid(), false); + switchToConversation(conversation); + } + + protected void openConversationForContact() { + int position = contact_context_id; + openConversationForContact(position); + } + + protected void openConversationForBookmark() { + openConversationForBookmark(conference_context_id); + } + + protected void openConversationForBookmark(int position) { + Bookmark bookmark = (Bookmark) conferences.get(position); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(bookmark.getAccount(), + bookmark.getJid(), true); + conversation.setBookmark(bookmark); + if (!conversation.getMucOptions().online()) { + xmppConnectionService.joinMuc(conversation); + } + if (!bookmark.autojoin()) { + bookmark.setAutojoin(true); + xmppConnectionService.pushBookmarks(bookmark.getAccount()); + } + switchToConversation(conversation); + } + + protected void openDetailsForContact() { + int position = contact_context_id; + Contact contact = (Contact) contacts.get(position); + switchToContactDetails(contact); + } + + protected void deleteContact() { + int position = contact_context_id; + final Contact contact = (Contact) contacts.get(position); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(R.string.cancel, null); + builder.setTitle(R.string.action_delete_contact); + builder.setMessage(getString(R.string.remove_contact_text, + contact.getJid())); + builder.setPositiveButton(R.string.delete, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteContactOnServer(contact); + filter(mSearchEditText.getText().toString()); + } + }); + builder.create().show(); + + } + + protected void deleteConference() { + int position = conference_context_id; + final Bookmark bookmark = (Bookmark) conferences.get(position); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setNegativeButton(R.string.cancel, null); + builder.setTitle(R.string.delete_bookmark); + builder.setMessage(getString(R.string.remove_bookmark_text, + bookmark.getJid())); + builder.setPositiveButton(R.string.delete, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + bookmark.unregisterConversation(); + Account account = bookmark.getAccount(); + account.getBookmarks().remove(bookmark); + xmppConnectionService.pushBookmarks(account); + filter(mSearchEditText.getText().toString()); + } + }); + builder.create().show(); + + } + + @SuppressLint("InflateParams") + protected void showCreateContactDialog(String prefilledJid) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.create_contact); + View dialogView = getLayoutInflater().inflate( + R.layout.create_contact_dialog, null); + final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); + final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView + .findViewById(R.id.jid); + jid.setAdapter(new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, mKnownHosts)); + if (prefilledJid != null) { + jid.append(prefilledJid); + } + populateAccountSpinner(spinner); + builder.setView(dialogView); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.create, null); + final AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener( + new View.OnClickListener() { + + @Override + public void onClick(View v) { + if (!xmppConnectionServiceBound) { + return; + } + if (Validator.isValidJid(jid.getText().toString())) { + String accountJid = (String) spinner + .getSelectedItem(); + String contactJid = jid.getText().toString(); + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + Contact contact = account.getRoster().getContact( + contactJid); + if (contact.showInRoster()) { + jid.setError(getString(R.string.contact_already_exists)); + } else { + xmppConnectionService.createContact(contact); + dialog.dismiss(); + switchToConversation(contact); + } + } else { + jid.setError(getString(R.string.invalid_jid)); + } + } + }); + + } + + @SuppressLint("InflateParams") + protected void showJoinConferenceDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.join_conference); + View dialogView = getLayoutInflater().inflate( + R.layout.join_conference_dialog, null); + final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); + final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView + .findViewById(R.id.jid); + jid.setAdapter(new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, mKnownConferenceHosts)); + populateAccountSpinner(spinner); + final CheckBox bookmarkCheckBox = (CheckBox) dialogView + .findViewById(R.id.bookmark); + builder.setView(dialogView); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.join, null); + final AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener( + new View.OnClickListener() { + + @Override + public void onClick(View v) { + if (!xmppConnectionServiceBound) { + return; + } + if (Validator.isValidJid(jid.getText().toString())) { + String accountJid = (String) spinner + .getSelectedItem(); + String conferenceJid = jid.getText().toString(); + Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + if (bookmarkCheckBox.isChecked()) { + if (account.hasBookmarkFor(conferenceJid)) { + jid.setError(getString(R.string.bookmark_already_exists)); + } else { + Bookmark bookmark = new Bookmark(account, + conferenceJid); + bookmark.setAutojoin(true); + account.getBookmarks().add(bookmark); + xmppConnectionService + .pushBookmarks(account); + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + conversation.setBookmark(bookmark); + if (!conversation.getMucOptions().online()) { + xmppConnectionService + .joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); + } + } else { + Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + if (!conversation.getMucOptions().online()) { + xmppConnectionService.joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); + } + } else { + jid.setError(getString(R.string.invalid_jid)); + } + } + }); + } + + protected void switchToConversation(Contact contact) { + Conversation conversation = xmppConnectionService + .findOrCreateConversation(contact.getAccount(), + contact.getJid(), false); + switchToConversation(conversation); + } + + private void populateAccountSpinner(Spinner spinner) { + ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, + android.R.layout.simple_spinner_item, mActivatedAccounts); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + this.mOptionsMenu = menu; + getMenuInflater().inflate(R.menu.start_conversation, menu); + MenuItem menuCreateContact = menu + .findItem(R.id.action_create_contact); + MenuItem menuCreateConference = menu + .findItem(R.id.action_join_conference); + mMenuSearchView = menu.findItem(R.id.action_search); + mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener); + View mSearchView = mMenuSearchView.getActionView(); + mSearchEditText = (EditText) mSearchView + .findViewById(R.id.search_field); + mSearchEditText.addTextChangedListener(mSearchTextWatcher); + if (getActionBar().getSelectedNavigationIndex() == 0) { + menuCreateConference.setVisible(false); + } else { + menuCreateContact.setVisible(false); + } + if (mInitialJid != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.append(mInitialJid); + filter(mInitialJid); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_create_contact: + showCreateContactDialog(null); + break; + case R.id.action_join_conference: + showJoinConferenceDialog(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) { + mOptionsMenu.findItem(R.id.action_search).expandActionView(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onBackendConnected() { + xmppConnectionService.setOnRosterUpdateListener(this.onRosterUpdate); + this.mActivatedAccounts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + this.mActivatedAccounts.add(account.getJid()); + } + } + this.mKnownHosts = xmppConnectionService.getKnownHosts(); + this.mKnownConferenceHosts = xmppConnectionService + .getKnownConferenceHosts(); + if (!startByIntent()) { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } else { + filter(null); + } + } + } + + protected boolean startByIntent() { + if (getIntent() != null + && Intent.ACTION_SENDTO.equals(getIntent().getAction())) { + try { + String jid = URLDecoder.decode( + getIntent().getData().getEncodedPath(), "UTF-8").split( + "/")[1]; + setIntent(null); + return handleJid(jid); + } catch (UnsupportedEncodingException e) { + setIntent(null); + return false; + } + } else if (getIntent() != null + && Intent.ACTION_VIEW.equals(getIntent().getAction())) { + Uri uri = getIntent().getData(); + String jid = uri.getSchemeSpecificPart().split("\\?")[0]; + return handleJid(jid); + } + return false; + } + + private boolean handleJid(String jid) { + List<Contact> contacts = xmppConnectionService.findContacts(jid); + if (contacts.size() == 0) { + showCreateContactDialog(jid); + return false; + } else if (contacts.size() == 1) { + switchToConversation(contacts.get(0)); + return true; + } else { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(jid); + filter(jid); + } else { + mInitialJid = jid; + } + return true; + } + } + + protected void filter(String needle) { + if (xmppConnectionServiceBound) { + this.filterContacts(needle); + this.filterConferences(needle); + } + } + + protected void filterContacts(String needle) { + this.contacts.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Contact contact : account.getRoster().getContacts()) { + if (contact.showInRoster() && contact.match(needle)) { + this.contacts.add(contact); + } + } + } + } + Collections.sort(this.contacts); + mContactsAdapter.notifyDataSetChanged(); + } + + protected void filterConferences(String needle) { + this.conferences.clear(); + for (Account account : xmppConnectionService.getAccounts()) { + if (account.getStatus() != Account.STATUS_DISABLED) { + for (Bookmark bookmark : account.getBookmarks()) { + if (bookmark.match(needle)) { + this.conferences.add(bookmark); + } + } + } + } + Collections.sort(this.conferences); + mConferenceAdapter.notifyDataSetChanged(); + } + + private void onTabChanged() { + invalidateOptionsMenu(); + } + + public static class MyListFragment extends ListFragment { + private AdapterView.OnItemClickListener mOnItemClickListener; + private int mResContextMenu; + + public void setContextMenu(int res) { + this.mResContextMenu = res; + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + if (mOnItemClickListener != null) { + mOnItemClickListener.onItemClick(l, v, position, id); + } + } + + public void setOnListItemClickListener(AdapterView.OnItemClickListener l) { + this.mOnItemClickListener = l; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + registerForContextMenu(getListView()); + getListView().setFastScrollEnabled(true); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + StartConversationActivity activity = (StartConversationActivity) getActivity(); + activity.getMenuInflater().inflate(mResContextMenu, menu); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + if (mResContextMenu == R.menu.conference_context) { + activity.conference_context_id = acmi.position; + } else { + activity.contact_context_id = acmi.position; + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + StartConversationActivity activity = (StartConversationActivity) getActivity(); + switch (item.getItemId()) { + case R.id.context_start_conversation: + activity.openConversationForContact(); + break; + case R.id.context_contact_details: + activity.openDetailsForContact(); + break; + case R.id.context_delete_contact: + activity.deleteContact(); + break; + case R.id.context_join_conference: + activity.openConversationForBookmark(); + break; + case R.id.context_delete_conference: + activity.deleteConference(); + } + return true; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/UiCallback.java b/src/main/java/eu/siacs/conversations/ui/UiCallback.java new file mode 100644 index 00000000..c80199e1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/UiCallback.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.ui; + +import android.app.PendingIntent; + +public interface UiCallback<T> { + public void success(T object); + + public void error(int errorCode, T object); + + public void userInputRequried(PendingIntent pi, T object); +} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java new file mode 100644 index 00000000..222f3295 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -0,0 +1,648 @@ +package eu.siacs.conversations.ui; + +import java.io.FileNotFoundException; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.AvatarService; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; +import eu.siacs.conversations.utils.ExceptionHelper; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.AlertDialog.Builder; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.DialogInterface.OnClickListener; +import android.content.IntentSender.SendIntentException; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.text.InputType; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ImageView; + +public abstract class XmppActivity extends Activity { + + protected static final int REQUEST_ANNOUNCE_PGP = 0x0101; + protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102; + + public XmppConnectionService xmppConnectionService; + public boolean xmppConnectionServiceBound = false; + + protected int mPrimaryTextColor; + protected int mSecondaryTextColor; + protected int mSecondaryBackgroundColor; + protected int mColorRed; + protected int mColorOrange; + protected int mColorGreen; + protected int mPrimaryColor; + + protected boolean mUseSubject = true; + + private DisplayMetrics metrics; + + protected interface OnValueEdited { + public void onValueEdited(String value); + } + + public interface OnPresenceSelected { + public void onPresenceSelected(); + } + + protected ServiceConnection mConnection = new ServiceConnection() { + + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + XmppConnectionBinder binder = (XmppConnectionBinder) service; + xmppConnectionService = binder.getService(); + xmppConnectionServiceBound = true; + onBackendConnected(); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + xmppConnectionServiceBound = false; + } + }; + + @Override + protected void onStart() { + super.onStart(); + if (!xmppConnectionServiceBound) { + connectToBackend(); + } + } + + public void connectToBackend() { + Intent intent = new Intent(this, XmppConnectionService.class); + intent.setAction("ui"); + startService(intent); + bindService(intent, mConnection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onStop() { + super.onStop(); + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + } + + protected void hideKeyboard() { + InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + + View focus = getCurrentFocus(); + + if (focus != null) { + + inputManager.hideSoftInputFromWindow(focus.getWindowToken(), + InputMethodManager.HIDE_NOT_ALWAYS); + } + } + + public boolean hasPgp() { + return xmppConnectionService.getPgpEngine() != null; + } + + public void showInstallPgpDialog() { + Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.openkeychain_required)); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage(getText(R.string.openkeychain_required_long)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setNeutralButton(getString(R.string.restart), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (xmppConnectionServiceBound) { + unbindService(mConnection); + xmppConnectionServiceBound = false; + } + stopService(new Intent(XmppActivity.this, + XmppConnectionService.class)); + finish(); + } + }); + builder.setPositiveButton(getString(R.string.install), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + Uri uri = Uri + .parse("market://details?id=org.sufficientlysecure.keychain"); + Intent marketIntent = new Intent(Intent.ACTION_VIEW, + uri); + PackageManager manager = getApplicationContext() + .getPackageManager(); + List<ResolveInfo> infos = manager + .queryIntentActivities(marketIntent, 0); + if (infos.size() > 0) { + startActivity(marketIntent); + } else { + uri = Uri.parse("http://www.openkeychain.org/"); + Intent browserIntent = new Intent( + Intent.ACTION_VIEW, uri); + startActivity(browserIntent); + } + finish(); + } + }); + builder.create().show(); + } + + abstract void onBackendConnected(); + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_settings: + startActivity(new Intent(this, SettingsActivity.class)); + break; + case R.id.action_accounts: + startActivity(new Intent(this, ManageAccountActivity.class)); + break; + case android.R.id.home: + finish(); + break; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + metrics = getResources().getDisplayMetrics(); + ExceptionHelper.init(getApplicationContext()); + mPrimaryTextColor = getResources().getColor(R.color.primarytext); + mSecondaryTextColor = getResources().getColor(R.color.secondarytext); + mColorRed = getResources().getColor(R.color.red); + mColorOrange = getResources().getColor(R.color.orange); + mColorGreen = getResources().getColor(R.color.green); + mPrimaryColor = getResources().getColor(R.color.primary); + mSecondaryBackgroundColor = getResources().getColor( + R.color.secondarybackground); + if (getPreferences().getBoolean("use_larger_font", false)) { + setTheme(R.style.ConversationsTheme_LargerText); + } + mUseSubject = getPreferences().getBoolean("use_subject", true); + } + + protected SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + } + + public boolean useSubjectToIdentifyConference() { + return mUseSubject; + } + + public void switchToConversation(Conversation conversation) { + switchToConversation(conversation, null, false); + } + + public void switchToConversation(Conversation conversation, String text, + boolean newTask) { + Intent viewConversationIntent = new Intent(this, + ConversationActivity.class); + viewConversationIntent.setAction(Intent.ACTION_VIEW); + viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, + conversation.getUuid()); + if (text != null) { + viewConversationIntent.putExtra(ConversationActivity.TEXT, text); + } + viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); + if (newTask) { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + } else { + viewConversationIntent.setFlags(viewConversationIntent.getFlags() + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + } + startActivity(viewConversationIntent); + finish(); + } + + public void switchToContactDetails(Contact contact) { + Intent intent = new Intent(this, ContactDetailsActivity.class); + intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT); + intent.putExtra("account", contact.getAccount().getJid()); + intent.putExtra("contact", contact.getJid()); + startActivity(intent); + } + + public void switchToAccount(Account account) { + Intent intent = new Intent(this, EditAccountActivity.class); + intent.putExtra("jid", account.getJid()); + startActivity(intent); + } + + protected void inviteToConversation(Conversation conversation) { + Intent intent = new Intent(getApplicationContext(), + ChooseContactActivity.class); + intent.putExtra("conversation", conversation.getUuid()); + startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION); + } + + protected void announcePgp(Account account, final Conversation conversation) { + xmppConnectionService.getPgpEngine().generateSignature(account, + "online", new UiCallback<Account>() { + + @Override + public void userInputRequried(PendingIntent pi, + Account account) { + try { + startIntentSenderForResult(pi.getIntentSender(), + REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); + } catch (SendIntentException e) { + } + } + + @Override + public void success(Account account) { + xmppConnectionService.databaseBackend + .updateAccount(account); + xmppConnectionService.sendPresencePacket(account, + xmppConnectionService.getPresenceGenerator() + .sendPresence(account)); + if (conversation != null) { + conversation + .setNextEncryption(Message.ENCRYPTION_PGP); + xmppConnectionService.databaseBackend + .updateConversation(conversation); + } + } + + @Override + public void error(int error, Account account) { + displayErrorDialog(error); + } + }); + } + + protected void displayErrorDialog(final int errorCode) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + AlertDialog.Builder builder = new AlertDialog.Builder( + XmppActivity.this); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setTitle(getString(R.string.error)); + builder.setMessage(errorCode); + builder.setNeutralButton(R.string.accept, null); + builder.create().show(); + } + }); + + } + + protected void showAddToRosterDialog(final Conversation conversation) { + String jid = conversation.getContactJid(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(jid); + builder.setMessage(getString(R.string.not_in_roster)); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add_contact), + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String jid = conversation.getContactJid(); + Account account = conversation.getAccount(); + Contact contact = account.getRoster().getContact(jid); + xmppConnectionService.createContact(contact); + switchToContactDetails(contact); + } + }); + builder.create().show(); + } + + private void showAskForPresenceDialog(final Contact contact) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(contact.getJid()); + builder.setMessage(R.string.request_presence_updates); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.request_now, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + if (xmppConnectionServiceBound) { + xmppConnectionService.sendPresencePacket(contact + .getAccount(), xmppConnectionService + .getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); + } + } + }); + builder.create().show(); + } + + private void warnMutalPresenceSubscription(final Conversation conversation, + final OnPresenceSelected listener) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(conversation.getContact().getJid()); + builder.setMessage(R.string.without_mutual_presence_updates); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ignore, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.setNextPresence(null); + if (listener != null) { + listener.onPresenceSelected(); + } + } + }); + builder.create().show(); + } + + protected void quickEdit(String previousValue, OnValueEdited callback) { + quickEdit(previousValue, callback, false); + } + + protected void quickPasswordEdit(String previousValue, + OnValueEdited callback) { + quickEdit(previousValue, callback, true); + } + + @SuppressLint("InflateParams") + private void quickEdit(final String previousValue, + final OnValueEdited callback, boolean password) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = getLayoutInflater().inflate(R.layout.quickedit, null); + final EditText editor = (EditText) view.findViewById(R.id.editor); + OnClickListener mClickListener = new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + String value = editor.getText().toString(); + if (!previousValue.equals(value) && value.trim().length() > 0) { + callback.onValueEdited(value); + } + } + }; + if (password) { + editor.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_PASSWORD); + editor.setHint(R.string.password); + builder.setPositiveButton(R.string.accept, mClickListener); + } else { + builder.setPositiveButton(R.string.edit, mClickListener); + } + editor.requestFocus(); + editor.setText(previousValue); + builder.setView(view); + builder.setNegativeButton(R.string.cancel, null); + builder.create().show(); + } + + public void selectPresence(final Conversation conversation, + final OnPresenceSelected listener) { + Contact contact = conversation.getContact(); + if (!contact.showInRoster()) { + showAddToRosterDialog(conversation); + } else { + Presences presences = contact.getPresences(); + if (presences.size() == 0) { + if (!contact.getOption(Contact.Options.TO) + && !contact.getOption(Contact.Options.ASKING) + && contact.getAccount().getStatus() == Account.STATUS_ONLINE) { + showAskForPresenceDialog(contact); + } else if (!contact.getOption(Contact.Options.TO) + || !contact.getOption(Contact.Options.FROM)) { + warnMutalPresenceSubscription(conversation, listener); + } else { + conversation.setNextPresence(null); + listener.onPresenceSelected(); + } + } else if (presences.size() == 1) { + String presence = presences.asStringArray()[0]; + conversation.setNextPresence(presence); + listener.onPresenceSelected(); + } else { + final StringBuilder presence = new StringBuilder(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.choose_presence)); + final String[] presencesArray = presences.asStringArray(); + int preselectedPresence = 0; + for (int i = 0; i < presencesArray.length; ++i) { + if (presencesArray[i].equals(contact.lastseen.presence)) { + preselectedPresence = i; + break; + } + } + presence.append(presencesArray[preselectedPresence]); + builder.setSingleChoiceItems(presencesArray, + preselectedPresence, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, + int which) { + presence.delete(0, presence.length()); + presence.append(presencesArray[which]); + } + }); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton(R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + conversation.setNextPresence(presence.toString()); + listener.onPresenceSelected(); + } + }); + builder.create().show(); + } + } + } + + protected void onActivityResult(int requestCode, int resultCode, + final Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_INVITE_TO_CONVERSATION + && resultCode == RESULT_OK) { + String contactJid = data.getStringExtra("contact"); + String conversationUuid = data.getStringExtra("conversation"); + Conversation conversation = xmppConnectionService + .findConversationByUuid(conversationUuid); + if (conversation.getMode() == Conversation.MODE_MULTI) { + xmppConnectionService.invite(conversation, contactJid); + } + Log.d(Config.LOGTAG, "inviting " + contactJid + " to " + + conversation.getName()); + } + } + + public int getSecondaryTextColor() { + return this.mSecondaryTextColor; + } + + public int getPrimaryTextColor() { + return this.mPrimaryTextColor; + } + + public int getWarningTextColor() { + return this.mColorRed; + } + + public int getPrimaryColor() { + return this.mPrimaryColor; + } + + public int getSecondaryBackgroundColor() { + return this.mSecondaryBackgroundColor; + } + + public int getPixel(int dp) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } + + public boolean copyTextToClipboard(String text,int labelResId) { + ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + String label = getResources().getString(labelResId); + if (mClipBoardManager != null) { + ClipData mClipData = ClipData.newPlainText(label, text); + mClipBoardManager.setPrimaryClip(mClipData); + return true; + } + return false; + } + + public AvatarService avatarService() { + return xmppConnectionService.getAvatarService(); + } + + class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { + private final WeakReference<ImageView> imageViewReference; + private Message message = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<ImageView>(imageView); + } + + @Override + protected Bitmap doInBackground(Message... params) { + message = params[0]; + try { + return xmppConnectionService.getFileBackend().getThumbnail( + message, (int) (metrics.density * 288), false); + } catch (FileNotFoundException e) { + return null; + } + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (imageViewReference != null && bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + public void loadBitmap(Message message, ImageView imageView) { + Bitmap bm; + try { + bm = xmppConnectionService.getFileBackend().getThumbnail(message, + (int) (metrics.density * 288), true); + } catch (FileNotFoundException e) { + bm = null; + } + if (bm != null) { + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + if (cancelPotentialWork(message, imageView)) { + imageView.setBackgroundColor(0xff333333); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable( + getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(message); + } catch (RejectedExecutionException e) { + return; + } + } + } + } + + public static boolean cancelPotentialWork(Message message, + ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Message oldMessage = bitmapWorkerTask.message; + if (oldMessage == null || message != oldMessage) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, + BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<BitmapWorkerTask>( + bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java new file mode 100644 index 00000000..e13b3204 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -0,0 +1,102 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.ui.XmppActivity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class AccountAdapter extends ArrayAdapter<Account> { + + private XmppActivity activity; + + public AccountAdapter(XmppActivity activity, List<Account> objects) { + super(activity, 0, objects); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + Account account = getItem(position); + if (view == null) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.account_row, parent, false); + } + TextView jid = (TextView) view.findViewById(R.id.account_jid); + jid.setText(account.getJid()); + TextView statusView = (TextView) view.findViewById(R.id.account_status); + ImageView imageView = (ImageView) view.findViewById(R.id.account_image); + imageView.setImageBitmap(activity.avatarService().get(account, + activity.getPixel(48))); + switch (account.getStatus()) { + case Account.STATUS_DISABLED: + statusView.setText(getContext().getString( + R.string.account_status_disabled)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_ONLINE: + statusView.setText(getContext().getString( + R.string.account_status_online)); + statusView.setTextColor(activity.getPrimaryColor()); + break; + case Account.STATUS_CONNECTING: + statusView.setText(getContext().getString( + R.string.account_status_connecting)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_OFFLINE: + statusView.setText(getContext().getString( + R.string.account_status_offline)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_UNAUTHORIZED: + statusView.setText(getContext().getString( + R.string.account_status_unauthorized)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_SERVER_NOT_FOUND: + statusView.setText(getContext().getString( + R.string.account_status_not_found)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_NO_INTERNET: + statusView.setText(getContext().getString( + R.string.account_status_no_internet)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_FAILED: + statusView.setText(getContext().getString( + R.string.account_status_regis_fail)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_CONFLICT: + statusView.setText(getContext().getString( + R.string.account_status_regis_conflict)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + case Account.STATUS_REGISTRATION_SUCCESSFULL: + statusView.setText(getContext().getString( + R.string.account_status_regis_success)); + statusView.setTextColor(activity.getSecondaryTextColor()); + break; + case Account.STATUS_REGISTRATION_NOT_SUPPORTED: + statusView.setText(getContext().getString( + R.string.account_status_regis_not_sup)); + statusView.setTextColor(activity.getWarningTextColor()); + break; + default: + statusView.setText(""); + break; + } + + return view; + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java new file mode 100644 index 00000000..b5c20dc5 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -0,0 +1,139 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.utils.UIHelper; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class ConversationAdapter extends ArrayAdapter<Conversation> { + + private XmppActivity activity; + + public ConversationAdapter(XmppActivity activity, + List<Conversation> conversations) { + super(activity, 0, conversations); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + if (view == null) { + LayoutInflater inflater = (LayoutInflater) activity + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + view = inflater.inflate(R.layout.conversation_list_row, + parent, false); + } + Conversation conversation = getItem(position); + if (this.activity instanceof ConversationActivity) { + ConversationActivity activity = (ConversationActivity) this.activity; + if (!activity.isConversationsOverviewHideable()) { + if (conversation == activity.getSelectedConversation()) { + view.setBackgroundColor(activity + .getSecondaryBackgroundColor()); + } else { + view.setBackgroundColor(Color.TRANSPARENT); + } + } else { + view.setBackgroundColor(Color.TRANSPARENT); + } + } + TextView convName = (TextView) view + .findViewById(R.id.conversation_name); + if (conversation.getMode() == Conversation.MODE_SINGLE + || activity.useSubjectToIdentifyConference()) { + convName.setText(conversation.getName()); + } else { + convName.setText(conversation.getContactJid().split("/")[0]); + } + TextView mLastMessage = (TextView) view + .findViewById(R.id.conversation_lastmsg); + TextView mTimestamp = (TextView) view + .findViewById(R.id.conversation_lastupdate); + ImageView imagePreview = (ImageView) view + .findViewById(R.id.conversation_lastimage); + + Message message = conversation.getLatestMessage(); + + if (!conversation.isRead()) { + convName.setTypeface(null, Typeface.BOLD); + } else { + convName.setTypeface(null, Typeface.NORMAL); + } + + if (message.getType() == Message.TYPE_IMAGE + || message.getDownloadable() != null) { + Downloadable d = message.getDownloadable(); + if (conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.ITALIC); + } else { + mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC); + } + if (d != null) { + mLastMessage.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + if (d.getStatus() == Downloadable.STATUS_CHECKING) { + mLastMessage.setText(R.string.checking_image); + } else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) { + mLastMessage.setText(R.string.receiving_image); + } else if (d.getStatus() == Downloadable.STATUS_OFFER) { + mLastMessage.setText(R.string.image_offered_for_download); + } else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + mLastMessage.setText(R.string.image_offered_for_download); + } else if (d.getStatus() == Downloadable.STATUS_DELETED) { + mLastMessage.setText(R.string.image_file_deleted); + } else { + mLastMessage.setText(""); + } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + imagePreview.setVisibility(View.GONE); + mLastMessage.setVisibility(View.VISIBLE); + mLastMessage.setText(R.string.encrypted_message_received); + } else { + mLastMessage.setVisibility(View.GONE); + imagePreview.setVisibility(View.VISIBLE); + activity.loadBitmap(message, imagePreview); + } + } else { + if ((message.getEncryption() != Message.ENCRYPTION_PGP) + && (message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { + String body = Config.PARSE_EMOTICONS ? UIHelper + .transformAsciiEmoticons(message.getBody()) : message + .getBody(); + mLastMessage.setText(body); + } else { + mLastMessage.setText(R.string.encrypted_message_received); + } + if (!conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.BOLD); + } else { + mLastMessage.setTypeface(null, Typeface.NORMAL); + } + mLastMessage.setVisibility(View.VISIBLE); + imagePreview.setVisibility(View.GONE); + } + mTimestamp.setText(UIHelper.readableTimeDifference(getContext(), + conversation.getLatestMessage().getTimeSent())); + + ImageView profilePicture = (ImageView) view + .findViewById(R.id.conversation_image); + profilePicture.setImageBitmap(activity.avatarService().get( + conversation, activity.getPixel(56))); + + return view; + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java new file mode 100644 index 00000000..143dfda1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java @@ -0,0 +1,74 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import android.content.Context; +import android.widget.ArrayAdapter; +import android.widget.Filter; + +public class KnownHostsAdapter extends ArrayAdapter<String> { + private ArrayList<String> domains; + private Filter domainFilter = new Filter() { + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + if (constraint != null) { + ArrayList<String> suggestions = new ArrayList<String>(); + final String[] split = constraint.toString().split("@"); + if (split.length == 1) { + for (String domain : domains) { + suggestions.add(split[0].toLowerCase(Locale + .getDefault()) + "@" + domain); + } + } else if (split.length == 2) { + for (String domain : domains) { + if (domain.contentEquals(split[1])) { + suggestions.clear(); + break; + } else if (domain.contains(split[1])) { + suggestions.add(split[0].toLowerCase(Locale + .getDefault()) + "@" + domain); + } + } + } else { + return new FilterResults(); + } + FilterResults filterResults = new FilterResults(); + filterResults.values = suggestions; + filterResults.count = suggestions.size(); + return filterResults; + } else { + return new FilterResults(); + } + } + + @Override + protected void publishResults(CharSequence constraint, + FilterResults results) { + ArrayList filteredList = (ArrayList) results.values; + if (results != null && results.count > 0) { + clear(); + for (Object c : filteredList) { + add((String) c); + } + notifyDataSetChanged(); + } + } + }; + + public KnownHostsAdapter(Context context, int viewResourceId, + List<String> mKnownHosts) { + super(context, viewResourceId, mKnownHosts); + domains = new ArrayList<String>(mKnownHosts.size()); + for (String domain : mKnownHosts) { + domains.add(new String(domain)); + } + } + + @Override + public Filter getFilter() { + return domainFilter; + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java new file mode 100644 index 00000000..efc6b4d9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -0,0 +1,44 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.ui.XmppActivity; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +public class ListItemAdapter extends ArrayAdapter<ListItem> { + + protected XmppActivity activity; + + public ListItemAdapter(XmppActivity activity, List<ListItem> objects) { + super(activity, 0, objects); + this.activity = activity; + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ListItem item = getItem(position); + if (view == null) { + view = inflater.inflate(R.layout.contact, parent, false); + } + TextView name = (TextView) view.findViewById(R.id.contact_display_name); + TextView jid = (TextView) view.findViewById(R.id.contact_jid); + ImageView picture = (ImageView) view.findViewById(R.id.contact_photo); + + jid.setText(item.getJid()); + name.setText(item.getDisplayName()); + picture.setImageBitmap(activity.avatarService().get(item, + activity.getPixel(48))); + return view; + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java new file mode 100644 index 00000000..a24f90d7 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -0,0 +1,555 @@ +package eu.siacs.conversations.ui.adapter; + +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Message.ImageParams; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.utils.UIHelper; +import android.content.Intent; +import android.graphics.Typeface; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +public class MessageAdapter extends ArrayAdapter<Message> { + + private static final int SENT = 0; + private static final int RECEIVED = 1; + private static final int STATUS = 2; + private static final int NULL = 3; + + private ConversationActivity activity; + + private DisplayMetrics metrics; + + private OnContactPictureClicked mOnContactPictureClickedListener; + private OnContactPictureLongClicked mOnContactPictureLongClickedListener; + + private OnLongClickListener openContextMenu = new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + v.showContextMenu(); + return true; + } + }; + + public MessageAdapter(ConversationActivity activity, List<Message> messages) { + super(activity, 0, messages); + this.activity = activity; + metrics = getContext().getResources().getDisplayMetrics(); + } + + public void setOnContactPictureClicked(OnContactPictureClicked listener) { + this.mOnContactPictureClickedListener = listener; + } + + public void setOnContactPictureLongClicked( + OnContactPictureLongClicked listener) { + this.mOnContactPictureLongClickedListener = listener; + } + + @Override + public int getViewTypeCount() { + return 4; + } + + @Override + public int getItemViewType(int position) { + if (getItem(position).wasMergedIntoPrevious()) { + return NULL; + } else if (getItem(position).getType() == Message.TYPE_STATUS) { + return STATUS; + } else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) { + return RECEIVED; + } else { + return SENT; + } + } + + private void displayStatus(ViewHolder viewHolder, Message message) { + String filesize = null; + String info = null; + boolean error = false; + if (viewHolder.indicatorReceived != null) { + viewHolder.indicatorReceived.setVisibility(View.GONE); + } + boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getMergedStatus() <= Message.STATUS_RECEIVED; + if (message.getType() == Message.TYPE_IMAGE + || message.getDownloadable() != null) { + ImageParams params = message.getImageParams(); + if (params.size != 0) { + filesize = params.size / 1024 + " KB"; + } + if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) { + error = true; + } + } + switch (message.getMergedStatus()) { + case Message.STATUS_WAITING: + info = getContext().getString(R.string.waiting); + break; + case Message.STATUS_UNSEND: + info = getContext().getString(R.string.sending); + break; + case Message.STATUS_OFFERED: + info = getContext().getString(R.string.offering); + break; + case Message.STATUS_SEND_RECEIVED: + if (activity.indicateReceived()) { + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + } + break; + case Message.STATUS_SEND_DISPLAYED: + if (activity.indicateReceived()) { + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + } + break; + case Message.STATUS_SEND_FAILED: + info = getContext().getString(R.string.send_failed); + error = true; + break; + default: + if (multiReceived) { + Contact contact = message.getContact(); + if (contact != null) { + info = contact.getDisplayName(); + } else { + if (message.getPresence() != null) { + info = message.getPresence(); + } else { + info = message.getCounterpart(); + } + } + } + break; + } + if (error) { + viewHolder.time.setTextColor(activity.getWarningTextColor()); + } else { + viewHolder.time.setTextColor(activity.getSecondaryTextColor()); + } + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + viewHolder.indicator.setVisibility(View.GONE); + } else { + viewHolder.indicator.setVisibility(View.VISIBLE); + } + + String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), + message.getMergedTimeSent()); + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(filesize + " \u00B7 " + info); + } else if ((filesize == null) && (info != null)) { + viewHolder.time.setText(formatedTime + " \u00B7 " + info); + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(formatedTime + " \u00B7 " + filesize); + } else { + viewHolder.time.setText(formatedTime); + } + } else { + if ((filesize != null) && (info != null)) { + viewHolder.time.setText(filesize + " \u00B7 " + info); + } else if ((filesize == null) && (info != null)) { + if (error) { + viewHolder.time.setText(info + " \u00B7 " + formatedTime); + } else { + viewHolder.time.setText(info); + } + } else if ((filesize != null) && (info == null)) { + viewHolder.time.setText(filesize + " \u00B7 " + formatedTime); + } else { + viewHolder.time.setText(formatedTime); + } + } + } + + private void displayInfoMessage(ViewHolder viewHolder, int r) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(getContext().getString(r)); + viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayDecryptionFailed(ViewHolder viewHolder) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(getContext().getString( + R.string.decryption_failed)); + viewHolder.messageBody.setTextColor(activity.getWarningTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayTextMessage(ViewHolder viewHolder, Message message) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + if (message.getBody() != null) { + if (message.getType() != Message.TYPE_PRIVATE) { + String body = Config.PARSE_EMOTICONS ? UIHelper + .transformAsciiEmoticons(message.getMergedBody()) + : message.getMergedBody(); + viewHolder.messageBody.setText(body); + } else { + String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity + .getString(R.string.private_message); + } else { + String to; + if (message.getPresence() != null) { + to = message.getPresence(); + } else { + to = message.getCounterpart(); + } + privateMarker = activity.getString( + R.string.private_message_to, to); + } + SpannableString span = new SpannableString(privateMarker + " " + + message.getBody()); + span.setSpan( + new ForegroundColorSpan(activity + .getSecondaryTextColor()), 0, privateMarker + .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + span.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, + privateMarker.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(span); + } + } else { + viewHolder.messageBody.setText(""); + } + viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor()); + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(true); + } + + private void displayDownloadableMessage(ViewHolder viewHolder, + final Message message, int resid) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(resid); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + startDonwloadable(message); + } + }); + viewHolder.download_button.setOnLongClickListener(openContextMenu); + } + + private void displayImageMessage(ViewHolder viewHolder, + final Message message) { + if (viewHolder.download_button != null) { + viewHolder.download_button.setVisibility(View.GONE); + } + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.VISIBLE); + ImageParams params = message.getImageParams(); + double target = metrics.density * 288; + int scalledW; + int scalledH; + if (params.width <= params.height) { + scalledW = (int) (params.width / ((double) params.height / target)); + scalledH = (int) target; + } else { + scalledW = (int) target; + scalledH = (int) (params.height / ((double) params.width / target)); + } + viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams( + scalledW, scalledH)); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(activity.xmppConnectionService + .getFileBackend().getJingleFileUri(message), "image/*"); + getContext().startActivity(intent); + } + }); + viewHolder.image.setOnLongClickListener(openContextMenu); + } + + @Override + public View getView(int position, View view, ViewGroup parent) { + final Message item = getItem(position); + int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case NULL: + view = activity.getLayoutInflater().inflate( + R.layout.message_null, parent, false); + break; + case SENT: + view = activity.getLayoutInflater().inflate( + R.layout.message_sent, parent, false); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get( + item.getConversation().getAccount(), + activity.getPixel(48))); + viewHolder.download_button = (Button) view + .findViewById(R.id.download_button); + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + viewHolder.indicatorReceived = (ImageView) view + .findViewById(R.id.indicator_received); + view.setTag(viewHolder); + break; + case RECEIVED: + view = activity.getLayoutInflater().inflate( + R.layout.message_received, parent, false); + viewHolder.message_box = (LinearLayout) view + .findViewById(R.id.message_box); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + viewHolder.download_button = (Button) view + .findViewById(R.id.download_button); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(item.getContact(), + activity.getPixel(48))); + } + viewHolder.indicator = (ImageView) view + .findViewById(R.id.security_indicator); + viewHolder.image = (ImageView) view + .findViewById(R.id.message_image); + viewHolder.messageBody = (TextView) view + .findViewById(R.id.message_body); + viewHolder.time = (TextView) view + .findViewById(R.id.message_time); + view.setTag(viewHolder); + break; + case STATUS: + view = activity.getLayoutInflater().inflate( + R.layout.message_status, parent, false); + viewHolder.contact_picture = (ImageView) view + .findViewById(R.id.message_photo); + if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { + + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get( + item.getConversation().getContact(), + activity.getPixel(32))); + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + String name = item.getConversation() + .getName(); + String read = getContext() + .getString( + R.string.contact_has_read_up_to_this_point, + name); + Toast.makeText(getContext(), read, + Toast.LENGTH_SHORT).show(); + } + }); + + } + break; + default: + viewHolder = null; + break; + } + } else { + viewHolder = (ViewHolder) view.getTag(); + } + + if (type == STATUS) { + return view; + } + if (type == NULL) { + if (position == getCount() - 1) { + view.getLayoutParams().height = 1; + } else { + view.getLayoutParams().height = 0; + + } + view.setLayoutParams(view.getLayoutParams()); + return view; + } + + if (viewHolder.contact_picture != null) { + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(item); + ; + } + + } + }); + viewHolder.contact_picture + .setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(item); + return true; + } else { + return false; + } + } + }); + } + + if (type == RECEIVED) { + if (item.getConversation().getMode() == Conversation.MODE_MULTI) { + Contact contact = item.getContact(); + if (contact != null) { + viewHolder.contact_picture.setImageBitmap(activity + .avatarService() + .get(contact, activity.getPixel(48))); + } else { + String name = item.getPresence(); + if (name == null) { + name = item.getCounterpart(); + } + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(name, activity.getPixel(48))); + } + } + } + + if (item.getType() == Message.TYPE_IMAGE + || item.getDownloadable() != null) { + Downloadable d = item.getDownloadable(); + if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) { + displayInfoMessage(viewHolder, R.string.receiving_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_CHECKING) { + displayInfoMessage(viewHolder, R.string.checking_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_DELETED) { + displayInfoMessage(viewHolder, R.string.image_file_deleted); + } else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder, item, + R.string.download_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, item, + R.string.check_image_filesize); + } else if (d != null && d.getStatus() == Downloadable.STATUS_FAILED) { + displayInfoMessage(viewHolder, R.string.image_transmission_failed); + } else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED) + || (item.getEncryption() == Message.ENCRYPTION_NONE) + || (item.getEncryption() == Message.ENCRYPTION_OTR)) { + displayImageMessage(viewHolder, item); + } else if (item.getEncryption() == Message.ENCRYPTION_PGP) { + displayInfoMessage(viewHolder, R.string.encrypted_message); + } else { + displayDecryptionFailed(viewHolder); + } + } else { + if (item.getEncryption() == Message.ENCRYPTION_PGP) { + if (activity.hasPgp()) { + displayInfoMessage(viewHolder, R.string.encrypted_message); + } else { + displayInfoMessage(viewHolder, + R.string.install_openkeychain); + viewHolder.message_box + .setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + activity.showInstallPgpDialog(); + } + }); + } + } else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayDecryptionFailed(viewHolder); + } else { + displayTextMessage(viewHolder, item); + } + } + + displayStatus(viewHolder, item); + + return view; + } + + public void startDonwloadable(Message message) { + Downloadable downloadable = message.getDownloadable(); + if (downloadable != null) { + if (!downloadable.start()) { + Toast.makeText(activity, R.string.not_connected_try_again, + Toast.LENGTH_SHORT).show(); + } + } + } + + private static class ViewHolder { + + protected LinearLayout message_box; + protected Button download_button; + protected ImageView image; + protected ImageView indicator; + protected ImageView indicatorReceived; + protected TextView time; + protected TextView messageBody; + protected ImageView contact_picture; + + } + + public interface OnContactPictureClicked { + public void onContactPictureClicked(Message message); + } + + public interface OnContactPictureLongClicked { + public void onContactPictureLongClicked(Message message); + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java new file mode 100644 index 00000000..47595c6e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -0,0 +1,112 @@ +package eu.siacs.conversations.utils; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import eu.siacs.conversations.entities.Account; +import android.util.Base64; + +public class CryptoHelper { + public static final String FILETRANSFER = "?FILETRANSFERv1:"; + final protected static char[] hexArray = "0123456789abcdef".toCharArray(); + final protected static char[] vowels = "aeiou".toCharArray(); + final protected static char[] consonants = "bcdfghjklmnpqrstvwxyz" + .toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + public static byte[] hexToBytes(String hexString) { + int len = hexString.length(); + byte[] array = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character + .digit(hexString.charAt(i + 1), 16)); + } + return array; + } + + public static String saslPlain(String username, String password) { + String sasl = '\u0000' + username + '\u0000' + password; + return Base64.encodeToString(sasl.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } + + private static byte[] concatenateByteArrays(byte[] a, byte[] b) { + byte[] result = new byte[a.length + b.length]; + System.arraycopy(a, 0, result, 0, a.length); + System.arraycopy(b, 0, result, a.length, b.length); + return result; + } + + public static String saslDigestMd5(Account account, String challenge, + SecureRandom random) { + try { + String[] challengeParts = new String(Base64.decode(challenge, + Base64.DEFAULT)).split(","); + String nonce = ""; + for (int i = 0; i < challengeParts.length; ++i) { + String[] parts = challengeParts[i].split("="); + if (parts[0].equals("nonce")) { + nonce = parts[1].replace("\"", ""); + } else if (parts[0].equals("rspauth")) { + return null; + } + } + String digestUri = "xmpp/" + account.getServer(); + String nonceCount = "00000001"; + String x = account.getUsername() + ":" + account.getServer() + ":" + + account.getPassword(); + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); + String cNonce = new BigInteger(100, random).toString(32); + byte[] a1 = concatenateByteArrays(y, + (":" + nonce + ":" + cNonce).getBytes(Charset + .defaultCharset())); + String a2 = "AUTHENTICATE:" + digestUri; + String ha1 = bytesToHex(md.digest(a1)); + String ha2 = bytesToHex(md.digest(a2.getBytes(Charset + .defaultCharset()))); + String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + + ":auth:" + ha2; + String response = bytesToHex(md.digest(kd.getBytes(Charset + .defaultCharset()))); + String saslString = "username=\"" + account.getUsername() + + "\",realm=\"" + account.getServer() + "\",nonce=\"" + + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount + + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" + + response + ",charset=utf-8"; + return Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + public static String randomMucName(SecureRandom random) { + return randomWord(3, random) + "." + randomWord(7, random); + } + + protected static String randomWord(int lenght, SecureRandom random) { + StringBuilder builder = new StringBuilder(lenght); + for (int i = 0; i < lenght; ++i) { + if (i % 2 == 0) { + builder.append(consonants[random.nextInt(consonants.length)]); + } else { + builder.append(vowels[random.nextInt(vowels.length)]); + } + } + return builder.toString(); + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java new file mode 100644 index 00000000..f101e883 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -0,0 +1,171 @@ +package eu.siacs.conversations.utils; + +import de.measite.minidns.Client; +import de.measite.minidns.DNSMessage; +import de.measite.minidns.Record; +import de.measite.minidns.Record.TYPE; +import de.measite.minidns.Record.CLASS; +import de.measite.minidns.record.SRV; +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; +import de.measite.minidns.record.Data; +import de.measite.minidns.util.NameUtil; +import eu.siacs.conversations.Config; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Random; +import java.util.TreeMap; + +import android.os.Bundle; +import android.util.Log; + +public class DNSHelper { + protected static Client client = new Client(); + + public static Bundle getSRVRecord(String host) throws IOException { + String dns[] = client.findDNS(); + + if (dns != null) { + for (String dnsserver : dns) { + InetAddress ip = InetAddress.getByName(dnsserver); + Bundle b = queryDNS(host, ip); + if (b.containsKey("values")) { + return b; + } else if (b.containsKey("error") + && "nosrv".equals(b.getString("error", null))) { + return b; + } + } + } + return queryDNS(host, InetAddress.getByName("8.8.8.8")); + } + + public static Bundle queryDNS(String host, InetAddress dnsServer) { + Bundle bundle = new Bundle(); + try { + String qname = "_xmpp-client._tcp." + host; + Log.d(Config.LOGTAG, + "using dns server: " + dnsServer.getHostAddress() + + " to look up " + host); + DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, + dnsServer.getHostAddress()); + + // How should we handle priorities and weight? + // Wikipedia has a nice article about priorities vs. weights: + // https://en.wikipedia.org/wiki/SRV_record#Provisioning_for_high_service_availability + + // we bucket the SRV records based on priority, pick per priority + // a random order respecting the weight, and dump that priority by + // priority + + TreeMap<Integer, ArrayList<SRV>> priorities = new TreeMap<Integer, ArrayList<SRV>>(); + TreeMap<String, ArrayList<String>> ips4 = new TreeMap<String, ArrayList<String>>(); + TreeMap<String, ArrayList<String>> ips6 = new TreeMap<String, ArrayList<String>>(); + + for (Record[] rrset : new Record[][] { message.getAnswers(), + message.getAdditionalResourceRecords() }) { + for (Record rr : rrset) { + Data d = rr.getPayload(); + if (d instanceof SRV + && NameUtil.idnEquals(qname, rr.getName())) { + SRV srv = (SRV) d; + if (!priorities.containsKey(srv.getPriority())) { + priorities.put(srv.getPriority(), + new ArrayList<SRV>(2)); + } + priorities.get(srv.getPriority()).add(srv); + } + if (d instanceof A) { + A arecord = (A) d; + if (!ips4.containsKey(rr.getName())) { + ips4.put(rr.getName(), new ArrayList<String>(3)); + } + ips4.get(rr.getName()).add(arecord.toString()); + } + if (d instanceof AAAA) { + AAAA aaaa = (AAAA) d; + if (!ips6.containsKey(rr.getName())) { + ips6.put(rr.getName(), new ArrayList<String>(3)); + } + ips6.get(rr.getName()).add("[" + aaaa.toString() + "]"); + } + } + } + + Random rnd = new Random(); + ArrayList<SRV> result = new ArrayList<SRV>( + priorities.size() * 2 + 1); + for (ArrayList<SRV> s : priorities.values()) { + + // trivial case + if (s.size() <= 1) { + result.addAll(s); + continue; + } + + long totalweight = 0l; + for (SRV srv : s) { + totalweight += srv.getWeight(); + } + + while (totalweight > 0l && s.size() > 0) { + long p = (rnd.nextLong() & 0x7fffffffffffffffl) + % totalweight; + int i = 0; + while (p > 0) { + p -= s.get(i++).getPriority(); + } + i--; + // remove is expensive, but we have only a few entries + // anyway + SRV srv = s.remove(i); + totalweight -= srv.getWeight(); + result.add(srv); + } + + Collections.shuffle(s, rnd); + result.addAll(s); + + } + + if (result.size() == 0) { + bundle.putString("error", "nosrv"); + return bundle; + } + ArrayList<Bundle> values = new ArrayList<Bundle>(); + for (SRV srv : result) { + Bundle namePort = new Bundle(); + namePort.putString("name", srv.getName()); + namePort.putInt("port", srv.getPort()); + if (ips4.containsKey(srv.getName())) { + ArrayList<String> ip = ips4.get(srv.getName()); + Collections.shuffle(ip, rnd); + namePort.putString("ipv4", ip.get(0)); + } + values.add(namePort); + } + bundle.putParcelableArrayList("values", values); + } catch (SocketTimeoutException e) { + bundle.putString("error", "timeout"); + } catch (Exception e) { + bundle.putString("error", "unhandled"); + } + return bundle; + } + + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java new file mode 100644 index 00000000..88fa18ff --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java @@ -0,0 +1,44 @@ +package eu.siacs.conversations.utils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.Thread.UncaughtExceptionHandler; + +import android.content.Context; + +public class ExceptionHandler implements UncaughtExceptionHandler { + + private UncaughtExceptionHandler defaultHandler; + private Context context; + + public ExceptionHandler(Context context) { + this.context = context; + this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + Writer result = new StringWriter(); + PrintWriter printWriter = new PrintWriter(result); + ex.printStackTrace(printWriter); + String stacktrace = result.toString(); + printWriter.close(); + try { + OutputStream os = context.openFileOutput("stacktrace.txt", + Context.MODE_PRIVATE); + os.write(stacktrace.getBytes()); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + this.defaultHandler.uncaughtException(thread, ex); + } + +} diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java new file mode 100644 index 00000000..b5fc88bd --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -0,0 +1,117 @@ +package eu.siacs.conversations.utils; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.content.DialogInterface.OnClickListener; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.preference.PreferenceManager; +import android.text.format.DateUtils; +import android.util.Log; + +public class ExceptionHelper { + public static void init(Context context) { + if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) { + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler( + context)); + } + } + + public static void checkForCrash(Context context, + final XmppConnectionService service) { + try { + final SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(context); + boolean neverSend = preferences.getBoolean("never_send", false); + if (neverSend) { + return; + } + List<Account> accounts = service.getAccounts(); + Account account = null; + for (int i = 0; i < accounts.size(); ++i) { + if (!accounts.get(i).isOptionSet(Account.OPTION_DISABLED)) { + account = accounts.get(i); + break; + } + } + if (account == null) { + return; + } + final Account finalAccount = account; + FileInputStream file = context.openFileInput("stacktrace.txt"); + InputStreamReader inputStreamReader = new InputStreamReader(file); + BufferedReader stacktrace = new BufferedReader(inputStreamReader); + final StringBuilder report = new StringBuilder(); + PackageManager pm = context.getPackageManager(); + PackageInfo packageInfo = null; + try { + packageInfo = pm.getPackageInfo(context.getPackageName(), 0); + report.append("Version: " + packageInfo.versionName + '\n'); + report.append("Last Update: " + + DateUtils.formatDateTime(context, + packageInfo.lastUpdateTime, + DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_DATE) + '\n'); + } catch (NameNotFoundException e) { + } + String line; + while ((line = stacktrace.readLine()) != null) { + report.append(line); + report.append('\n'); + } + file.close(); + context.deleteFile("stacktrace.txt"); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(context.getString(R.string.crash_report_title)); + builder.setMessage(context.getText(R.string.crash_report_message)); + builder.setPositiveButton(context.getText(R.string.send_now), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + + Log.d(Config.LOGTAG, "using account=" + + finalAccount.getJid() + + " to send in stack trace"); + Conversation conversation = service + .findOrCreateConversation(finalAccount, + "bugs@siacs.eu", false); + Message message = new Message(conversation, report + .toString(), Message.ENCRYPTION_NONE); + service.sendMessage(message); + } + }); + builder.setNegativeButton(context.getText(R.string.send_never), + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + preferences.edit().putBoolean("never_send", true) + .commit(); + } + }); + builder.create().show(); + } catch (FileNotFoundException e) { + return; + } catch (IOException e) { + return; + } + + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java new file mode 100644 index 00000000..ceda7293 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.siacs.conversations.utils; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; + +public class ExifHelper { + private static final String TAG = "CameraExif"; + + public static int getOrientation(InputStream is) { + if (is == null) { + return 0; + } + + byte[] buf = new byte[8]; + int length = 0; + + // ISO/IEC 10918-1:1993(E) + while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) { + int marker = buf[1] & 0xFF; + + // Check if the marker is a padding. + if (marker == 0xFF) { + continue; + } + + // Check if the marker is SOI or TEM. + if (marker == 0xD8 || marker == 0x01) { + continue; + } + // Check if the marker is EOI or SOS. + if (marker == 0xD9 || marker == 0xDA) { + return 0; + } + + // Get the length and check if it is reasonable. + if (!read(is, buf, 2)) { + return 0; + } + length = pack(buf, 0, 2, false); + if (length < 2) { + Log.e(TAG, "Invalid length"); + return 0; + } + length -= 2; + + // Break if the marker is EXIF in APP1. + if (marker == 0xE1 && length >= 6) { + if (!read(is, buf, 6)) return 0; + length -= 6; + if (pack(buf, 0, 4, false) == 0x45786966 && + pack(buf, 4, 2, false) == 0) { + break; + } + } + + // Skip other markers. + try { + is.skip(length); + } catch (IOException ex) { + return 0; + } + length = 0; + } + + // JEITA CP-3451 Exif Version 2.2 + if (length > 8) { + int offset = 0; + byte[] jpeg = new byte[length]; + if (!read(is, jpeg, length)) { + return 0; + } + + // Identify the byte order. + int tag = pack(jpeg, offset, 4, false); + if (tag != 0x49492A00 && tag != 0x4D4D002A) { + Log.e(TAG, "Invalid byte order"); + return 0; + } + boolean littleEndian = (tag == 0x49492A00); + + // Get the offset and check if it is reasonable. + int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; + if (count < 10 || count > length) { + Log.e(TAG, "Invalid offset"); + return 0; + } + offset += count; + length -= count; + + // Get the count and go through all the elements. + count = pack(jpeg, offset - 2, 2, littleEndian); + while (count-- > 0 && length >= 12) { + // Get the tag and check if it is orientation. + tag = pack(jpeg, offset, 2, littleEndian); + if (tag == 0x0112) { + // We do not really care about type and count, do we? + int orientation = pack(jpeg, offset + 8, 2, littleEndian); + switch (orientation) { + case 1: + return 0; + case 3: + return 180; + case 6: + return 90; + case 8: + return 270; + } + Log.i(TAG, "Unsupported orientation"); + return 0; + } + offset += 12; + length -= 12; + } + } + + Log.i(TAG, "Orientation not found"); + return 0; + } + + private static int pack(byte[] bytes, int offset, int length, + boolean littleEndian) { + int step = 1; + if (littleEndian) { + offset += length - 1; + step = -1; + } + + int value = 0; + while (length-- > 0) { + value = (value << 8) | (bytes[offset] & 0xFF); + offset += step; + } + return value; + } + + private static boolean read(InputStream is, byte[] buf, int length) { + try { + return is.read(buf, 0, length) == length; + } catch (IOException ex) { + return false; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java new file mode 100644 index 00000000..9a689768 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.utils; + +import java.util.List; + +import android.os.Bundle; + +public interface OnPhoneContactsLoadedListener { + public void onPhoneContactsLoaded(List<Bundle> phoneContacts); +} diff --git a/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java new file mode 100644 index 00000000..8fe67234 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/PRNGFixes.java @@ -0,0 +1,327 @@ +package eu.siacs.conversations.utils; + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + * + * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +public final class PRNGFixes { + + private static final int VERSION_CODE_JELLY_BEAN = 16; + private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18; + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial(); + + /** Hidden constructor to prevent instantiation. */ + private PRNGFixes() { + } + + /** + * Applies all fixes. + * + * @throws SecurityException + * if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException + * if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN) + || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class + .forName( + "org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException( + "Unexpected number of bytes read from Linux PRNG: " + + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException + * if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() throws SecurityException { + if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = Security + .getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class + .equals(secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider() + .getClass())) { + throw new SecurityException( + "new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals(rng2.getProvider() + .getClass())) { + throw new SecurityException( + "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong" + + " Provider: " + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through all + * requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", 1.0, + "A Linux-specific random number provider that uses" + + " /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG ( + * {@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + * + * @GuardedBy("sLock") + */ + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException("Failed to read from " + + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream(new FileInputStream( + URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + seedBufferOut.writeInt(Process.myUid()); + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +}
\ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java new file mode 100644 index 00000000..5becc7e7 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.content.Loader.OnLoadCompleteListener; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.ContactsContract.Profile; + +public class PhoneHelper { + + public static void loadPhoneContacts(Context context, + final OnPhoneContactsLoadedListener listener) { + final List<Bundle> phoneContacts = new ArrayList<Bundle>(); + + final String[] PROJECTION = new String[] { ContactsContract.Data._ID, + ContactsContract.Data.DISPLAY_NAME, + ContactsContract.Data.PHOTO_URI, + ContactsContract.Data.LOOKUP_KEY, + ContactsContract.CommonDataKinds.Im.DATA }; + + final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\"" + + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE + + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER + + "\")"; + + CursorLoader mCursorLoader = new CursorLoader(context, + ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null, + null); + mCursorLoader.registerListener(0, new OnLoadCompleteListener<Cursor>() { + + @Override + public void onLoadComplete(Loader<Cursor> arg0, Cursor cursor) { + if (cursor == null) { + return; + } + while (cursor.moveToNext()) { + Bundle contact = new Bundle(); + contact.putInt("phoneid", cursor.getInt(cursor + .getColumnIndex(ContactsContract.Data._ID))); + contact.putString( + "displayname", + cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.DISPLAY_NAME))); + contact.putString("photouri", cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.PHOTO_URI))); + contact.putString("lookup", cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.LOOKUP_KEY))); + + contact.putString( + "jid", + cursor.getString(cursor + .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA))); + phoneContacts.add(contact); + } + if (listener != null) { + listener.onPhoneContactsLoaded(phoneContacts); + } + } + }); + try { + mCursorLoader.startLoading(); + } catch (RejectedExecutionException e) { + if (listener != null) { + listener.onPhoneContactsLoaded(phoneContacts); + } + } + } + + public static Uri getSefliUri(Context context) { + String[] mProjection = new String[] { Profile._ID, Profile.PHOTO_URI }; + Cursor mProfileCursor = context.getContentResolver().query( + Profile.CONTENT_URI, mProjection, null, null, null); + + if (mProfileCursor == null || mProfileCursor.getCount() == 0) { + return null; + } else { + mProfileCursor.moveToFirst(); + String uri = mProfileCursor.getString(1); + if (uri == null) { + return null; + } else { + return Uri.parse(uri); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java new file mode 100644 index 00000000..5141c83c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -0,0 +1,225 @@ +package eu.siacs.conversations.utils; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.regex.Pattern; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.ui.ManageAccountActivity; +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; +import android.text.format.DateFormat; +import android.text.format.DateUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +public class UIHelper { + private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; + private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE; + + public static String readableTimeDifference(Context context, long time) { + return readableTimeDifference(context, time, false); + } + + public static String readableTimeDifferenceFull(Context context, long time) { + return readableTimeDifference(context, time, true); + } + + private static String readableTimeDifference(Context context, long time, + boolean fullDate) { + if (time == 0) { + return context.getString(R.string.just_now); + } + Date date = new Date(time); + long difference = (System.currentTimeMillis() - time) / 1000; + if (difference < 60) { + return context.getString(R.string.just_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.minute_ago); + } else if (difference < 60 * 15) { + return context.getString(R.string.minutes_ago, + Math.round(difference / 60.0)); + } else if (today(date)) { + java.text.DateFormat df = DateFormat.getTimeFormat(context); + return df.format(date); + } else { + if (fullDate) { + return DateUtils.formatDateTime(context, date.getTime(), + FULL_DATE_FLAGS); + } else { + return DateUtils.formatDateTime(context, date.getTime(), + SHORT_DATE_FLAGS); + } + } + } + + private static boolean today(Date date) { + Calendar cal1 = Calendar.getInstance(); + Calendar cal2 = Calendar.getInstance(); + cal1.setTime(date); + cal2.setTimeInMillis(System.currentTimeMillis()); + return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) + && cal1.get(Calendar.DAY_OF_YEAR) == cal2 + .get(Calendar.DAY_OF_YEAR); + } + + public static String lastseen(Context context, long time) { + if (time == 0) { + return context.getString(R.string.never_seen); + } + long difference = (System.currentTimeMillis() - time) / 1000; + if (difference < 60) { + return context.getString(R.string.last_seen_now); + } else if (difference < 60 * 2) { + return context.getString(R.string.last_seen_min); + } else if (difference < 60 * 60) { + return context.getString(R.string.last_seen_mins, + Math.round(difference / 60.0)); + } else if (difference < 60 * 60 * 2) { + return context.getString(R.string.last_seen_hour); + } else if (difference < 60 * 60 * 24) { + return context.getString(R.string.last_seen_hours, + Math.round(difference / (60.0 * 60.0))); + } else if (difference < 60 * 60 * 48) { + return context.getString(R.string.last_seen_day); + } else { + return context.getString(R.string.last_seen_days, + Math.round(difference / (60.0 * 60.0 * 24.0))); + } + } + + public static void showErrorNotification(Context context, + List<Account> accounts) { + NotificationManager mNotificationManager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + List<Account> accountsWproblems = new ArrayList<Account>(); + for (Account account : accounts) { + if (account.hasErrorStatus()) { + accountsWproblems.add(account); + } + } + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( + context); + if (accountsWproblems.size() == 0) { + mNotificationManager.cancel(1111); + return; + } else if (accountsWproblems.size() == 1) { + mBuilder.setContentTitle(context + .getString(R.string.problem_connecting_to_account)); + mBuilder.setContentText(accountsWproblems.get(0).getJid()); + } else { + mBuilder.setContentTitle(context + .getString(R.string.problem_connecting_to_accounts)); + mBuilder.setContentText(context.getString(R.string.touch_to_fix)); + } + mBuilder.setOngoing(true); + mBuilder.setLights(0xffffffff, 2000, 4000); + mBuilder.setSmallIcon(R.drawable.ic_notification); + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addParentStack(ConversationActivity.class); + + Intent manageAccountsIntent = new Intent(context, + ManageAccountActivity.class); + stackBuilder.addNextIntent(manageAccountsIntent); + + PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT); + + mBuilder.setContentIntent(resultPendingIntent); + Notification notification = mBuilder.build(); + mNotificationManager.notify(1111, notification); + } + + @SuppressLint("InflateParams") + public static AlertDialog getVerifyFingerprintDialog( + final ConversationActivity activity, + final Conversation conversation, final View msg) { + final Contact contact = conversation.getContact(); + final Account account = conversation.getAccount(); + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setTitle("Verify fingerprint"); + LayoutInflater inflater = activity.getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_verify_otr, null); + TextView jid = (TextView) view.findViewById(R.id.verify_otr_jid); + TextView fingerprint = (TextView) view + .findViewById(R.id.verify_otr_fingerprint); + TextView yourprint = (TextView) view + .findViewById(R.id.verify_otr_yourprint); + + jid.setText(contact.getJid()); + fingerprint.setText(conversation.getOtrFingerprint()); + yourprint.setText(account.getOtrFingerprint()); + builder.setNegativeButton("Cancel", null); + builder.setPositiveButton("Verify", new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + contact.addOtrFingerprint(conversation.getOtrFingerprint()); + msg.setVisibility(View.GONE); + activity.xmppConnectionService.syncRosterToDisk(account); + } + }); + builder.setView(view); + return builder.create(); + } + + private final static class EmoticonPattern { + Pattern pattern; + String replacement; + + EmoticonPattern(String ascii, int unicode) { + this.pattern = Pattern.compile("(?<=(^|\\s))" + ascii + + "(?=(\\s|$))"); + this.replacement = new String(new int[] { unicode, }, 0, 1); + } + + String replaceAll(String body) { + return pattern.matcher(body).replaceAll(replacement); + } + } + + private static final EmoticonPattern[] patterns = new EmoticonPattern[] { + new EmoticonPattern(":-?D", 0x1f600), + new EmoticonPattern("\\^\\^", 0x1f601), + new EmoticonPattern(":'D", 0x1f602), + new EmoticonPattern("\\]-?D", 0x1f608), + new EmoticonPattern(";-?\\)", 0x1f609), + new EmoticonPattern(":-?\\)", 0x1f60a), + new EmoticonPattern("[B8]-?\\)", 0x1f60e), + new EmoticonPattern(":-?\\|", 0x1f610), + new EmoticonPattern(":-?[/\\\\]", 0x1f615), + new EmoticonPattern(":-?\\*", 0x1f617), + new EmoticonPattern(":-?[Ppb]", 0x1f61b), + new EmoticonPattern(":-?\\(", 0x1f61e), + new EmoticonPattern(":-?[0Oo]", 0x1f62e), + new EmoticonPattern("\\\\o/", 0x1F631), }; + + public static String transformAsciiEmoticons(String body) { + if (body != null) { + for (EmoticonPattern p : patterns) { + body = p.replaceAll(body); + } + body = body.trim(); + } + return body; + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/Validator.java b/src/main/java/eu/siacs/conversations/utils/Validator.java new file mode 100644 index 00000000..00130fa2 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/Validator.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Validator { + public static final Pattern VALID_JID = Pattern.compile( + "^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE); + + public static boolean isValidJid(String jid) { + Matcher matcher = VALID_JID.matcher(jid); + return matcher.find(); + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/XmlHelper.java b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java new file mode 100644 index 00000000..4dee07cf --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/XmlHelper.java @@ -0,0 +1,12 @@ +package eu.siacs.conversations.utils; + +public class XmlHelper { + public static String encodeEntities(String content) { + content = content.replace("&", "&"); + content = content.replace("<", "<"); + content = content.replace(">", ">"); + content = content.replace("\"", """); + content = content.replace("'", "'"); + return content; + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java new file mode 100644 index 00000000..b777c10c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java @@ -0,0 +1,54 @@ +package eu.siacs.conversations.utils.zlib; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * ZLibInputStream is a zlib and input stream compatible version of an + * InflaterInputStream. This class solves the incompatibility between + * {@link InputStream#available()} and {@link InflaterInputStream#available()}. + */ +public class ZLibInputStream extends InflaterInputStream { + + /** + * Construct a ZLibInputStream, reading data from the underlying stream. + * + * @param is + * The {@code InputStream} to read data from. + * @throws IOException + * If an {@code IOException} occurs. + */ + public ZLibInputStream(InputStream is) throws IOException { + super(is, new Inflater(), 512); + } + + /** + * Provide a more InputStream compatible version of available. A return + * value of 1 means that it is likly to read one byte without blocking, 0 + * means that the system is known to block for more input. + * + * @return 0 if no data is available, 1 otherwise + * @throws IOException + */ + @Override + public int available() throws IOException { + /* + * This is one of the funny code blocks. InflaterInputStream.available + * violates the contract of InputStream.available, which breaks kXML2. + * + * I'm not sure who's to blame, oracle/sun for a broken api or the + * google guys for mixing a sun bug with a xml reader that can't handle + * it.... + * + * Anyway, this simple if breaks suns distorted reality, but helps to + * use the api as intended. + */ + if (inf.needsInput()) { + return 0; + } + return super.available(); + } + +} diff --git a/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java new file mode 100644 index 00000000..8b3f5e68 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.utils.zlib; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.NoSuchAlgorithmException; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +/** + * <p> + * Android 2.2 includes Java7 FLUSH_SYNC option, which will be used by this + * Implementation, preferable via reflection. The @hide was remove in API level + * 19. This class might thus go away in the future. + * </p> + * <p> + * Please use {@link ZLibOutputStream#SUPPORTED} to check for flush + * compatibility. + * </p> + */ +public class ZLibOutputStream extends DeflaterOutputStream { + + /** + * The reflection based flush method. + */ + + private final static Method method; + /** + * SUPPORTED is true if a flush compatible method exists. + */ + public final static boolean SUPPORTED; + + /** + * Static block to initialize {@link #SUPPORTED} and {@link #method}. + */ + static { + Method m = null; + try { + m = Deflater.class.getMethod("deflate", byte[].class, int.class, + int.class, int.class); + } catch (SecurityException e) { + } catch (NoSuchMethodException e) { + } + method = m; + SUPPORTED = (method != null); + } + + /** + * Create a new ZLib compatible output stream wrapping the given low level + * stream. ZLib compatiblity means we will send a zlib header. + * + * @param os + * OutputStream The underlying stream. + * @throws IOException + * In case of a lowlevel transfer problem. + * @throws NoSuchAlgorithmException + * In case of a {@link Deflater} error. + */ + public ZLibOutputStream(OutputStream os) throws IOException, + NoSuchAlgorithmException { + super(os, new Deflater(Deflater.BEST_COMPRESSION)); + } + + /** + * Flush the given stream, preferring Java7 FLUSH_SYNC if available. + * + * @throws IOException + * In case of a lowlevel exception. + */ + @Override + public void flush() throws IOException { + if (!SUPPORTED) { + super.flush(); + return; + } + try { + int count = 0; + do { + count = (Integer) method.invoke(def, buf, 0, buf.length, 3); + if (count > 0) { + out.write(buf, 0, count); + } + } while (count > 0); + } catch (IllegalArgumentException e) { + throw new IOException("Can't flush"); + } catch (IllegalAccessException e) { + throw new IOException("Can't flush"); + } catch (InvocationTargetException e) { + throw new IOException("Can't flush"); + } + super.flush(); + } + +} diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java new file mode 100644 index 00000000..4e11ee2c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -0,0 +1,148 @@ +package eu.siacs.conversations.xml; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import eu.siacs.conversations.utils.XmlHelper; + +public class Element { + protected String name; + protected Hashtable<String, String> attributes = new Hashtable<String, String>(); + protected String content; + protected List<Element> children = new ArrayList<Element>(); + + public Element(String name) { + this.name = name; + } + + public Element addChild(Element child) { + this.content = null; + children.add(child); + return child; + } + + public Element addChild(String name) { + this.content = null; + Element child = new Element(name); + children.add(child); + return child; + } + + public Element addChild(String name, String xmlns) { + this.content = null; + Element child = new Element(name); + child.setAttribute("xmlns", xmlns); + children.add(child); + return child; + } + + public Element setContent(String content) { + this.content = content; + this.children.clear(); + return this; + } + + public Element findChild(String name) { + for (Element child : this.children) { + if (child.getName().equals(name)) { + return child; + } + } + return null; + } + + public Element findChild(String name, String xmlns) { + for (Element child : this.children) { + if (child.getName().equals(name) + && (child.getAttribute("xmlns").equals(xmlns))) { + return child; + } + } + return null; + } + + public boolean hasChild(String name) { + return findChild(name) != null; + } + + public boolean hasChild(String name, String xmlns) { + return findChild(name, xmlns) != null; + } + + public List<Element> getChildren() { + return this.children; + } + + public Element setChildren(List<Element> children) { + this.children = children; + return this; + } + + public String getContent() { + return content; + } + + public Element setAttribute(String name, String value) { + if (name != null && value != null) { + this.attributes.put(name, value); + } + return this; + } + + public Element setAttributes(Hashtable<String, String> attributes) { + this.attributes = attributes; + return this; + } + + public String getAttribute(String name) { + if (this.attributes.containsKey(name)) { + return this.attributes.get(name); + } else { + return null; + } + } + + public Hashtable<String, String> getAttributes() { + return this.attributes; + } + + public String toString() { + StringBuilder elementOutput = new StringBuilder(); + if ((content == null) && (children.size() == 0)) { + Tag emptyTag = Tag.empty(name); + emptyTag.setAtttributes(this.attributes); + elementOutput.append(emptyTag.toString()); + } else { + Tag startTag = Tag.start(name); + startTag.setAtttributes(this.attributes); + elementOutput.append(startTag); + if (content != null) { + elementOutput.append(XmlHelper.encodeEntities(content)); + } else { + for (Element child : children) { + elementOutput.append(child.toString()); + } + } + Tag endTag = Tag.end(name); + elementOutput.append(endTag); + } + return elementOutput.toString(); + } + + public String getName() { + return name; + } + + public void clearChildren() { + this.children.clear(); + } + + public void setAttribute(String name, long value) { + this.setAttribute(name, Long.toString(value)); + } + + public void setAttribute(String name, int value) { + this.setAttribute(name, Integer.toString(value)); + } +} diff --git a/src/main/java/eu/siacs/conversations/xml/Tag.java b/src/main/java/eu/siacs/conversations/xml/Tag.java new file mode 100644 index 00000000..b9ef979f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/Tag.java @@ -0,0 +1,104 @@ +package eu.siacs.conversations.xml; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; +import java.util.Set; + +import eu.siacs.conversations.utils.XmlHelper; + +public class Tag { + public static final int NO = -1; + public static final int START = 0; + public static final int END = 1; + public static final int EMPTY = 2; + + protected int type; + protected String name; + protected Hashtable<String, String> attributes = new Hashtable<String, String>(); + + protected Tag(int type, String name) { + this.type = type; + this.name = name; + } + + public static Tag no(String text) { + return new Tag(NO, text); + } + + public static Tag start(String name) { + return new Tag(START, name); + } + + public static Tag end(String name) { + return new Tag(END, name); + } + + public static Tag empty(String name) { + return new Tag(EMPTY, name); + } + + public String getName() { + return name; + } + + public String getAttribute(String attrName) { + return this.attributes.get(attrName); + } + + public Tag setAttribute(String attrName, String attrValue) { + this.attributes.put(attrName, attrValue); + return this; + } + + public Tag setAtttributes(Hashtable<String, String> attributes) { + this.attributes = attributes; + return this; + } + + public boolean isStart(String needle) { + if (needle == null) + return false; + return (this.type == START) && (needle.equals(this.name)); + } + + public boolean isEnd(String needle) { + if (needle == null) + return false; + return (this.type == END) && (needle.equals(this.name)); + } + + public boolean isNo() { + return (this.type == NO); + } + + public String toString() { + StringBuilder tagOutput = new StringBuilder(); + tagOutput.append('<'); + if (type == END) { + tagOutput.append('/'); + } + tagOutput.append(name); + if (type != END) { + Set<Entry<String, String>> attributeSet = attributes.entrySet(); + Iterator<Entry<String, String>> it = attributeSet.iterator(); + while (it.hasNext()) { + Entry<String, String> entry = it.next(); + tagOutput.append(' '); + tagOutput.append(entry.getKey()); + tagOutput.append("=\""); + tagOutput.append(XmlHelper.encodeEntities(entry.getValue())); + tagOutput.append('"'); + } + } + if (type == EMPTY) { + tagOutput.append('/'); + } + tagOutput.append('>'); + return tagOutput.toString(); + } + + public Hashtable<String, String> getAttributes() { + return this.attributes; + } +} diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java new file mode 100644 index 00000000..f11c1846 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -0,0 +1,114 @@ +package eu.siacs.conversations.xml; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.concurrent.LinkedBlockingQueue; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class TagWriter { + + private OutputStream plainOutputStream; + private OutputStreamWriter outputStream; + private boolean finshed = false; + private LinkedBlockingQueue<AbstractStanza> writeQueue = new LinkedBlockingQueue<AbstractStanza>(); + private Thread asyncStanzaWriter = new Thread() { + private boolean shouldStop = false; + + @Override + public void run() { + while (!shouldStop) { + if ((finshed) && (writeQueue.size() == 0)) { + return; + } + try { + AbstractStanza output = writeQueue.take(); + if (outputStream == null) { + shouldStop = true; + } else { + outputStream.write(output.toString()); + outputStream.flush(); + } + } catch (IOException e) { + shouldStop = true; + } catch (InterruptedException e) { + shouldStop = true; + } + } + } + }; + + public TagWriter() { + } + + public void setOutputStream(OutputStream out) throws IOException { + if (out == null) { + throw new IOException(); + } + this.plainOutputStream = out; + this.outputStream = new OutputStreamWriter(out); + } + + public OutputStream getOutputStream() throws IOException { + if (this.plainOutputStream == null) { + throw new IOException(); + } + return this.plainOutputStream; + } + + public TagWriter beginDocument() throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write("<?xml version='1.0'?>"); + outputStream.flush(); + return this; + } + + public TagWriter writeTag(Tag tag) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(tag.toString()); + outputStream.flush(); + return this; + } + + public TagWriter writeElement(Element element) throws IOException { + if (outputStream == null) { + throw new IOException("output stream was null"); + } + outputStream.write(element.toString()); + outputStream.flush(); + return this; + } + + public TagWriter writeStanzaAsync(AbstractStanza stanza) { + if (finshed) { + return this; + } else { + if (!asyncStanzaWriter.isAlive()) { + try { + asyncStanzaWriter.start(); + } catch (IllegalThreadStateException e) { + // already started + } + } + writeQueue.add(stanza); + return this; + } + } + + public void finish() { + this.finshed = true; + } + + public boolean finished() { + return (this.writeQueue.size() == 0); + } + + public boolean isActive() { + return outputStream != null; + } +} diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java new file mode 100644 index 00000000..52d3d46a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -0,0 +1,141 @@ +package eu.siacs.conversations.xml; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import eu.siacs.conversations.Config; + +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.util.Log; +import android.util.Xml; + +public class XmlReader { + private XmlPullParser parser; + private PowerManager.WakeLock wakeLock; + private InputStream is; + + public XmlReader(WakeLock wakeLock) { + this.parser = Xml.newPullParser(); + try { + this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, + true); + } catch (XmlPullParserException e) { + Log.d(Config.LOGTAG, "error setting namespace feature on parser"); + } + this.wakeLock = wakeLock; + } + + public void setInputStream(InputStream inputStream) throws IOException { + if (inputStream == null) { + throw new IOException(); + } + this.is = inputStream; + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + throw new IOException("error resetting parser"); + } + } + + public InputStream getInputStream() throws IOException { + if (this.is == null) { + throw new IOException(); + } + return is; + } + + public void reset() throws IOException { + if (this.is == null) { + throw new IOException(); + } + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + throw new IOException("error resetting parser"); + } + } + + public Tag readTag() throws XmlPullParserException, IOException { + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + try { + while (this.is != null + && parser.next() != XmlPullParser.END_DOCUMENT) { + wakeLock.acquire(); + if (parser.getEventType() == XmlPullParser.START_TAG) { + Tag tag = Tag.start(parser.getName()); + for (int i = 0; i < parser.getAttributeCount(); ++i) { + tag.setAttribute(parser.getAttributeName(i), + parser.getAttributeValue(i)); + } + String xmlns = parser.getNamespace(); + if (xmlns != null) { + tag.setAttribute("xmlns", xmlns); + } + return tag; + } else if (parser.getEventType() == XmlPullParser.END_TAG) { + Tag tag = Tag.end(parser.getName()); + return tag; + } else if (parser.getEventType() == XmlPullParser.TEXT) { + Tag tag = Tag.no(parser.getText()); + return tag; + } + } + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + } catch (ArrayIndexOutOfBoundsException e) { + throw new IOException( + "xml parser mishandled ArrayIndexOufOfBounds", e); + } catch (StringIndexOutOfBoundsException e) { + throw new IOException( + "xml parser mishandled StringIndexOufOfBounds", e); + } catch (NullPointerException e) { + throw new IOException("xml parser mishandled NullPointerException", + e); + } catch (IndexOutOfBoundsException e) { + throw new IOException("xml parser mishandled IndexOutOfBound", e); + } + return null; + } + + public Element readElement(Tag currentTag) throws XmlPullParserException, + IOException { + Element element = new Element(currentTag.getName()); + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + if (nextTag.isNo()) { + element.setContent(nextTag.getName()); + nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + } + while (!nextTag.isEnd(element.getName())) { + if (!nextTag.isNo()) { + Element child = this.readElement(nextTag); + element.addChild(child); + } + nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("unterupted mid tag"); + } + } + return element; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java new file mode 100644 index 00000000..f09cf33d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnBindListener.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnBindListener { + public void onBind(Account account); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java new file mode 100644 index 00000000..849e8e76 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Contact; + +public interface OnContactStatusChanged { + public void onContactStatusChanged(Contact contact, boolean online); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java new file mode 100644 index 00000000..a4cff986 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public interface OnIqPacketReceived extends PacketReceived { + public void onIqPacketReceived(Account account, IqPacket packet); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java new file mode 100644 index 00000000..5f670d93 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessageAcknowledged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnMessageAcknowledged { + public void onMessageAcknowledged(Account account, String id); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java new file mode 100644 index 00000000..325e945f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +public interface OnMessagePacketReceived extends PacketReceived { + public void onMessagePacketReceived(Account account, MessagePacket packet); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java new file mode 100644 index 00000000..95c1acfc --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; + +public interface OnPresencePacketReceived extends PacketReceived { + public void onPresencePacketReceived(Account account, PresencePacket packet); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java new file mode 100644 index 00000000..ad1d98cb --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnStatusChanged.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnStatusChanged { + public void onStatusChanged(Account account); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java new file mode 100644 index 00000000..d4502d73 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xmpp; + +public abstract interface PacketReceived { + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java new file mode 100644 index 00000000..39dcb362 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -0,0 +1,1129 @@ +package eu.siacs.conversations.xmpp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import javax.net.ssl.X509TrustManager; + +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.xmlpull.v1.XmlPullParserException; + +import de.duenndns.ssl.MemorizingTrustManager; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Parcelable; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.util.Log; +import android.util.SparseArray; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.DNSHelper; +import eu.siacs.conversations.utils.zlib.ZLibOutputStream; +import eu.siacs.conversations.utils.zlib.ZLibInputStream; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Tag; +import eu.siacs.conversations.xml.TagWriter; +import eu.siacs.conversations.xml.XmlReader; +import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket; +import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; +import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket; + +public class XmppConnection implements Runnable { + + protected Account account; + + private WakeLock wakeLock; + + private SecureRandom mRandom; + + private Socket socket; + private XmlReader tagReader; + private TagWriter tagWriter; + + private Features features = new Features(this); + + private boolean shouldBind = true; + private boolean shouldAuthenticate = true; + private Element streamFeatures; + private HashMap<String, List<String>> disco = new HashMap<String, List<String>>(); + + private String streamId = null; + private int smVersion = 3; + private SparseArray<String> messageReceipts = new SparseArray<String>(); + + private boolean usingCompression = false; + private boolean usingEncryption = false; + + private int stanzasReceived = 0; + private int stanzasSent = 0; + + private long lastPaketReceived = 0; + private long lastPingSent = 0; + private long lastConnect = 0; + private long lastSessionStarted = 0; + + private int attempt = 0; + + private static final int PACKET_IQ = 0; + private static final int PACKET_MESSAGE = 1; + private static final int PACKET_PRESENCE = 2; + + private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>(); + private OnPresencePacketReceived presenceListener = null; + private OnJinglePacketReceived jingleListener = null; + private OnIqPacketReceived unregisteredIqListener = null; + private OnMessagePacketReceived messageListener = null; + private OnStatusChanged statusListener = null; + private OnBindListener bindListener = null; + private OnMessageAcknowledged acknowledgedListener = null; + private MemorizingTrustManager mMemorizingTrustManager; + private final Context applicationContext; + + public XmppConnection(Account account, XmppConnectionService service) { + this.mRandom = service.getRNG(); + this.mMemorizingTrustManager = service.getMemorizingTrustManager(); + this.account = account; + this.wakeLock = service.getPowerManager().newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, account.getJid()); + tagWriter = new TagWriter(); + applicationContext = service.getApplicationContext(); + } + + protected void changeStatus(int nextStatus) { + if (account.getStatus() != nextStatus) { + if ((nextStatus == Account.STATUS_OFFLINE) + && (account.getStatus() != Account.STATUS_CONNECTING) + && (account.getStatus() != Account.STATUS_ONLINE) + && (account.getStatus() != Account.STATUS_DISABLED)) { + return; + } + if (nextStatus == Account.STATUS_ONLINE) { + this.attempt = 0; + } + account.setStatus(nextStatus); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + protected void connect() { + Log.d(Config.LOGTAG, account.getJid() + ": connecting"); + usingCompression = false; + usingEncryption = false; + lastConnect = SystemClock.elapsedRealtime(); + lastPingSent = SystemClock.elapsedRealtime(); + this.attempt++; + try { + shouldAuthenticate = shouldBind = !account + .isOptionSet(Account.OPTION_REGISTER); + tagReader = new XmlReader(wakeLock); + tagWriter = new TagWriter(); + packetCallbacks.clear(); + this.changeStatus(Account.STATUS_CONNECTING); + Bundle result = DNSHelper.getSRVRecord(account.getServer()); + ArrayList<Parcelable> values = result.getParcelableArrayList("values"); + if ("timeout".equals(result.getString("error"))) { + Log.d(Config.LOGTAG, account.getJid() + ": dns timeout"); + this.changeStatus(Account.STATUS_OFFLINE); + return; + } else if (values != null) { + int i = 0; + boolean socketError = true; + while (socketError && values.size() > i) { + Bundle namePort = (Bundle) values.get(i); + try { + String srvRecordServer = namePort.getString("name"); + int srvRecordPort = namePort.getInt("port"); + String srvIpServer = namePort.getString("ipv4"); + InetSocketAddress addr; + if (srvIpServer!=null) { + addr = new InetSocketAddress(srvIpServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid() + + ": using values from dns " + srvRecordServer + + "[" + srvIpServer + "]:" + srvRecordPort); + } else { + addr = new InetSocketAddress(srvRecordServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid() + + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + } + socket = new Socket(); + socket.connect(addr, 20000); + socketError = false; + } catch (UnknownHostException e) { + i++; + } catch (IOException e) { + i++; + } + } + if (socketError) { + throw new IOException(); + } + } else if (result.containsKey("error") + && "nosrv".equals(result.getString("error", null))) { + socket = new Socket(account.getServer(), 5222); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": timeout in DNS resolution"); + changeStatus(Account.STATUS_OFFLINE); + return; + } + OutputStream out = socket.getOutputStream(); + tagWriter.setOutputStream(out); + InputStream in = socket.getInputStream(); + tagReader.setInputStream(in); + tagWriter.beginDocument(); + sendStartStream(); + Tag nextTag; + while ((nextTag = tagReader.readTag()) != null) { + if (nextTag.isStart("stream")) { + processStream(nextTag); + break; + } else { + Log.d(Config.LOGTAG, + "found unexpected tag: " + nextTag.getName()); + return; + } + } + if (socket.isConnected()) { + socket.close(); + } + } catch (UnknownHostException e) { + this.changeStatus(Account.STATUS_SERVER_NOT_FOUND); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (IOException e) { + this.changeStatus(Account.STATUS_OFFLINE); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (NoSuchAlgorithmException e) { + this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, "compression exception " + e.getMessage()); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } catch (XmlPullParserException e) { + this.changeStatus(Account.STATUS_OFFLINE); + Log.d(Config.LOGTAG, "xml exception " + e.getMessage()); + if (wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (RuntimeException re) { + } + } + return; + } + + } + + @Override + public void run() { + connect(); + } + + private void processStream(Tag currentTag) throws XmlPullParserException, + IOException, NoSuchAlgorithmException { + Tag nextTag = tagReader.readTag(); + while ((nextTag != null) && (!nextTag.isEnd("stream"))) { + if (nextTag.isStart("error")) { + processStreamError(nextTag); + } else if (nextTag.isStart("features")) { + processStreamFeatures(nextTag); + } else if (nextTag.isStart("proceed")) { + switchOverToTls(nextTag); + } else if (nextTag.isStart("compressed")) { + switchOverToZLib(nextTag); + } else if (nextTag.isStart("success")) { + Log.d(Config.LOGTAG, account.getJid() + ": logged in"); + tagReader.readTag(); + tagReader.reset(); + sendStartStream(); + processStream(tagReader.readTag()); + break; + } else if (nextTag.isStart("failure")) { + tagReader.readElement(nextTag); + changeStatus(Account.STATUS_UNAUTHORIZED); + } else if (nextTag.isStart("challenge")) { + String challange = tagReader.readElement(nextTag).getContent(); + Element response = new Element("response"); + response.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-sasl"); + response.setContent(CryptoHelper.saslDigestMd5(account, + challange, mRandom)); + tagWriter.writeElement(response); + } else if (nextTag.isStart("enabled")) { + Element enabled = tagReader.readElement(nextTag); + if ("true".equals(enabled.getAttribute("resume"))) { + this.streamId = enabled.getAttribute("id"); + Log.d(Config.LOGTAG, account.getJid() + + ": stream managment(" + smVersion + + ") enabled (resumable)"); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": stream managment(" + smVersion + ") enabled"); + } + this.lastSessionStarted = SystemClock.elapsedRealtime(); + this.stanzasReceived = 0; + RequestPacket r = new RequestPacket(smVersion); + tagWriter.writeStanzaAsync(r); + } else if (nextTag.isStart("resumed")) { + lastPaketReceived = SystemClock.elapsedRealtime(); + Element resumed = tagReader.readElement(nextTag); + String h = resumed.getAttribute("h"); + try { + int serverCount = Integer.parseInt(h); + if (serverCount != stanzasSent) { + Log.d(Config.LOGTAG, account.getJid() + + ": session resumed with lost packages"); + stanzasSent = serverCount; + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": session resumed"); + } + if (acknowledgedListener != null) { + for (int i = 0; i < messageReceipts.size(); ++i) { + if (serverCount >= messageReceipts.keyAt(i)) { + acknowledgedListener.onMessageAcknowledged( + account, messageReceipts.valueAt(i)); + } + } + } + messageReceipts.clear(); + } catch (NumberFormatException e) { + + } + sendInitialPing(); + + } else if (nextTag.isStart("r")) { + tagReader.readElement(nextTag); + AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + tagWriter.writeStanzaAsync(ack); + } else if (nextTag.isStart("a")) { + Element ack = tagReader.readElement(nextTag); + lastPaketReceived = SystemClock.elapsedRealtime(); + int serverSequence = Integer.parseInt(ack.getAttribute("h")); + String msgId = this.messageReceipts.get(serverSequence); + if (msgId != null) { + if (this.acknowledgedListener != null) { + this.acknowledgedListener.onMessageAcknowledged( + account, msgId); + } + this.messageReceipts.remove(serverSequence); + } + } else if (nextTag.isStart("failed")) { + tagReader.readElement(nextTag); + Log.d(Config.LOGTAG, account.getJid() + ": resumption failed"); + streamId = null; + if (account.getStatus() != Account.STATUS_ONLINE) { + sendBindRequest(); + } + } else if (nextTag.isStart("iq")) { + processIq(nextTag); + } else if (nextTag.isStart("message")) { + processMessage(nextTag); + } else if (nextTag.isStart("presence")) { + processPresence(nextTag); + } + nextTag = tagReader.readTag(); + } + if (account.getStatus() == Account.STATUS_ONLINE) { + account.setStatus(Account.STATUS_OFFLINE); + if (statusListener != null) { + statusListener.onStatusChanged(account); + } + } + } + + private void sendInitialPing() { + Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping"); + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setFrom(account.getFullJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, account.getJid() + + ": online with resource " + account.getResource()); + changeStatus(Account.STATUS_ONLINE); + } + }); + } + + private Element processPacket(Tag currentTag, int packetType) + throws XmlPullParserException, IOException { + Element element; + switch (packetType) { + case PACKET_IQ: + element = new IqPacket(); + break; + case PACKET_MESSAGE: + element = new MessagePacket(); + break; + case PACKET_PRESENCE: + element = new PresencePacket(); + break; + default: + return null; + } + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = tagReader.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + while (!nextTag.isEnd(element.getName())) { + if (!nextTag.isNo()) { + Element child = tagReader.readElement(nextTag); + String type = currentTag.getAttribute("type"); + if (packetType == PACKET_IQ + && "jingle".equals(child.getName()) + && ("set".equalsIgnoreCase(type) || "get" + .equalsIgnoreCase(type))) { + element = new JinglePacket(); + element.setAttributes(currentTag.getAttributes()); + } + element.addChild(child); + } + nextTag = tagReader.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + } + ++stanzasReceived; + lastPaketReceived = SystemClock.elapsedRealtime(); + return element; + } + + private void processIq(Tag currentTag) throws XmlPullParserException, + IOException { + IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); + + if (packet.getId() == null) { + return; // an iq packet without id is definitely invalid + } + + if (packet instanceof JinglePacket) { + if (this.jingleListener != null) { + this.jingleListener.onJinglePacketReceived(account, + (JinglePacket) packet); + } + } else { + if (packetCallbacks.containsKey(packet.getId())) { + if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) { + ((OnIqPacketReceived) packetCallbacks.get(packet.getId())) + .onIqPacketReceived(account, packet); + } + + packetCallbacks.remove(packet.getId()); + } else if ((packet.getType() == IqPacket.TYPE_GET || packet + .getType() == IqPacket.TYPE_SET) + && this.unregisteredIqListener != null) { + this.unregisteredIqListener.onIqPacketReceived(account, packet); + } + } + } + + private void processMessage(Tag currentTag) throws XmlPullParserException, + IOException { + MessagePacket packet = (MessagePacket) processPacket(currentTag, + PACKET_MESSAGE); + String id = packet.getAttribute("id"); + if ((id != null) && (packetCallbacks.containsKey(id))) { + if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) { + ((OnMessagePacketReceived) packetCallbacks.get(id)) + .onMessagePacketReceived(account, packet); + } + packetCallbacks.remove(id); + } else if (this.messageListener != null) { + this.messageListener.onMessagePacketReceived(account, packet); + } + } + + private void processPresence(Tag currentTag) throws XmlPullParserException, + IOException { + PresencePacket packet = (PresencePacket) processPacket(currentTag, + PACKET_PRESENCE); + String id = packet.getAttribute("id"); + if ((id != null) && (packetCallbacks.containsKey(id))) { + if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) { + ((OnPresencePacketReceived) packetCallbacks.get(id)) + .onPresencePacketReceived(account, packet); + } + packetCallbacks.remove(id); + } else if (this.presenceListener != null) { + this.presenceListener.onPresencePacketReceived(account, packet); + } + } + + private void sendCompressionZlib() throws IOException { + Element compress = new Element("compress"); + compress.setAttribute("xmlns", "http://jabber.org/protocol/compress"); + compress.addChild("method").setContent("zlib"); + tagWriter.writeElement(compress); + } + + private void switchOverToZLib(Tag currentTag) + throws XmlPullParserException, IOException, + NoSuchAlgorithmException { + tagReader.readTag(); // read tag close + tagWriter.setOutputStream(new ZLibOutputStream(tagWriter + .getOutputStream())); + tagReader + .setInputStream(new ZLibInputStream(tagReader.getInputStream())); + + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid() + ": compression enabled"); + usingCompression = true; + processStream(tagReader.readTag()); + } + + private void sendStartTLS() throws IOException { + Tag startTLS = Tag.empty("starttls"); + startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); + tagWriter.writeTag(startTLS); + } + + private SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(applicationContext); + } + + private boolean enableLegacySSL() { + return getPreferences().getBoolean("enable_legacy_ssl", false); + } + + private void switchOverToTls(Tag currentTag) throws XmlPullParserException, + IOException { + tagReader.readTag(); + try { + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, + new X509TrustManager[] { this.mMemorizingTrustManager }, + mRandom); + SSLSocketFactory factory = sc.getSocketFactory(); + + HostnameVerifier verifier = this.mMemorizingTrustManager + .wrapHostnameVerifier(new StrictHostnameVerifier()); + SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, + socket.getInetAddress().getHostAddress(), socket.getPort(), + true); + + // Support all protocols except legacy SSL. + // The min SDK version prevents us having to worry about SSLv2. In + // future, this may be + // true of SSLv3 as well. + final String[] supportProtocols; + if (enableLegacySSL()) { + supportProtocols = sslSocket.getSupportedProtocols(); + } else { + final List<String> supportedProtocols = new LinkedList<String>( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = new String[supportedProtocols.size()]; + supportedProtocols.toArray(supportProtocols); + } + sslSocket.setEnabledProtocols(supportProtocols); + + if (verifier != null + && !verifier.verify(account.getServer(), + sslSocket.getSession())) { + Log.d(Config.LOGTAG, account.getJid() + + ": host mismatch in TLS connection"); + sslSocket.close(); + throw new IOException(); + } + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid() + + ": TLS connection established"); + usingEncryption = true; + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (NoSuchAlgorithmException e1) { + e1.printStackTrace(); + } catch (KeyManagementException e) { + e.printStackTrace(); + } + } + + private void sendSaslAuthPlain() throws IOException { + String saslString = CryptoHelper.saslPlain(account.getUsername(), + account.getPassword()); + Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.setAttribute("mechanism", "PLAIN"); + auth.setContent(saslString); + tagWriter.writeElement(auth); + } + + private void sendSaslAuthDigestMd5() throws IOException { + Element auth = new Element("auth"); + auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); + auth.setAttribute("mechanism", "DIGEST-MD5"); + tagWriter.writeElement(auth); + } + + private void processStreamFeatures(Tag currentTag) + throws XmlPullParserException, IOException { + this.streamFeatures = tagReader.readElement(currentTag); + if (this.streamFeatures.hasChild("starttls") && !usingEncryption) { + sendStartTLS(); + } else if (compressionAvailable()) { + sendCompressionZlib(); + } else if (this.streamFeatures.hasChild("register") + && account.isOptionSet(Account.OPTION_REGISTER) + && usingEncryption) { + sendRegistryRequest(); + } else if (!this.streamFeatures.hasChild("register") + && account.isOptionSet(Account.OPTION_REGISTER)) { + changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED); + disconnect(true); + } else if (this.streamFeatures.hasChild("mechanisms") + && shouldAuthenticate && usingEncryption) { + List<String> mechanisms = extractMechanisms(streamFeatures + .findChild("mechanisms")); + if (mechanisms.contains("PLAIN")) { + sendSaslAuthPlain(); + } else if (mechanisms.contains("DIGEST-MD5")) { + sendSaslAuthDigestMd5(); + } + } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + + smVersion) + && streamId != null) { + ResumePacket resume = new ResumePacket(this.streamId, + stanzasReceived, smVersion); + this.tagWriter.writeStanzaAsync(resume); + } else if (this.streamFeatures.hasChild("bind") && shouldBind) { + sendBindRequest(); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": incompatible server. disconnecting"); + disconnect(true); + } + } + + private boolean compressionAvailable() { + if (!this.streamFeatures.hasChild("compression", + "http://jabber.org/features/compress")) + return false; + if (!ZLibOutputStream.SUPPORTED) + return false; + if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) + return false; + + Element compression = this.streamFeatures.findChild("compression", + "http://jabber.org/features/compress"); + for (Element child : compression.getChildren()) { + if (!"method".equals(child.getName())) + continue; + + if ("zlib".equalsIgnoreCase(child.getContent())) { + return true; + } + } + return false; + } + + private List<String> extractMechanisms(Element stream) { + ArrayList<String> mechanisms = new ArrayList<String>(stream + .getChildren().size()); + for (Element child : stream.getChildren()) { + mechanisms.add(child.getContent()); + } + return mechanisms; + } + + private void sendRegistryRequest() { + IqPacket register = new IqPacket(IqPacket.TYPE_GET); + register.query("jabber:iq:register"); + register.setTo(account.getServer()); + sendIqPacket(register, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element instructions = packet.query().findChild("instructions"); + if (packet.query().hasChild("username") + && (packet.query().hasChild("password"))) { + IqPacket register = new IqPacket(IqPacket.TYPE_SET); + Element username = new Element("username") + .setContent(account.getUsername()); + Element password = new Element("password") + .setContent(account.getPassword()); + register.query("jabber:iq:register").addChild(username); + register.query().addChild(password); + sendIqPacket(register, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.setOption(Account.OPTION_REGISTER, + false); + changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL); + } else if (packet.hasChild("error") + && (packet.findChild("error") + .hasChild("conflict"))) { + changeStatus(Account.STATUS_REGISTRATION_CONFLICT); + } else { + changeStatus(Account.STATUS_REGISTRATION_FAILED); + Log.d(Config.LOGTAG, packet.toString()); + } + disconnect(true); + } + }); + } else { + changeStatus(Account.STATUS_REGISTRATION_FAILED); + disconnect(true); + Log.d(Config.LOGTAG, account.getJid() + + ": could not register. instructions are" + + instructions.getContent()); + } + } + }); + } + + private void sendBindRequest() throws IOException { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") + .addChild("resource").setContent(account.getResource()); + this.sendUnboundIqPacket(iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element bind = packet.findChild("bind"); + if (bind != null) { + Element jid = bind.findChild("jid"); + if (jid != null && jid.getContent() != null) { + account.setResource(jid.getContent().split("/", 2)[1]); + if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { + smVersion = 3; + EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + stanzasSent = 0; + messageReceipts.clear(); + } else if (streamFeatures.hasChild("sm", + "urn:xmpp:sm:2")) { + smVersion = 2; + EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + stanzasSent = 0; + messageReceipts.clear(); + } + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + if (bindListener != null) { + bindListener.onBind(account); + } + sendInitialPing(); + } else { + disconnect(true); + } + } else { + disconnect(true); + } + } + }); + if (this.streamFeatures.hasChild("session")) { + Log.d(Config.LOGTAG, account.getJid() + + ": sending deprecated session"); + IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); + startSession.addChild("session", + "urn:ietf:params:xml:ns:xmpp-session"); + this.sendUnboundIqPacket(startSession, null); + } + } + + private void sendServiceDiscoveryInfo(final String server) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server); + iq.query("http://jabber.org/protocol/disco#info"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + List<Element> elements = packet.query().getChildren(); + List<String> features = new ArrayList<String>(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("feature")) { + features.add(elements.get(i).getAttribute("var")); + } + } + disco.put(server, features); + + if (account.getServer().equals(server)) { + enableAdvancedStreamFeatures(); + } + } + }); + } + + private void enableAdvancedStreamFeatures() { + if (getFeatures().carbons()) { + sendEnableCarbons(); + } + } + + private void sendServiceDiscoveryItems(final String server) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(server); + iq.query("http://jabber.org/protocol/disco#items"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + List<Element> elements = packet.query().getChildren(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("item")) { + String jid = elements.get(i).getAttribute("jid"); + sendServiceDiscoveryInfo(jid); + } + } + } + }); + } + + private void sendEnableCarbons() { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.addChild("enable", "urn:xmpp:carbons:2"); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (!packet.hasChild("error")) { + Log.d(Config.LOGTAG, account.getJid() + + ": successfully enabled carbons"); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": error enableing carbons " + packet.toString()); + } + } + }); + } + + private void processStreamError(Tag currentTag) + throws XmlPullParserException, IOException { + Element streamError = tagReader.readElement(currentTag); + if (streamError != null && streamError.hasChild("conflict")) { + String resource = account.getResource().split("\\.")[0]; + account.setResource(resource + "." + nextRandomId()); + Log.d(Config.LOGTAG, + account.getJid() + ": switching resource due to conflict (" + + account.getResource() + ")"); + } + } + + private void sendStartStream() throws IOException { + Tag stream = Tag.start("stream:stream"); + stream.setAttribute("from", account.getJid()); + stream.setAttribute("to", account.getServer()); + stream.setAttribute("version", "1.0"); + stream.setAttribute("xml:lang", "en"); + stream.setAttribute("xmlns", "jabber:client"); + stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); + tagWriter.writeTag(stream); + } + + private String nextRandomId() { + return new BigInteger(50, mRandom).toString(32); + } + + public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { + if (packet.getId() == null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } + packet.setFrom(account.getFullJid()); + this.sendPacket(packet, callback); + } + + public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) { + if (packet.getId() == null) { + String id = nextRandomId(); + packet.setAttribute("id", id); + } + this.sendPacket(packet, callback); + } + + public void sendMessagePacket(MessagePacket packet) { + this.sendPacket(packet, null); + } + + public void sendPresencePacket(PresencePacket packet) { + this.sendPacket(packet, null); + } + + private synchronized void sendPacket(final AbstractStanza packet, + PacketReceived callback) { + if (packet.getName().equals("iq") || packet.getName().equals("message") + || packet.getName().equals("presence")) { + ++stanzasSent; + } + tagWriter.writeStanzaAsync(packet); + if (packet instanceof MessagePacket && packet.getId() != null + && this.streamId != null) { + Log.d(Config.LOGTAG, "request delivery report for stanza " + + stanzasSent); + this.messageReceipts.put(stanzasSent, packet.getId()); + tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion)); + } + if (callback != null) { + if (packet.getId() == null) { + packet.setId(nextRandomId()); + } + packetCallbacks.put(packet.getId(), callback); + } + } + + public void sendPing() { + if (streamFeatures.hasChild("sm")) { + tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } else { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setFrom(account.getFullJid()); + iq.addChild("ping", "urn:xmpp:ping"); + this.sendIqPacket(iq, null); + } + this.lastPingSent = SystemClock.elapsedRealtime(); + } + + public void setOnMessagePacketReceivedListener( + OnMessagePacketReceived listener) { + this.messageListener = listener; + } + + public void setOnUnregisteredIqPacketReceivedListener( + OnIqPacketReceived listener) { + this.unregisteredIqListener = listener; + } + + public void setOnPresencePacketReceivedListener( + OnPresencePacketReceived listener) { + this.presenceListener = listener; + } + + public void setOnJinglePacketReceivedListener( + OnJinglePacketReceived listener) { + this.jingleListener = listener; + } + + public void setOnStatusChangedListener(OnStatusChanged listener) { + this.statusListener = listener; + } + + public void setOnBindListener(OnBindListener listener) { + this.bindListener = listener; + } + + public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) { + this.acknowledgedListener = listener; + } + + public void disconnect(boolean force) { + Log.d(Config.LOGTAG, account.getJid() + ": disconnecting"); + try { + if (force) { + socket.close(); + return; + } + new Thread(new Runnable() { + + @Override + public void run() { + if (tagWriter.isActive()) { + tagWriter.finish(); + try { + while (!tagWriter.finished()) { + Log.d(Config.LOGTAG, "not yet finished"); + Thread.sleep(100); + } + tagWriter.writeTag(Tag.end("stream:stream")); + socket.close(); + } catch (IOException e) { + Log.d(Config.LOGTAG, + "io exception during disconnect"); + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "interrupted"); + } + } + } + }).start(); + } catch (IOException e) { + Log.d(Config.LOGTAG, "io exception during disconnect"); + } + } + + public List<String> findDiscoItemsByFeature(String feature) { + List<String> items = new ArrayList<String>(); + for (Entry<String, List<String>> cursor : disco.entrySet()) { + if (cursor.getValue().contains(feature)) { + items.add(cursor.getKey()); + } + } + return items; + } + + public String findDiscoItemByFeature(String feature) { + List<String> items = findDiscoItemsByFeature(feature); + if (items.size() >= 1) { + return items.get(0); + } + return null; + } + + public void r() { + this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); + } + + public String getMucServer() { + return findDiscoItemByFeature("http://jabber.org/protocol/muc"); + } + + public int getTimeToNextAttempt() { + int interval = (int) (25 * Math.pow(1.5, attempt)); + int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + return interval - secondsSinceLast; + } + + public int getAttempt() { + return this.attempt; + } + + public Features getFeatures() { + return this.features; + } + + public class Features { + XmppConnection connection; + + public Features(XmppConnection connection) { + this.connection = connection; + } + + private boolean hasDiscoFeature(String server, String feature) { + if (!connection.disco.containsKey(server)) { + return false; + } + return connection.disco.get(server).contains(feature); + } + + public boolean carbons() { + return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); + } + + public boolean sm() { + return streamId != null; + } + + public boolean csi() { + if (connection.streamFeatures == null) { + return false; + } else { + return connection.streamFeatures.hasChild("csi", + "urn:xmpp:csi:0"); + } + } + + public boolean pubsub() { + return hasDiscoFeature(account.getServer(), + "http://jabber.org/protocol/pubsub#publish"); + } + + public boolean rosterVersioning() { + if (connection.streamFeatures == null) { + return false; + } else { + return connection.streamFeatures.hasChild("ver"); + } + } + + public boolean streamhost() { + return connection + .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null; + } + + public boolean compression() { + return connection.usingCompression; + } + } + + public long getLastSessionEstablished() { + long diff; + if (this.lastSessionStarted == 0) { + diff = SystemClock.elapsedRealtime() - this.lastConnect; + } else { + diff = SystemClock.elapsedRealtime() - this.lastSessionStarted; + } + return System.currentTimeMillis() - diff; + } + + public long getLastConnect() { + return this.lastConnect; + } + + public long getLastPingSent() { + return this.lastPingSent; + } + + public long getLastPacketReceived() { + return this.lastPaketReceived; + } + + public void sendActive() { + this.sendPacket(new ActivePacket(), null); + } + + public void sendInactive() { + this.sendPacket(new InactivePacket(), null); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java new file mode 100644 index 00000000..3e7c7b68 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleCandidate.java @@ -0,0 +1,143 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.xml.Element; + +public class JingleCandidate { + + public static int TYPE_UNKNOWN; + public static int TYPE_DIRECT = 0; + public static int TYPE_PROXY = 1; + + private boolean ours; + private boolean usedByCounterpart = false; + private String cid; + private String host; + private int port; + private int type; + private String jid; + private int priority; + + public JingleCandidate(String cid, boolean ours) { + this.ours = ours; + this.cid = cid; + } + + public String getCid() { + return cid; + } + + public void setHost(String host) { + this.host = host; + } + + public String getHost() { + return this.host; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getJid() { + return this.jid; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return this.port; + } + + public void setType(int type) { + this.type = type; + } + + public void setType(String type) { + if ("proxy".equals(type)) { + this.type = TYPE_PROXY; + } else if ("direct".equals(type)) { + this.type = TYPE_DIRECT; + } else { + this.type = TYPE_UNKNOWN; + } + } + + public void setPriority(int i) { + this.priority = i; + } + + public int getPriority() { + return this.priority; + } + + public boolean equals(JingleCandidate other) { + return this.getCid().equals(other.getCid()); + } + + public boolean equalValues(JingleCandidate other) { + return other.getHost().equals(this.getHost()) + && (other.getPort() == this.getPort()); + } + + public boolean isOurs() { + return ours; + } + + public int getType() { + return this.type; + } + + public static List<JingleCandidate> parse(List<Element> canditates) { + List<JingleCandidate> parsedCandidates = new ArrayList<JingleCandidate>(); + for (Element c : canditates) { + parsedCandidates.add(JingleCandidate.parse(c)); + } + return parsedCandidates; + } + + public static JingleCandidate parse(Element candidate) { + JingleCandidate parsedCandidate = new JingleCandidate( + candidate.getAttribute("cid"), false); + parsedCandidate.setHost(candidate.getAttribute("host")); + parsedCandidate.setJid(candidate.getAttribute("jid")); + parsedCandidate.setType(candidate.getAttribute("type")); + parsedCandidate.setPriority(Integer.parseInt(candidate + .getAttribute("priority"))); + parsedCandidate + .setPort(Integer.parseInt(candidate.getAttribute("port"))); + return parsedCandidate; + } + + public Element toElement() { + Element element = new Element("candidate"); + element.setAttribute("cid", this.getCid()); + element.setAttribute("host", this.getHost()); + element.setAttribute("port", Integer.toString(this.getPort())); + element.setAttribute("jid", this.getJid()); + element.setAttribute("priority", Integer.toString(this.getPriority())); + if (this.getType() == TYPE_DIRECT) { + element.setAttribute("type", "direct"); + } else if (this.getType() == TYPE_PROXY) { + element.setAttribute("type", "proxy"); + } + return element; + } + + public void flagAsUsedByCounterpart() { + this.usedByCounterpart = true; + } + + public boolean isUsedByCounterpart() { + return this.usedByCounterpart; + } + + public String toString() { + return this.getHost() + ":" + this.getPort() + " (prio=" + + this.getPriority() + ")"; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java new file mode 100644 index 00000000..6b9ca9aa --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -0,0 +1,902 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleConnection implements Downloadable { + + private final String[] extensions = { "webp", "jpeg", "jpg", "png" }; + private final String[] cryptoExtensions = { "pgp", "gpg", "otr" }; + + private JingleConnectionManager mJingleConnectionManager; + private XmppConnectionService mXmppConnectionService; + + protected static final int JINGLE_STATUS_INITIATED = 0; + protected static final int JINGLE_STATUS_ACCEPTED = 1; + protected static final int JINGLE_STATUS_TERMINATED = 2; + protected static final int JINGLE_STATUS_CANCELED = 3; + protected static final int JINGLE_STATUS_FINISHED = 4; + protected static final int JINGLE_STATUS_TRANSMITTING = 5; + protected static final int JINGLE_STATUS_FAILED = 99; + + private int ibbBlockSize = 4096; + + private int mJingleStatus = -1; + private int mStatus = -1; + private Message message; + private String sessionId; + private Account account; + private String initiator; + private String responder; + private List<JingleCandidate> candidates = new ArrayList<JingleCandidate>(); + private ConcurrentHashMap<String, JingleSocks5Transport> connections = new ConcurrentHashMap<String, JingleSocks5Transport>(); + + private String transportId; + private Element fileOffer; + private DownloadableFile file = null; + + private String contentName; + private String contentCreator; + + private boolean receivedCandidate = false; + private boolean sentCandidate = false; + + private boolean acceptedAutomatically = false; + + private JingleTransport transport = null; + + private OnIqPacketReceived responseListener = new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + cancel(); + } + } + }; + + final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { + + @Override + public void onFileTransmitted(DownloadableFile file) { + if (responder.equals(account.getFullJid())) { + sendSuccess(); + if (acceptedAutomatically) { + message.markUnread(); + JingleConnection.this.mXmppConnectionService + .getNotificationService().push(message); + } + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(file.getAbsolutePath(), options); + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + message.setBody(Long.toString(file.getSize()) + '|' + + imageWidth + '|' + imageHeight); + mXmppConnectionService.databaseBackend.createMessage(message); + mXmppConnectionService.markMessage(message, + Message.STATUS_RECEIVED); + } + Log.d(Config.LOGTAG, + "sucessfully transmitted file:" + file.getAbsolutePath()); + if (message.getEncryption() != Message.ENCRYPTION_PGP) { + Intent intent = new Intent( + Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(file)); + mXmppConnectionService.sendBroadcast(intent); + } + } + + @Override + public void onFileTransferAborted() { + JingleConnection.this.sendCancel(); + JingleConnection.this.cancel(); + } + }; + + private OnProxyActivated onProxyActivated = new OnProxyActivated() { + + @Override + public void success() { + if (initiator.equals(account.getFullJid())) { + Log.d(Config.LOGTAG, "we were initiating. sending file"); + transport.send(file, onFileTransmissionSatusChanged); + } else { + transport.receive(file, onFileTransmissionSatusChanged); + Log.d(Config.LOGTAG, "we were responding. receiving file"); + } + } + + @Override + public void failed() { + Log.d(Config.LOGTAG, "proxy activation failed"); + } + }; + + public JingleConnection(JingleConnectionManager mJingleConnectionManager) { + this.mJingleConnectionManager = mJingleConnectionManager; + this.mXmppConnectionService = mJingleConnectionManager + .getXmppConnectionService(); + } + + public String getSessionId() { + return this.sessionId; + } + + public Account getAccount() { + return this.account; + } + + public String getCounterPart() { + return this.message.getCounterpart(); + } + + public void deliverPacket(JinglePacket packet) { + boolean returnResult = true; + if (packet.isAction("session-terminate")) { + Reason reason = packet.getReason(); + if (reason != null) { + if (reason.hasChild("cancel")) { + this.cancel(); + } else if (reason.hasChild("success")) { + this.receiveSuccess(); + } else { + this.cancel(); + } + } else { + this.cancel(); + } + } else if (packet.isAction("session-accept")) { + returnResult = receiveAccept(packet); + } else if (packet.isAction("transport-info")) { + returnResult = receiveTransportInfo(packet); + } else if (packet.isAction("transport-replace")) { + if (packet.getJingleContent().hasIbbTransport()) { + returnResult = this.receiveFallbackToIbb(packet); + } else { + returnResult = false; + Log.d(Config.LOGTAG, "trying to fallback to something unknown" + + packet.toString()); + } + } else if (packet.isAction("transport-accept")) { + returnResult = this.receiveTransportAccept(packet); + } else { + Log.d(Config.LOGTAG, "packet arrived in connection. action was " + + packet.getAction()); + returnResult = false; + } + IqPacket response; + if (returnResult) { + response = packet.generateRespone(IqPacket.TYPE_RESULT); + + } else { + response = packet.generateRespone(IqPacket.TYPE_ERROR); + } + account.getXmppConnection().sendIqPacket(response, null); + } + + public void init(Message message) { + this.contentCreator = "initiator"; + this.contentName = this.mJingleConnectionManager.nextRandomId(); + this.message = message; + this.account = message.getConversation().getAccount(); + this.initiator = this.account.getFullJid(); + this.responder = this.message.getCounterpart(); + this.sessionId = this.mJingleConnectionManager.nextRandomId(); + if (this.candidates.size() > 0) { + this.sendInitRequest(); + } else { + this.mJingleConnectionManager.getPrimaryCandidate(account, + new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, + final JingleCandidate candidate) { + if (success) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + JingleConnection.this, candidate); + connections.put(candidate.getCid(), + socksConnection); + socksConnection + .connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection to our own primary candidete failed"); + sendInitRequest(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "succesfully connected to our own primary candidate"); + mergeCandidate(candidate); + sendInitRequest(); + } + }); + mergeCandidate(candidate); + } else { + Log.d(Config.LOGTAG, + "no primary candidate of our own was found"); + sendInitRequest(); + } + } + }); + } + + } + + public void init(Account account, JinglePacket packet) { + this.mJingleStatus = JINGLE_STATUS_INITIATED; + Conversation conversation = this.mXmppConnectionService + .findOrCreateConversation(account, + packet.getFrom().split("/", 2)[0], false); + this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); + this.message.setStatus(Message.STATUS_RECEIVED); + this.message.setType(Message.TYPE_IMAGE); + this.mStatus = Downloadable.STATUS_OFFER; + this.message.setDownloadable(this); + String[] fromParts = packet.getFrom().split("/", 2); + this.message.setPresence(fromParts[1]); + this.account = account; + this.initiator = packet.getFrom(); + this.responder = this.account.getFullJid(); + this.sessionId = packet.getSessionId(); + Content content = packet.getJingleContent(); + this.contentCreator = content.getAttribute("creator"); + this.contentName = content.getAttribute("name"); + this.transportId = content.getTransportId(); + this.mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); + this.fileOffer = packet.getJingleContent().getFileOffer(); + if (fileOffer != null) { + Element fileSize = fileOffer.findChild("size"); + Element fileNameElement = fileOffer.findChild("name"); + if (fileNameElement != null) { + boolean supportedFile = false; + String[] filename = fileNameElement.getContent() + .toLowerCase(Locale.US).split("\\."); + if (Arrays.asList(this.extensions).contains( + filename[filename.length - 1])) { + supportedFile = true; + } else if (Arrays.asList(this.cryptoExtensions).contains( + filename[filename.length - 1])) { + if (filename.length == 3) { + if (Arrays.asList(this.extensions).contains( + filename[filename.length - 2])) { + supportedFile = true; + if (filename[filename.length - 1].equals("otr")) { + Log.d(Config.LOGTAG, "receiving otr file"); + this.message + .setEncryption(Message.ENCRYPTION_OTR); + } else { + this.message + .setEncryption(Message.ENCRYPTION_PGP); + } + } + } + } + if (supportedFile) { + long size = Long.parseLong(fileSize.getContent()); + message.setBody(Long.toString(size)); + conversation.add(message); + mXmppConnectionService.updateConversationUi(); + if (size <= this.mJingleConnectionManager + .getAutoAcceptFileSize()) { + Log.d(Config.LOGTAG, "auto accepting file from " + + packet.getFrom()); + this.acceptedAutomatically = true; + this.sendAccept(); + } else { + message.markUnread(); + Log.d(Config.LOGTAG, + "not auto accepting new file offer with size: " + + size + + " allowed size:" + + this.mJingleConnectionManager + .getAutoAcceptFileSize()); + this.mXmppConnectionService.getNotificationService() + .push(message); + } + this.file = this.mXmppConnectionService.getFileBackend() + .getFile(message, false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + byte[] key = conversation.getSymmetricKey(); + if (key == null) { + this.sendCancel(); + this.cancel(); + return; + } else { + this.file.setKey(key); + } + } + this.file.setExpectedSize(size); + } else { + this.sendCancel(); + this.cancel(); + } + } else { + this.sendCancel(); + this.cancel(); + } + } else { + this.sendCancel(); + this.cancel(); + } + } + + private void sendInitRequest() { + this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED); + JinglePacket packet = this.bootstrapPacket("session-initiate"); + Content content = new Content(this.contentCreator, this.contentName); + if (message.getType() == Message.TYPE_IMAGE) { + content.setTransportId(this.transportId); + this.file = this.mXmppConnectionService.getFileBackend().getFile( + message, false); + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + Conversation conversation = this.message.getConversation(); + this.mXmppConnectionService.renewSymmetricKey(conversation); + content.setFileOffer(this.file, true); + this.file.setKey(conversation.getSymmetricKey()); + } else { + content.setFileOffer(this.file, false); + } + this.transportId = this.mJingleConnectionManager.nextRandomId(); + content.setTransportId(this.transportId); + content.socks5transport().setChildren(getCandidatesAsElements()); + packet.setContent(content); + this.sendJinglePacket(packet); + this.mJingleStatus = JINGLE_STATUS_INITIATED; + } + } + + private List<Element> getCandidatesAsElements() { + List<Element> elements = new ArrayList<Element>(); + for (JingleCandidate c : this.candidates) { + elements.add(c.toElement()); + } + return elements; + } + + private void sendAccept() { + mJingleStatus = JINGLE_STATUS_ACCEPTED; + this.mStatus = Downloadable.STATUS_DOWNLOADING; + mXmppConnectionService.updateConversationUi(); + this.mJingleConnectionManager.getPrimaryCandidate(this.account, + new OnPrimaryCandidateFound() { + + @Override + public void onPrimaryCandidateFound(boolean success, + final JingleCandidate candidate) { + final JinglePacket packet = bootstrapPacket("session-accept"); + final Content content = new Content(contentCreator, + contentName); + content.setFileOffer(fileOffer); + content.setTransportId(transportId); + if ((success) && (!equalCandidateExists(candidate))) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + JingleConnection.this, candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection to our own primary candidate failed"); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "connected to primary candidate"); + mergeCandidate(candidate); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + }); + } else { + Log.d(Config.LOGTAG, + "did not find a primary candidate for ourself"); + content.socks5transport().setChildren( + getCandidatesAsElements()); + packet.setContent(content); + sendJinglePacket(packet); + connectNextCandidate(); + } + } + }); + + } + + private JinglePacket bootstrapPacket(String action) { + JinglePacket packet = new JinglePacket(); + packet.setAction(action); + packet.setFrom(account.getFullJid()); + packet.setTo(this.message.getCounterpart()); + packet.setSessionId(this.sessionId); + packet.setInitiator(this.initiator); + return packet; + } + + private void sendJinglePacket(JinglePacket packet) { + // Log.d(Config.LOGTAG,packet.toString()); + account.getXmppConnection().sendIqPacket(packet, responseListener); + } + + private boolean receiveAccept(JinglePacket packet) { + Content content = packet.getJingleContent(); + mergeCandidates(JingleCandidate.parse(content.socks5transport() + .getChildren())); + this.mJingleStatus = JINGLE_STATUS_ACCEPTED; + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + this.connectNextCandidate(); + return true; + } + + private boolean receiveTransportInfo(JinglePacket packet) { + Content content = packet.getJingleContent(); + if (content.hasSocks5Transport()) { + if (content.socks5transport().hasChild("activated")) { + if ((this.transport != null) + && (this.transport instanceof JingleSocks5Transport)) { + onProxyActivated.success(); + } else { + String cid = content.socks5transport() + .findChild("activated").getAttribute("cid"); + Log.d(Config.LOGTAG, "received proxy activated (" + cid + + ")prior to choosing our own transport"); + JingleSocks5Transport connection = this.connections + .get(cid); + if (connection != null) { + connection.setActivated(true); + } else { + Log.d(Config.LOGTAG, "activated connection not found"); + this.sendCancel(); + this.cancel(); + } + } + return true; + } else if (content.socks5transport().hasChild("proxy-error")) { + onProxyActivated.failed(); + return true; + } else if (content.socks5transport().hasChild("candidate-error")) { + Log.d(Config.LOGTAG, "received candidate error"); + this.receivedCandidate = true; + if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) + && (this.sentCandidate)) { + this.connect(); + } + return true; + } else if (content.socks5transport().hasChild("candidate-used")) { + String cid = content.socks5transport() + .findChild("candidate-used").getAttribute("cid"); + if (cid != null) { + Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid); + JingleCandidate candidate = getCandidate(cid); + candidate.flagAsUsedByCounterpart(); + this.receivedCandidate = true; + if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) + && (this.sentCandidate)) { + this.connect(); + } else { + Log.d(Config.LOGTAG, + "ignoring because file is already in transmission or we havent sent our candidate yet"); + } + return true; + } else { + return false; + } + } else { + return false; + } + } else { + return true; + } + } + + private void connect() { + final JingleSocks5Transport connection = chooseConnection(); + this.transport = connection; + if (connection == null) { + Log.d(Config.LOGTAG, "could not find suitable candidate"); + this.disconnect(); + if (this.initiator.equals(account.getFullJid())) { + this.sendFallbackToIbb(); + } + } else { + this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; + if (connection.needsActivation()) { + if (connection.getCandidate().isOurs()) { + Log.d(Config.LOGTAG, "candidate " + + connection.getCandidate().getCid() + + " was our proxy. going to activate"); + IqPacket activation = new IqPacket(IqPacket.TYPE_SET); + activation.setTo(connection.getCandidate().getJid()); + activation.query("http://jabber.org/protocol/bytestreams") + .setAttribute("sid", this.getSessionId()); + activation.query().addChild("activate") + .setContent(this.getCounterPart()); + this.account.getXmppConnection().sendIqPacket(activation, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + onProxyActivated.failed(); + } else { + onProxyActivated.success(); + sendProxyActivated(connection + .getCandidate().getCid()); + } + } + }); + } else { + Log.d(Config.LOGTAG, + "candidate " + + connection.getCandidate().getCid() + + " was a proxy. waiting for other party to activate"); + } + } else { + if (initiator.equals(account.getFullJid())) { + Log.d(Config.LOGTAG, "we were initiating. sending file"); + connection.send(file, onFileTransmissionSatusChanged); + } else { + Log.d(Config.LOGTAG, "we were responding. receiving file"); + connection.receive(file, onFileTransmissionSatusChanged); + } + } + } + } + + private JingleSocks5Transport chooseConnection() { + JingleSocks5Transport connection = null; + for (Entry<String, JingleSocks5Transport> cursor : connections + .entrySet()) { + JingleSocks5Transport currentConnection = cursor.getValue(); + // Log.d(Config.LOGTAG,"comparing candidate: "+currentConnection.getCandidate().toString()); + if (currentConnection.isEstablished() + && (currentConnection.getCandidate().isUsedByCounterpart() || (!currentConnection + .getCandidate().isOurs()))) { + // Log.d(Config.LOGTAG,"is usable"); + if (connection == null) { + connection = currentConnection; + } else { + if (connection.getCandidate().getPriority() < currentConnection + .getCandidate().getPriority()) { + connection = currentConnection; + } else if (connection.getCandidate().getPriority() == currentConnection + .getCandidate().getPriority()) { + // Log.d(Config.LOGTAG,"found two candidates with same priority"); + if (initiator.equals(account.getFullJid())) { + if (currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } else { + if (!currentConnection.getCandidate().isOurs()) { + connection = currentConnection; + } + } + } + } + } + } + return connection; + } + + private void sendSuccess() { + JinglePacket packet = bootstrapPacket("session-terminate"); + Reason reason = new Reason(); + reason.addChild("success"); + packet.setReason(reason); + this.sendJinglePacket(packet); + this.disconnect(); + this.mJingleStatus = JINGLE_STATUS_FINISHED; + this.message.setStatus(Message.STATUS_RECEIVED); + this.message.setDownloadable(null); + this.mXmppConnectionService.updateMessage(message); + this.mJingleConnectionManager.finishConnection(this); + } + + private void sendFallbackToIbb() { + Log.d(Config.LOGTAG, "sending fallback to ibb"); + JinglePacket packet = this.bootstrapPacket("transport-replace"); + Content content = new Content(this.contentCreator, this.contentName); + this.transportId = this.mJingleConnectionManager.nextRandomId(); + content.setTransportId(this.transportId); + content.ibbTransport().setAttribute("block-size", + Integer.toString(this.ibbBlockSize)); + packet.setContent(content); + this.sendJinglePacket(packet); + } + + private boolean receiveFallbackToIbb(JinglePacket packet) { + Log.d(Config.LOGTAG, "receiving fallack to ibb"); + String receivedBlockSize = packet.getJingleContent().ibbTransport() + .getAttribute("block-size"); + if (receivedBlockSize != null) { + int bs = Integer.parseInt(receivedBlockSize); + if (bs > this.ibbBlockSize) { + this.ibbBlockSize = bs; + } + } + this.transportId = packet.getJingleContent().getTransportId(); + this.transport = new JingleInbandTransport(this.account, + this.responder, this.transportId, this.ibbBlockSize); + this.transport.receive(file, onFileTransmissionSatusChanged); + JinglePacket answer = bootstrapPacket("transport-accept"); + Content content = new Content("initiator", "a-file-offer"); + content.setTransportId(this.transportId); + content.ibbTransport().setAttribute("block-size", + Integer.toString(this.ibbBlockSize)); + answer.setContent(content); + this.sendJinglePacket(answer); + return true; + } + + private boolean receiveTransportAccept(JinglePacket packet) { + if (packet.getJingleContent().hasIbbTransport()) { + String receivedBlockSize = packet.getJingleContent().ibbTransport() + .getAttribute("block-size"); + if (receivedBlockSize != null) { + int bs = Integer.parseInt(receivedBlockSize); + if (bs > this.ibbBlockSize) { + this.ibbBlockSize = bs; + } + } + this.transport = new JingleInbandTransport(this.account, + this.responder, this.transportId, this.ibbBlockSize); + this.transport.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, "ibb open failed"); + } + + @Override + public void established() { + JingleConnection.this.transport.send(file, + onFileTransmissionSatusChanged); + } + }); + return true; + } else { + return false; + } + } + + private void receiveSuccess() { + this.mJingleStatus = JINGLE_STATUS_FINISHED; + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND); + this.disconnect(); + this.mJingleConnectionManager.finishConnection(this); + } + + public void cancel() { + this.mJingleStatus = JINGLE_STATUS_CANCELED; + this.disconnect(); + if (this.message != null) { + if (this.responder.equals(account.getFullJid())) { + this.mStatus = Downloadable.STATUS_FAILED; + this.mXmppConnectionService.updateConversationUi(); + } else { + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); + } + } + this.mJingleConnectionManager.finishConnection(this); + } + + private void sendCancel() { + JinglePacket packet = bootstrapPacket("session-terminate"); + Reason reason = new Reason(); + reason.addChild("cancel"); + packet.setReason(reason); + this.sendJinglePacket(packet); + } + + private void connectNextCandidate() { + for (JingleCandidate candidate : this.candidates) { + if ((!connections.containsKey(candidate.getCid()) && (!candidate + .isOurs()))) { + this.connectWithCandidate(candidate); + return; + } + } + this.sendCandidateError(); + } + + private void connectWithCandidate(final JingleCandidate candidate) { + final JingleSocks5Transport socksConnection = new JingleSocks5Transport( + this, candidate); + connections.put(candidate.getCid(), socksConnection); + socksConnection.connect(new OnTransportConnected() { + + @Override + public void failed() { + Log.d(Config.LOGTAG, + "connection failed with " + candidate.getHost() + ":" + + candidate.getPort()); + connectNextCandidate(); + } + + @Override + public void established() { + Log.d(Config.LOGTAG, + "established connection with " + candidate.getHost() + + ":" + candidate.getPort()); + sendCandidateUsed(candidate.getCid()); + } + }); + } + + private void disconnect() { + Iterator<Entry<String, JingleSocks5Transport>> it = this.connections + .entrySet().iterator(); + while (it.hasNext()) { + Entry<String, JingleSocks5Transport> pairs = it.next(); + pairs.getValue().disconnect(); + it.remove(); + } + } + + private void sendProxyActivated(String cid) { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("activated") + .setAttribute("cid", cid); + packet.setContent(content); + this.sendJinglePacket(packet); + } + + private void sendCandidateUsed(final String cid) { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("candidate-used") + .setAttribute("cid", cid); + packet.setContent(content); + this.sentCandidate = true; + if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { + connect(); + } + this.sendJinglePacket(packet); + } + + private void sendCandidateError() { + JinglePacket packet = bootstrapPacket("transport-info"); + Content content = new Content(this.contentCreator, this.contentName); + content.setTransportId(this.transportId); + content.socks5transport().addChild("candidate-error"); + packet.setContent(content); + this.sentCandidate = true; + if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { + connect(); + } + this.sendJinglePacket(packet); + } + + public String getInitiator() { + return this.initiator; + } + + public String getResponder() { + return this.responder; + } + + public int getJingleStatus() { + return this.mJingleStatus; + } + + private boolean equalCandidateExists(JingleCandidate candidate) { + for (JingleCandidate c : this.candidates) { + if (c.equalValues(candidate)) { + return true; + } + } + return false; + } + + private void mergeCandidate(JingleCandidate candidate) { + for (JingleCandidate c : this.candidates) { + if (c.equals(candidate)) { + return; + } + } + this.candidates.add(candidate); + } + + private void mergeCandidates(List<JingleCandidate> candidates) { + for (JingleCandidate c : candidates) { + mergeCandidate(c); + } + } + + private JingleCandidate getCandidate(String cid) { + for (JingleCandidate c : this.candidates) { + if (c.getCid().equals(cid)) { + return c; + } + } + return null; + } + + interface OnProxyActivated { + public void success(); + + public void failed(); + } + + public boolean hasTransportId(String sid) { + return sid.equals(this.transportId); + } + + public JingleTransport getTransport() { + return this.transport; + } + + public boolean start() { + if (account.getStatus() == Account.STATUS_ONLINE) { + if (mJingleStatus == JINGLE_STATUS_INITIATED) { + new Thread(new Runnable() { + + @Override + public void run() { + sendAccept(); + } + }).start(); + } + return true; + } else { + return false; + } + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); + } else { + return 0; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java new file mode 100644 index 00000000..d937146a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -0,0 +1,168 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import android.annotation.SuppressLint; +import android.util.Log; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleConnectionManager extends AbstractConnectionManager { + private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>(); + + private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>(); + + @SuppressLint("TrulyRandom") + private SecureRandom random = new SecureRandom(); + + public JingleConnectionManager(XmppConnectionService service) { + super(service); + } + + public void deliverPacket(Account account, JinglePacket packet) { + if (packet.isAction("session-initiate")) { + JingleConnection connection = new JingleConnection(this); + connection.init(account, packet); + connections.add(connection); + } else { + for (JingleConnection connection : connections) { + if (connection.getAccount() == account + && connection.getSessionId().equals( + packet.getSessionId()) + && connection.getCounterPart().equals(packet.getFrom())) { + connection.deliverPacket(packet); + return; + } + } + IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); + Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("item-not-found", + "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); + account.getXmppConnection().sendIqPacket(response, null); + } + } + + public JingleConnection createNewConnection(Message message) { + JingleConnection connection = new JingleConnection(this); + connection.init(message); + this.connections.add(connection); + return connection; + } + + public JingleConnection createNewConnection(JinglePacket packet) { + JingleConnection connection = new JingleConnection(this); + this.connections.add(connection); + return connection; + } + + public void finishConnection(JingleConnection connection) { + this.connections.remove(connection); + } + + public void getPrimaryCandidate(Account account, + final OnPrimaryCandidateFound listener) { + if (!this.primaryCandidates.containsKey(account.getJid())) { + String xmlns = "http://jabber.org/protocol/bytestreams"; + final String proxy = account.getXmppConnection() + .findDiscoItemByFeature(xmlns); + if (proxy != null) { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setTo(proxy); + iq.query(xmlns); + account.getXmppConnection().sendIqPacket(iq, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + Element streamhost = packet + .query() + .findChild("streamhost", + "http://jabber.org/protocol/bytestreams"); + if (streamhost != null) { + JingleCandidate candidate = new JingleCandidate( + nextRandomId(), true); + candidate.setHost(streamhost + .getAttribute("host")); + candidate.setPort(Integer + .parseInt(streamhost + .getAttribute("port"))); + candidate + .setType(JingleCandidate.TYPE_PROXY); + candidate.setJid(proxy); + candidate.setPriority(655360 + 65535); + primaryCandidates.put(account.getJid(), + candidate); + listener.onPrimaryCandidateFound(true, + candidate); + } else { + listener.onPrimaryCandidateFound(false, + null); + } + } + }); + } else { + listener.onPrimaryCandidateFound(false, null); + } + + } else { + listener.onPrimaryCandidateFound(true, + this.primaryCandidates.get(account.getJid())); + } + } + + public String nextRandomId() { + return new BigInteger(50, random).toString(32); + } + + public void deliverIbbPacket(Account account, IqPacket packet) { + String sid = null; + Element payload = null; + if (packet.hasChild("open", "http://jabber.org/protocol/ibb")) { + payload = packet + .findChild("open", "http://jabber.org/protocol/ibb"); + sid = payload.getAttribute("sid"); + } else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) { + payload = packet + .findChild("data", "http://jabber.org/protocol/ibb"); + sid = payload.getAttribute("sid"); + } + if (sid != null) { + for (JingleConnection connection : connections) { + if (connection.getAccount() == account + && connection.hasTransportId(sid)) { + JingleTransport transport = connection.getTransport(); + if (transport instanceof JingleInbandTransport) { + JingleInbandTransport inbandTransport = (JingleInbandTransport) transport; + inbandTransport.deliverPayload(packet, payload); + return; + } + } + } + Log.d(Config.LOGTAG, + "couldnt deliver payload: " + payload.toString()); + } else { + Log.d(Config.LOGTAG, "no sid found in incomming ibb packet"); + } + } + + public void cancelInTransmission() { + for (JingleConnection connection : this.connections) { + if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) { + connection.cancel(); + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java new file mode 100644 index 00000000..cc1e92f6 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -0,0 +1,191 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import android.util.Base64; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JingleInbandTransport extends JingleTransport { + + private Account account; + private String counterpart; + private int blockSize; + private int bufferSize; + private int seq = 0; + private String sessionId; + + private boolean established = false; + + private DownloadableFile file; + + private InputStream fileInputStream = null; + private OutputStream fileOutputStream; + private long remainingSize; + private MessageDigest digest; + + private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged; + + private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + sendNextBlock(); + } + } + }; + + public JingleInbandTransport(Account account, String counterpart, + String sid, int blocksize) { + this.account = account; + this.counterpart = counterpart; + this.blockSize = blocksize; + this.bufferSize = blocksize / 4; + this.sessionId = sid; + } + + public void connect(final OnTransportConnected callback) { + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.setTo(this.counterpart); + Element open = iq.addChild("open", "http://jabber.org/protocol/ibb"); + open.setAttribute("sid", this.sessionId); + open.setAttribute("stanza", "iq"); + open.setAttribute("block-size", Integer.toString(this.blockSize)); + + this.account.getXmppConnection().sendIqPacket(iq, + new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, + IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + callback.failed(); + } else { + callback.established(); + } + } + }); + } + + @Override + public void receive(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + file.getParentFile().mkdirs(); + file.createNewFile(); + this.fileOutputStream = file.createOutputStream(); + if (this.fileOutputStream == null) { + callback.onFileTransferAborted(); + return; + } + this.remainingSize = file.getExpectedSize(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } + } + + @Override + public void send(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { + this.onFileTransmissionStatusChanged = callback; + this.file = file; + try { + this.digest = MessageDigest.getInstance("SHA-1"); + this.digest.reset(); + fileInputStream = this.file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + this.sendNextBlock(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + + private void sendNextBlock() { + byte[] buffer = new byte[this.bufferSize]; + try { + int count = fileInputStream.read(buffer); + if (count == -1) { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + fileInputStream.close(); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } else { + this.digest.update(buffer); + String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP); + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.setTo(this.counterpart); + Element data = iq.addChild("data", + "http://jabber.org/protocol/ibb"); + data.setAttribute("seq", Integer.toString(this.seq)); + data.setAttribute("block-size", + Integer.toString(this.blockSize)); + data.setAttribute("sid", this.sessionId); + data.setContent(base64); + this.account.getXmppConnection().sendIqPacket(iq, + this.onAckReceived); + this.seq++; + } + } catch (IOException e) { + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + private void receiveNextBlock(String data) { + try { + byte[] buffer = Base64.decode(data, Base64.NO_WRAP); + if (this.remainingSize < buffer.length) { + buffer = Arrays + .copyOfRange(buffer, 0, (int) this.remainingSize); + } + this.remainingSize -= buffer.length; + + this.fileOutputStream.write(buffer); + + this.digest.update(buffer); + if (this.remainingSize <= 0) { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + fileOutputStream.flush(); + fileOutputStream.close(); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + } + } catch (IOException e) { + this.onFileTransmissionStatusChanged.onFileTransferAborted(); + } + } + + public void deliverPayload(IqPacket packet, Element payload) { + if (payload.getName().equals("open")) { + if (!established) { + established = true; + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_RESULT), null); + } else { + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_ERROR), null); + } + } else if (payload.getName().equals("data")) { + this.receiveNextBlock(payload.getContent()); + this.account.getXmppConnection().sendIqPacket( + packet.generateRespone(IqPacket.TYPE_RESULT), null); + } else { + // TODO some sort of exception + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java new file mode 100644 index 00000000..1da2f0cd --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -0,0 +1,212 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.utils.CryptoHelper; + +public class JingleSocks5Transport extends JingleTransport { + private JingleCandidate candidate; + private String destination; + private OutputStream outputStream; + private InputStream inputStream; + private boolean isEstablished = false; + private boolean activated = false; + protected Socket socket; + + public JingleSocks5Transport(JingleConnection jingleConnection, + JingleCandidate candidate) { + this.candidate = candidate; + try { + MessageDigest mDigest = MessageDigest.getInstance("SHA-1"); + StringBuilder destBuilder = new StringBuilder(); + destBuilder.append(jingleConnection.getSessionId()); + if (candidate.isOurs()) { + destBuilder.append(jingleConnection.getAccount().getFullJid()); + destBuilder.append(jingleConnection.getCounterPart()); + } else { + destBuilder.append(jingleConnection.getCounterPart()); + destBuilder.append(jingleConnection.getAccount().getFullJid()); + } + mDigest.reset(); + this.destination = CryptoHelper.bytesToHex(mDigest + .digest(destBuilder.toString().getBytes())); + } catch (NoSuchAlgorithmException e) { + + } + } + + public void connect(final OnTransportConnected callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + socket = new Socket(candidate.getHost(), + candidate.getPort()); + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + byte[] login = { 0x05, 0x01, 0x00 }; + byte[] expectedReply = { 0x05, 0x00 }; + byte[] reply = new byte[2]; + outputStream.write(login); + inputStream.read(reply); + final String connect = Character.toString('\u0005') + + '\u0001' + '\u0000' + '\u0003' + '\u0028' + + destination + '\u0000' + '\u0000'; + if (Arrays.equals(reply, expectedReply)) { + outputStream.write(connect.getBytes()); + byte[] result = new byte[2]; + inputStream.read(result); + int status = result[1]; + if (status == 0) { + isEstablished = true; + callback.established(); + } else { + callback.failed(); + } + } else { + socket.close(); + callback.failed(); + } + } catch (UnknownHostException e) { + callback.failed(); + } catch (IOException e) { + callback.failed(); + } + } + }).start(); + + } + + public void send(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback) { + new Thread(new Runnable() { + + @Override + public void run() { + InputStream fileInputStream = null; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + fileInputStream = file.createInputStream(); + if (fileInputStream == null) { + callback.onFileTransferAborted(); + return; + } + int count; + byte[] buffer = new byte[8192]; + while ((count = fileInputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + } + outputStream.flush(); + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + if (callback != null) { + callback.onFileTransmitted(file); + } + } catch (FileNotFoundException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + } catch (IOException e) { + callback.onFileTransferAborted(); + } + } + } + }).start(); + + } + + public void receive(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback) { + new Thread(new Runnable() { + + @Override + public void run() { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + inputStream.skip(45); + socket.setSoTimeout(30000); + file.getParentFile().mkdirs(); + file.createNewFile(); + OutputStream fileOutputStream = file.createOutputStream(); + if (fileOutputStream == null) { + callback.onFileTransferAborted(); + return; + } + long remainingSize = file.getExpectedSize(); + byte[] buffer = new byte[8192]; + int count = buffer.length; + while (remainingSize > 0) { + count = inputStream.read(buffer); + if (count == -1) { + callback.onFileTransferAborted(); + return; + } else { + fileOutputStream.write(buffer, 0, count); + digest.update(buffer, 0, count); + remainingSize -= count; + } + } + fileOutputStream.flush(); + fileOutputStream.close(); + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + callback.onFileTransmitted(file); + } catch (FileNotFoundException e) { + callback.onFileTransferAborted(); + } catch (IOException e) { + callback.onFileTransferAborted(); + } catch (NoSuchAlgorithmException e) { + callback.onFileTransferAborted(); + } + } + }).start(); + } + + public boolean isProxy() { + return this.candidate.getType() == JingleCandidate.TYPE_PROXY; + } + + public boolean needsActivation() { + return (this.isProxy() && !this.activated); + } + + public void disconnect() { + if (this.socket != null) { + try { + this.socket.close(); + } catch (IOException e) { + + } + } + } + + public boolean isEstablished() { + return this.isEstablished; + } + + public JingleCandidate getCandidate() { + return this.candidate; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java new file mode 100644 index 00000000..1374e61c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.DownloadableFile; + +public abstract class JingleTransport { + public abstract void connect(final OnTransportConnected callback); + + public abstract void receive(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback); + + public abstract void send(final DownloadableFile file, + final OnFileTransmissionStatusChanged callback); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java new file mode 100644 index 00000000..e45e7441 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.DownloadableFile; + +public interface OnFileTransmissionStatusChanged { + public void onFileTransmitted(DownloadableFile file); + + public void onFileTransferAborted(); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java new file mode 100644 index 00000000..2aaf62a1 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java @@ -0,0 +1,9 @@ +package eu.siacs.conversations.xmpp.jingle; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.PacketReceived; +import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; + +public interface OnJinglePacketReceived extends PacketReceived { + public void onJinglePacketReceived(Account account, JinglePacket packet); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java new file mode 100644 index 00000000..03a437b2 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnPrimaryCandidateFound.java @@ -0,0 +1,6 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnPrimaryCandidateFound { + public void onPrimaryCandidateFound(boolean success, + JingleCandidate canditate); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java new file mode 100644 index 00000000..38f03c5d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnTransportConnected.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp.jingle; + +public interface OnTransportConnected { + public void failed(); + + public void established(); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java new file mode 100644 index 00000000..bcadbe77 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -0,0 +1,102 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.xml.Element; + +public class Content extends Element { + + private String transportId; + + private Content(String name) { + super(name); + } + + public Content() { + super("content"); + } + + public Content(String creator, String name) { + super("content"); + this.setAttribute("creator", creator); + this.setAttribute("name", name); + } + + public void setTransportId(String sid) { + this.transportId = sid; + } + + public void setFileOffer(DownloadableFile actualFile, boolean otr) { + Element description = this.addChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + Element offer = description.addChild("offer"); + Element file = offer.addChild("file"); + file.addChild("size").setContent(Long.toString(actualFile.getSize())); + if (otr) { + file.addChild("name").setContent(actualFile.getName() + ".otr"); + } else { + file.addChild("name").setContent(actualFile.getName()); + } + } + + public Element getFileOffer() { + Element description = this.findChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + if (description == null) { + return null; + } + Element offer = description.findChild("offer"); + if (offer == null) { + return null; + } + return offer.findChild("file"); + } + + public void setFileOffer(Element fileOffer) { + Element description = this.findChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + if (description == null) { + description = this.addChild("description", + "urn:xmpp:jingle:apps:file-transfer:3"); + } + description.addChild(fileOffer); + } + + public String getTransportId() { + if (hasSocks5Transport()) { + this.transportId = socks5transport().getAttribute("sid"); + } else if (hasIbbTransport()) { + this.transportId = ibbTransport().getAttribute("sid"); + } + return this.transportId; + } + + public Element socks5transport() { + Element transport = this.findChild("transport", + "urn:xmpp:jingle:transports:s5b:1"); + if (transport == null) { + transport = this.addChild("transport", + "urn:xmpp:jingle:transports:s5b:1"); + transport.setAttribute("sid", this.transportId); + } + return transport; + } + + public Element ibbTransport() { + Element transport = this.findChild("transport", + "urn:xmpp:jingle:transports:ibb:1"); + if (transport == null) { + transport = this.addChild("transport", + "urn:xmpp:jingle:transports:ibb:1"); + transport.setAttribute("sid", this.transportId); + } + return transport; + } + + public boolean hasSocks5Transport() { + return this.hasChild("transport", "urn:xmpp:jingle:transports:s5b:1"); + } + + public boolean hasIbbTransport() { + return this.hasChild("transport", "urn:xmpp:jingle:transports:ibb:1"); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java new file mode 100644 index 00000000..77a73643 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -0,0 +1,95 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class JinglePacket extends IqPacket { + Content content = null; + Reason reason = null; + Element jingle = new Element("jingle"); + + @Override + public Element addChild(Element child) { + if ("jingle".equals(child.getName())) { + Element contentElement = child.findChild("content"); + if (contentElement != null) { + this.content = new Content(); + this.content.setChildren(contentElement.getChildren()); + this.content.setAttributes(contentElement.getAttributes()); + } + Element reasonElement = child.findChild("reason"); + if (reasonElement != null) { + this.reason = new Reason(); + this.reason.setChildren(reasonElement.getChildren()); + this.reason.setAttributes(reasonElement.getAttributes()); + } + this.jingle.setAttributes(child.getAttributes()); + } + return child; + } + + public JinglePacket setContent(Content content) { + this.content = content; + return this; + } + + public Content getJingleContent() { + if (this.content == null) { + this.content = new Content(); + } + return this.content; + } + + public JinglePacket setReason(Reason reason) { + this.reason = reason; + return this; + } + + public Reason getReason() { + return this.reason; + } + + private void build() { + this.children.clear(); + this.jingle.clearChildren(); + this.jingle.setAttribute("xmlns", "urn:xmpp:jingle:1"); + if (this.content != null) { + jingle.addChild(this.content); + } + if (this.reason != null) { + jingle.addChild(this.reason); + } + this.children.add(jingle); + this.setAttribute("type", "set"); + } + + public String getSessionId() { + return this.jingle.getAttribute("sid"); + } + + public void setSessionId(String sid) { + this.jingle.setAttribute("sid", sid); + } + + @Override + public String toString() { + this.build(); + return super.toString(); + } + + public void setAction(String action) { + this.jingle.setAttribute("action", action); + } + + public String getAction() { + return this.jingle.getAttribute("action"); + } + + public void setInitiator(String initiator) { + this.jingle.setAttribute("initiator", initiator); + } + + public boolean isAction(String action) { + return action.equalsIgnoreCase(this.getAction()); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java new file mode 100644 index 00000000..610d5e76 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Reason.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.jingle.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class Reason extends Element { + private Reason(String name) { + super(name); + } + + public Reason() { + super("reason"); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java new file mode 100644 index 00000000..154fadf6 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -0,0 +1,71 @@ +package eu.siacs.conversations.xmpp.pep; + +import eu.siacs.conversations.xml.Element; +import android.util.Base64; + +public class Avatar { + public String type; + public String sha1sum; + public String image; + public int height; + public int width; + public long size; + public String owner; + + public byte[] getImageAsBytes() { + return Base64.decode(image, Base64.DEFAULT); + } + + public String getFilename() { + if (type == null) { + return sha1sum; + } else if (type.equalsIgnoreCase("image/webp")) { + return sha1sum + ".webp"; + } else if (type.equalsIgnoreCase("image/png")) { + return sha1sum + ".png"; + } else { + return sha1sum; + } + } + + public static Avatar parseMetadata(Element items) { + Element item = items.findChild("item"); + if (item == null) { + return null; + } + Element metadata = item.findChild("metadata"); + if (metadata == null) { + return null; + } + String primaryId = item.getAttribute("id"); + if (primaryId == null) { + return null; + } + for (Element child : metadata.getChildren()) { + if (child.getName().equals("info") + && primaryId.equals(child.getAttribute("id"))) { + Avatar avatar = new Avatar(); + String height = child.getAttribute("height"); + String width = child.getAttribute("width"); + String size = child.getAttribute("bytes"); + try { + if (height != null) { + avatar.height = Integer.parseInt(height); + } + if (width != null) { + avatar.width = Integer.parseInt(width); + } + if (size != null) { + avatar.size = Long.parseLong(size); + } + } catch (NumberFormatException e) { + return null; + } + avatar.type = child.getAttribute("type"); + avatar.sha1sum = child.getAttribute("id"); + return avatar; + } + } + return null; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java new file mode 100644 index 00000000..eef41c79 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java @@ -0,0 +1,34 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class AbstractStanza extends Element { + + protected AbstractStanza(String name) { + super(name); + } + + public String getTo() { + return getAttribute("to"); + } + + public String getFrom() { + return getAttribute("from"); + } + + public String getId() { + return this.getAttribute("id"); + } + + public void setTo(String to) { + setAttribute("to", to); + } + + public void setFrom(String from) { + setAttribute("from", from); + } + + public void setId(String id) { + setAttribute("id", id); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java new file mode 100644 index 00000000..9df05e67 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class IqPacket extends AbstractStanza { + + public static final int TYPE_ERROR = -1; + public static final int TYPE_SET = 0; + public static final int TYPE_RESULT = 1; + public static final int TYPE_GET = 2; + + private IqPacket(String name) { + super(name); + } + + public IqPacket(int type) { + super("iq"); + switch (type) { + case TYPE_SET: + this.setAttribute("type", "set"); + break; + case TYPE_GET: + this.setAttribute("type", "get"); + break; + case TYPE_RESULT: + this.setAttribute("type", "result"); + break; + case TYPE_ERROR: + this.setAttribute("type", "error"); + break; + default: + break; + } + } + + public IqPacket() { + super("iq"); + } + + public Element query() { + Element query = findChild("query"); + if (query == null) { + query = addChild("query"); + } + return query; + } + + public Element query(String xmlns) { + Element query = query(); + query.setAttribute("xmlns", xmlns); + return query(); + } + + public int getType() { + String type = getAttribute("type"); + if ("error".equals(type)) { + return TYPE_ERROR; + } else if ("result".equals(type)) { + return TYPE_RESULT; + } else if ("set".equals(type)) { + return TYPE_SET; + } else if ("get".equals(type)) { + return TYPE_GET; + } else { + return 1000; + } + } + + public IqPacket generateRespone(int type) { + IqPacket packet = new IqPacket(type); + packet.setTo(this.getFrom()); + packet.setId(this.getId()); + return packet; + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java new file mode 100644 index 00000000..4e7b532b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -0,0 +1,66 @@ +package eu.siacs.conversations.xmpp.stanzas; + +import eu.siacs.conversations.xml.Element; + +public class MessagePacket extends AbstractStanza { + public static final int TYPE_CHAT = 0; + public static final int TYPE_NORMAL = 2; + public static final int TYPE_GROUPCHAT = 3; + public static final int TYPE_ERROR = 4; + public static final int TYPE_HEADLINE = 5; + + public MessagePacket() { + super("message"); + } + + public String getBody() { + Element body = this.findChild("body"); + if (body != null) { + return body.getContent(); + } else { + return null; + } + } + + public void setBody(String text) { + this.children.remove(findChild("body")); + Element body = new Element("body"); + body.setContent(text); + this.children.add(body); + } + + public void setType(int type) { + switch (type) { + case TYPE_CHAT: + this.setAttribute("type", "chat"); + break; + case TYPE_GROUPCHAT: + this.setAttribute("type", "groupchat"); + break; + case TYPE_NORMAL: + break; + default: + this.setAttribute("type", "chat"); + break; + } + } + + public int getType() { + String type = getAttribute("type"); + if (type == null) { + return TYPE_NORMAL; + } else if (type.equals("normal")) { + return TYPE_NORMAL; + } else if (type.equals("chat")) { + return TYPE_CHAT; + } else if (type.equals("groupchat")) { + return TYPE_GROUPCHAT; + } else if (type.equals("error")) { + return TYPE_ERROR; + } else if (type.equals("headline")) { + return TYPE_HEADLINE; + } else { + return TYPE_NORMAL; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java new file mode 100644 index 00000000..7ea32099 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java @@ -0,0 +1,8 @@ +package eu.siacs.conversations.xmpp.stanzas; + +public class PresencePacket extends AbstractStanza { + + public PresencePacket() { + super("presence"); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java new file mode 100644 index 00000000..78ab66d8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.xmpp.stanzas.csi; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class ActivePacket extends AbstractStanza { + public ActivePacket() { + super("active"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java new file mode 100644 index 00000000..f109280f --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.xmpp.stanzas.csi; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class InactivePacket extends AbstractStanza { + public InactivePacket() { + super("inactive"); + setAttribute("xmlns", "urn:xmpp:csi:0"); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java new file mode 100644 index 00000000..f93b5d87 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class AckPacket extends AbstractStanza { + + public AckPacket(int sequence, int smVersion) { + super("a"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("h", Integer.toString(sequence)); + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java new file mode 100644 index 00000000..78cd81ed --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class EnablePacket extends AbstractStanza { + + public EnablePacket(int smVersion) { + super("enable"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("resume", "true"); + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java new file mode 100644 index 00000000..98cfc748 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java @@ -0,0 +1,12 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class RequestPacket extends AbstractStanza { + + public RequestPacket(int smVersion) { + super("r"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java new file mode 100644 index 00000000..9cdcfa5e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java @@ -0,0 +1,14 @@ +package eu.siacs.conversations.xmpp.stanzas.streammgmt; + +import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; + +public class ResumePacket extends AbstractStanza { + + public ResumePacket(String id, int sequence, int smVersion) { + super("resume"); + this.setAttribute("xmlns", "urn:xmpp:sm:" + smVersion); + this.setAttribute("previd", id); + this.setAttribute("h", Integer.toString(sequence)); + } + +} diff --git a/src/main/res/drawable-hdpi/ic_action_add_group.png b/src/main/res/drawable-hdpi/ic_action_add_group.png Binary files differnew file mode 100644 index 00000000..97640355 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_add_group.png diff --git a/src/main/res/drawable-hdpi/ic_action_add_person.png b/src/main/res/drawable-hdpi/ic_action_add_person.png Binary files differnew file mode 100644 index 00000000..9d88d0f4 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_add_person.png diff --git a/src/main/res/drawable-hdpi/ic_action_chat.png b/src/main/res/drawable-hdpi/ic_action_chat.png Binary files differnew file mode 100644 index 00000000..0847ac46 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_chat.png diff --git a/src/main/res/drawable-hdpi/ic_action_copy.png b/src/main/res/drawable-hdpi/ic_action_copy.png Binary files differnew file mode 100644 index 00000000..22327391 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_copy.png diff --git a/src/main/res/drawable-hdpi/ic_action_discard.png b/src/main/res/drawable-hdpi/ic_action_discard.png Binary files differnew file mode 100644 index 00000000..703b31f8 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_discard.png diff --git a/src/main/res/drawable-hdpi/ic_action_edit.png b/src/main/res/drawable-hdpi/ic_action_edit.png Binary files differnew file mode 100644 index 00000000..756db316 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_edit.png diff --git a/src/main/res/drawable-hdpi/ic_action_edit_dark.png b/src/main/res/drawable-hdpi/ic_action_edit_dark.png Binary files differnew file mode 100644 index 00000000..5f7c6eff --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_edit_dark.png diff --git a/src/main/res/drawable-hdpi/ic_action_group.png b/src/main/res/drawable-hdpi/ic_action_group.png Binary files differnew file mode 100644 index 00000000..3e7f16d5 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_group.png diff --git a/src/main/res/drawable-hdpi/ic_action_new.png b/src/main/res/drawable-hdpi/ic_action_new.png Binary files differnew file mode 100644 index 00000000..d866d616 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_new.png diff --git a/src/main/res/drawable-hdpi/ic_action_new_attachment.png b/src/main/res/drawable-hdpi/ic_action_new_attachment.png Binary files differnew file mode 100644 index 00000000..c01c2b38 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_new_attachment.png diff --git a/src/main/res/drawable-hdpi/ic_action_not_secure.png b/src/main/res/drawable-hdpi/ic_action_not_secure.png Binary files differnew file mode 100644 index 00000000..2c917615 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_not_secure.png diff --git a/src/main/res/drawable-hdpi/ic_action_refresh.png b/src/main/res/drawable-hdpi/ic_action_refresh.png Binary files differnew file mode 100644 index 00000000..45b22282 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_refresh.png diff --git a/src/main/res/drawable-hdpi/ic_action_remove.png b/src/main/res/drawable-hdpi/ic_action_remove.png Binary files differnew file mode 100644 index 00000000..58a56e45 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_remove.png diff --git a/src/main/res/drawable-hdpi/ic_action_search.png b/src/main/res/drawable-hdpi/ic_action_search.png Binary files differnew file mode 100644 index 00000000..772e3598 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_search.png diff --git a/src/main/res/drawable-hdpi/ic_action_secure.png b/src/main/res/drawable-hdpi/ic_action_secure.png Binary files differnew file mode 100644 index 00000000..4439d1ae --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_secure.png diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_away.png b/src/main/res/drawable-hdpi/ic_action_send_now_away.png Binary files differnew file mode 100644 index 00000000..505cbe63 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_send_now_away.png diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png Binary files differnew file mode 100644 index 00000000..a376524d --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_send_now_dnd.png diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_offline.png b/src/main/res/drawable-hdpi/ic_action_send_now_offline.png Binary files differnew file mode 100644 index 00000000..d4d2d510 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_send_now_offline.png diff --git a/src/main/res/drawable-hdpi/ic_action_send_now_online.png b/src/main/res/drawable-hdpi/ic_action_send_now_online.png Binary files differnew file mode 100644 index 00000000..48676f7b --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_action_send_now_online.png diff --git a/src/main/res/drawable-hdpi/ic_activity.png b/src/main/res/drawable-hdpi/ic_activity.png Binary files differnew file mode 100644 index 00000000..613da683 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_activity.png diff --git a/src/main/res/drawable-hdpi/ic_indicator.png b/src/main/res/drawable-hdpi/ic_indicator.png Binary files differnew file mode 100644 index 00000000..6de8969f --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_indicator.png diff --git a/src/main/res/drawable-hdpi/ic_launcher.png b/src/main/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..d48df2c3 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_launcher.png diff --git a/src/main/res/drawable-hdpi/ic_notification.png b/src/main/res/drawable-hdpi/ic_notification.png Binary files differnew file mode 100644 index 00000000..664ba535 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_notification.png diff --git a/src/main/res/drawable-hdpi/ic_profile.png b/src/main/res/drawable-hdpi/ic_profile.png Binary files differnew file mode 100644 index 00000000..3f071dec --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_profile.png diff --git a/src/main/res/drawable-hdpi/ic_received_indicator.png b/src/main/res/drawable-hdpi/ic_received_indicator.png Binary files differnew file mode 100644 index 00000000..b1e3f274 --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_received_indicator.png diff --git a/src/main/res/drawable-hdpi/ic_secure_indicator.png b/src/main/res/drawable-hdpi/ic_secure_indicator.png Binary files differnew file mode 100644 index 00000000..2a2934fb --- /dev/null +++ b/src/main/res/drawable-hdpi/ic_secure_indicator.png diff --git a/src/main/res/drawable-hdpi/tab_selected_conversations.9.png b/src/main/res/drawable-hdpi/tab_selected_conversations.9.png Binary files differnew file mode 100644 index 00000000..b8f44c21 --- /dev/null +++ b/src/main/res/drawable-hdpi/tab_selected_conversations.9.png diff --git a/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png b/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png Binary files differnew file mode 100644 index 00000000..5512dbd3 --- /dev/null +++ b/src/main/res/drawable-hdpi/tab_selected_focused_conversations.9.png diff --git a/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png b/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png Binary files differnew file mode 100644 index 00000000..e5f1df22 --- /dev/null +++ b/src/main/res/drawable-hdpi/tab_selected_pressed_conversations.9.png diff --git a/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png b/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png Binary files differnew file mode 100644 index 00000000..7cd46d63 --- /dev/null +++ b/src/main/res/drawable-hdpi/tab_unselected_conversations.9.png diff --git a/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png b/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png Binary files differnew file mode 100644 index 00000000..438ecdd8 --- /dev/null +++ b/src/main/res/drawable-hdpi/tab_unselected_focused_conversations.9.png diff --git a/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png b/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png Binary files differnew file mode 100644 index 00000000..4f18a95a --- /dev/null +++ b/src/main/res/drawable-hdpi/tab_unselected_pressed_conversations.9.png diff --git a/src/main/res/drawable-mdpi/ic_action_add_group.png b/src/main/res/drawable-mdpi/ic_action_add_group.png Binary files differnew file mode 100644 index 00000000..9a655899 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_add_group.png diff --git a/src/main/res/drawable-mdpi/ic_action_add_person.png b/src/main/res/drawable-mdpi/ic_action_add_person.png Binary files differnew file mode 100644 index 00000000..b7d8f46a --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_add_person.png diff --git a/src/main/res/drawable-mdpi/ic_action_chat.png b/src/main/res/drawable-mdpi/ic_action_chat.png Binary files differnew file mode 100644 index 00000000..8fdb5d75 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_chat.png diff --git a/src/main/res/drawable-mdpi/ic_action_copy.png b/src/main/res/drawable-mdpi/ic_action_copy.png Binary files differnew file mode 100644 index 00000000..71348202 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_copy.png diff --git a/src/main/res/drawable-mdpi/ic_action_discard.png b/src/main/res/drawable-mdpi/ic_action_discard.png Binary files differnew file mode 100644 index 00000000..248fb09c --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_discard.png diff --git a/src/main/res/drawable-mdpi/ic_action_edit.png b/src/main/res/drawable-mdpi/ic_action_edit.png Binary files differnew file mode 100644 index 00000000..68a45320 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_edit.png diff --git a/src/main/res/drawable-mdpi/ic_action_edit_dark.png b/src/main/res/drawable-mdpi/ic_action_edit_dark.png Binary files differnew file mode 100644 index 00000000..650b4d89 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_edit_dark.png diff --git a/src/main/res/drawable-mdpi/ic_action_group.png b/src/main/res/drawable-mdpi/ic_action_group.png Binary files differnew file mode 100644 index 00000000..1ee3cccd --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_group.png diff --git a/src/main/res/drawable-mdpi/ic_action_new.png b/src/main/res/drawable-mdpi/ic_action_new.png Binary files differnew file mode 100644 index 00000000..f17e7980 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_new.png diff --git a/src/main/res/drawable-mdpi/ic_action_new_attachment.png b/src/main/res/drawable-mdpi/ic_action_new_attachment.png Binary files differnew file mode 100644 index 00000000..1d265aac --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_new_attachment.png diff --git a/src/main/res/drawable-mdpi/ic_action_not_secure.png b/src/main/res/drawable-mdpi/ic_action_not_secure.png Binary files differnew file mode 100644 index 00000000..faffa233 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_not_secure.png diff --git a/src/main/res/drawable-mdpi/ic_action_refresh.png b/src/main/res/drawable-mdpi/ic_action_refresh.png Binary files differnew file mode 100644 index 00000000..de008e51 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_refresh.png diff --git a/src/main/res/drawable-mdpi/ic_action_remove.png b/src/main/res/drawable-mdpi/ic_action_remove.png Binary files differnew file mode 100644 index 00000000..342a79de --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_remove.png diff --git a/src/main/res/drawable-mdpi/ic_action_search.png b/src/main/res/drawable-mdpi/ic_action_search.png Binary files differnew file mode 100644 index 00000000..4edb1ff9 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_search.png diff --git a/src/main/res/drawable-mdpi/ic_action_secure.png b/src/main/res/drawable-mdpi/ic_action_secure.png Binary files differnew file mode 100644 index 00000000..05332ebf --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_secure.png diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_away.png b/src/main/res/drawable-mdpi/ic_action_send_now_away.png Binary files differnew file mode 100644 index 00000000..0fdca901 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_send_now_away.png diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png Binary files differnew file mode 100644 index 00000000..c0aef36c --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_send_now_dnd.png diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_offline.png b/src/main/res/drawable-mdpi/ic_action_send_now_offline.png Binary files differnew file mode 100644 index 00000000..7723f4aa --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_send_now_offline.png diff --git a/src/main/res/drawable-mdpi/ic_action_send_now_online.png b/src/main/res/drawable-mdpi/ic_action_send_now_online.png Binary files differnew file mode 100644 index 00000000..39d00ee4 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_action_send_now_online.png diff --git a/src/main/res/drawable-mdpi/ic_activity.png b/src/main/res/drawable-mdpi/ic_activity.png Binary files differnew file mode 100644 index 00000000..c8727f57 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_activity.png diff --git a/src/main/res/drawable-mdpi/ic_indicator.png b/src/main/res/drawable-mdpi/ic_indicator.png Binary files differnew file mode 100644 index 00000000..bb4fee10 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_indicator.png diff --git a/src/main/res/drawable-mdpi/ic_launcher.png b/src/main/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..200daf4c --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_launcher.png diff --git a/src/main/res/drawable-mdpi/ic_notification.png b/src/main/res/drawable-mdpi/ic_notification.png Binary files differnew file mode 100644 index 00000000..5d1aca10 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_notification.png diff --git a/src/main/res/drawable-mdpi/ic_profile.png b/src/main/res/drawable-mdpi/ic_profile.png Binary files differnew file mode 100644 index 00000000..0d056c7c --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_profile.png diff --git a/src/main/res/drawable-mdpi/ic_received_indicator.png b/src/main/res/drawable-mdpi/ic_received_indicator.png Binary files differnew file mode 100644 index 00000000..88ff1efb --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_received_indicator.png diff --git a/src/main/res/drawable-mdpi/ic_secure_indicator.png b/src/main/res/drawable-mdpi/ic_secure_indicator.png Binary files differnew file mode 100644 index 00000000..5a73aef4 --- /dev/null +++ b/src/main/res/drawable-mdpi/ic_secure_indicator.png diff --git a/src/main/res/drawable-mdpi/tab_selected_conversations.9.png b/src/main/res/drawable-mdpi/tab_selected_conversations.9.png Binary files differnew file mode 100644 index 00000000..09d42dc8 --- /dev/null +++ b/src/main/res/drawable-mdpi/tab_selected_conversations.9.png diff --git a/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png b/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png Binary files differnew file mode 100644 index 00000000..20af01de --- /dev/null +++ b/src/main/res/drawable-mdpi/tab_selected_focused_conversations.9.png diff --git a/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png b/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png Binary files differnew file mode 100644 index 00000000..13a878be --- /dev/null +++ b/src/main/res/drawable-mdpi/tab_selected_pressed_conversations.9.png diff --git a/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png b/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png Binary files differnew file mode 100644 index 00000000..ad2dbae9 --- /dev/null +++ b/src/main/res/drawable-mdpi/tab_unselected_conversations.9.png diff --git a/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png b/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png Binary files differnew file mode 100644 index 00000000..dfff5ac8 --- /dev/null +++ b/src/main/res/drawable-mdpi/tab_unselected_focused_conversations.9.png diff --git a/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png b/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png Binary files differnew file mode 100644 index 00000000..4365d178 --- /dev/null +++ b/src/main/res/drawable-mdpi/tab_unselected_pressed_conversations.9.png diff --git a/src/main/res/drawable-xhdpi/ic_action_add_group.png b/src/main/res/drawable-xhdpi/ic_action_add_group.png Binary files differnew file mode 100644 index 00000000..c493aa5a --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_add_group.png diff --git a/src/main/res/drawable-xhdpi/ic_action_add_person.png b/src/main/res/drawable-xhdpi/ic_action_add_person.png Binary files differnew file mode 100644 index 00000000..4e8de1b6 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_add_person.png diff --git a/src/main/res/drawable-xhdpi/ic_action_chat.png b/src/main/res/drawable-xhdpi/ic_action_chat.png Binary files differnew file mode 100644 index 00000000..8a9a4314 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_chat.png diff --git a/src/main/res/drawable-xhdpi/ic_action_copy.png b/src/main/res/drawable-xhdpi/ic_action_copy.png Binary files differnew file mode 100644 index 00000000..5ddf1513 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_copy.png diff --git a/src/main/res/drawable-xhdpi/ic_action_discard.png b/src/main/res/drawable-xhdpi/ic_action_discard.png Binary files differnew file mode 100644 index 00000000..9eeeed12 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_discard.png diff --git a/src/main/res/drawable-xhdpi/ic_action_edit.png b/src/main/res/drawable-xhdpi/ic_action_edit.png Binary files differnew file mode 100644 index 00000000..67e056fe --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_edit.png diff --git a/src/main/res/drawable-xhdpi/ic_action_edit_dark.png b/src/main/res/drawable-xhdpi/ic_action_edit_dark.png Binary files differnew file mode 100644 index 00000000..8ab436d8 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_edit_dark.png diff --git a/src/main/res/drawable-xhdpi/ic_action_group.png b/src/main/res/drawable-xhdpi/ic_action_group.png Binary files differnew file mode 100644 index 00000000..fa2af497 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_group.png diff --git a/src/main/res/drawable-xhdpi/ic_action_new.png b/src/main/res/drawable-xhdpi/ic_action_new.png Binary files differnew file mode 100644 index 00000000..dde2141f --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_new.png diff --git a/src/main/res/drawable-xhdpi/ic_action_new_attachment.png b/src/main/res/drawable-xhdpi/ic_action_new_attachment.png Binary files differnew file mode 100644 index 00000000..41cbab20 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_new_attachment.png diff --git a/src/main/res/drawable-xhdpi/ic_action_not_secure.png b/src/main/res/drawable-xhdpi/ic_action_not_secure.png Binary files differnew file mode 100644 index 00000000..c0902a03 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_not_secure.png diff --git a/src/main/res/drawable-xhdpi/ic_action_refresh.png b/src/main/res/drawable-xhdpi/ic_action_refresh.png Binary files differnew file mode 100644 index 00000000..cdc160d4 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_refresh.png diff --git a/src/main/res/drawable-xhdpi/ic_action_remove.png b/src/main/res/drawable-xhdpi/ic_action_remove.png Binary files differnew file mode 100644 index 00000000..58e2e3b4 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_remove.png diff --git a/src/main/res/drawable-xhdpi/ic_action_search.png b/src/main/res/drawable-xhdpi/ic_action_search.png Binary files differnew file mode 100644 index 00000000..19658e4a --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_search.png diff --git a/src/main/res/drawable-xhdpi/ic_action_secure.png b/src/main/res/drawable-xhdpi/ic_action_secure.png Binary files differnew file mode 100644 index 00000000..4e08b95a --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_secure.png diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_away.png b/src/main/res/drawable-xhdpi/ic_action_send_now_away.png Binary files differnew file mode 100644 index 00000000..bb999d85 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_send_now_away.png diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png Binary files differnew file mode 100644 index 00000000..a0bf5561 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_send_now_dnd.png diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png b/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png Binary files differnew file mode 100644 index 00000000..6da9ff7b --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_send_now_offline.png diff --git a/src/main/res/drawable-xhdpi/ic_action_send_now_online.png b/src/main/res/drawable-xhdpi/ic_action_send_now_online.png Binary files differnew file mode 100644 index 00000000..348ba657 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_action_send_now_online.png diff --git a/src/main/res/drawable-xhdpi/ic_activity.png b/src/main/res/drawable-xhdpi/ic_activity.png Binary files differnew file mode 100644 index 00000000..95ffbecf --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_activity.png diff --git a/src/main/res/drawable-xhdpi/ic_indicator.png b/src/main/res/drawable-xhdpi/ic_indicator.png Binary files differnew file mode 100644 index 00000000..3e5141c2 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_indicator.png diff --git a/src/main/res/drawable-xhdpi/ic_launcher.png b/src/main/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..927a2d2a --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_launcher.png diff --git a/src/main/res/drawable-xhdpi/ic_notification.png b/src/main/res/drawable-xhdpi/ic_notification.png Binary files differnew file mode 100644 index 00000000..dfa643d0 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_notification.png diff --git a/src/main/res/drawable-xhdpi/ic_profile.png b/src/main/res/drawable-xhdpi/ic_profile.png Binary files differnew file mode 100644 index 00000000..88a82cf0 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_profile.png diff --git a/src/main/res/drawable-xhdpi/ic_received_indicator.png b/src/main/res/drawable-xhdpi/ic_received_indicator.png Binary files differnew file mode 100644 index 00000000..2c871933 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_received_indicator.png diff --git a/src/main/res/drawable-xhdpi/ic_secure_indicator.png b/src/main/res/drawable-xhdpi/ic_secure_indicator.png Binary files differnew file mode 100644 index 00000000..1f4c9a32 --- /dev/null +++ b/src/main/res/drawable-xhdpi/ic_secure_indicator.png diff --git a/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png b/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png Binary files differnew file mode 100644 index 00000000..34eb4ec0 --- /dev/null +++ b/src/main/res/drawable-xhdpi/tab_selected_conversations.9.png diff --git a/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png b/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png Binary files differnew file mode 100644 index 00000000..3155ef69 --- /dev/null +++ b/src/main/res/drawable-xhdpi/tab_selected_focused_conversations.9.png diff --git a/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png b/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png Binary files differnew file mode 100644 index 00000000..5c2440e4 --- /dev/null +++ b/src/main/res/drawable-xhdpi/tab_selected_pressed_conversations.9.png diff --git a/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png b/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png Binary files differnew file mode 100644 index 00000000..e9ab742e --- /dev/null +++ b/src/main/res/drawable-xhdpi/tab_unselected_conversations.9.png diff --git a/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png b/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png Binary files differnew file mode 100644 index 00000000..42a2191e --- /dev/null +++ b/src/main/res/drawable-xhdpi/tab_unselected_focused_conversations.9.png diff --git a/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png b/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png Binary files differnew file mode 100644 index 00000000..a5a2c25e --- /dev/null +++ b/src/main/res/drawable-xhdpi/tab_unselected_pressed_conversations.9.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_add_group.png b/src/main/res/drawable-xxhdpi/ic_action_add_group.png Binary files differnew file mode 100644 index 00000000..2b46dbb9 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_add_group.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_add_person.png b/src/main/res/drawable-xxhdpi/ic_action_add_person.png Binary files differnew file mode 100644 index 00000000..e9a58eaf --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_add_person.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_chat.png b/src/main/res/drawable-xxhdpi/ic_action_chat.png Binary files differnew file mode 100644 index 00000000..04000fd0 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_chat.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_copy.png b/src/main/res/drawable-xxhdpi/ic_action_copy.png Binary files differnew file mode 100644 index 00000000..a0508df8 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_copy.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_discard.png b/src/main/res/drawable-xxhdpi/ic_action_discard.png Binary files differnew file mode 100644 index 00000000..cb1260a4 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_discard.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_edit.png b/src/main/res/drawable-xxhdpi/ic_action_edit.png Binary files differnew file mode 100644 index 00000000..3a241ea4 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_edit.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png b/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png Binary files differnew file mode 100644 index 00000000..f2b2078b --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_edit_dark.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_group.png b/src/main/res/drawable-xxhdpi/ic_action_group.png Binary files differnew file mode 100644 index 00000000..9289b1c8 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_group.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_new.png b/src/main/res/drawable-xxhdpi/ic_action_new.png Binary files differnew file mode 100644 index 00000000..c42c2bfb --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_new.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png b/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png Binary files differnew file mode 100644 index 00000000..ce7536cb --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_new_attachment.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_not_secure.png b/src/main/res/drawable-xxhdpi/ic_action_not_secure.png Binary files differnew file mode 100644 index 00000000..a186f1fb --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_not_secure.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_refresh.png b/src/main/res/drawable-xxhdpi/ic_action_refresh.png Binary files differnew file mode 100644 index 00000000..cb847f37 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_refresh.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_remove.png b/src/main/res/drawable-xxhdpi/ic_action_remove.png Binary files differnew file mode 100644 index 00000000..331c545b --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_remove.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_search.png b/src/main/res/drawable-xxhdpi/ic_action_search.png Binary files differnew file mode 100644 index 00000000..a1086388 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_search.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_secure.png b/src/main/res/drawable-xxhdpi/ic_action_secure.png Binary files differnew file mode 100644 index 00000000..ccf1fb00 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_secure.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png Binary files differnew file mode 100644 index 00000000..12ec4d33 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_send_now_away.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png Binary files differnew file mode 100644 index 00000000..7719f81a --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_send_now_dnd.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png Binary files differnew file mode 100644 index 00000000..18895813 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_send_now_offline.png diff --git a/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png b/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png Binary files differnew file mode 100644 index 00000000..29bde36e --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_action_send_now_online.png diff --git a/src/main/res/drawable-xxhdpi/ic_activity.png b/src/main/res/drawable-xxhdpi/ic_activity.png Binary files differnew file mode 100644 index 00000000..0b642d9b --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_activity.png diff --git a/src/main/res/drawable-xxhdpi/ic_indicator.png b/src/main/res/drawable-xxhdpi/ic_indicator.png Binary files differnew file mode 100644 index 00000000..2c51b8b7 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_indicator.png diff --git a/src/main/res/drawable-xxhdpi/ic_launcher.png b/src/main/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..65c1af34 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_launcher.png diff --git a/src/main/res/drawable-xxhdpi/ic_notification.png b/src/main/res/drawable-xxhdpi/ic_notification.png Binary files differnew file mode 100644 index 00000000..ee1e9534 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_notification.png diff --git a/src/main/res/drawable-xxhdpi/ic_profile.png b/src/main/res/drawable-xxhdpi/ic_profile.png Binary files differnew file mode 100644 index 00000000..309dc513 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_profile.png diff --git a/src/main/res/drawable-xxhdpi/ic_received_indicator.png b/src/main/res/drawable-xxhdpi/ic_received_indicator.png Binary files differnew file mode 100644 index 00000000..039a9ef9 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_received_indicator.png diff --git a/src/main/res/drawable-xxhdpi/ic_secure_indicator.png b/src/main/res/drawable-xxhdpi/ic_secure_indicator.png Binary files differnew file mode 100644 index 00000000..1ee9b67d --- /dev/null +++ b/src/main/res/drawable-xxhdpi/ic_secure_indicator.png diff --git a/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png Binary files differnew file mode 100644 index 00000000..e4439e7c --- /dev/null +++ b/src/main/res/drawable-xxhdpi/tab_selected_conversations.9.png diff --git a/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png Binary files differnew file mode 100644 index 00000000..dd2ded89 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/tab_selected_focused_conversations.9.png diff --git a/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png Binary files differnew file mode 100644 index 00000000..58c8a576 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/tab_selected_pressed_conversations.9.png diff --git a/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png Binary files differnew file mode 100644 index 00000000..566062f0 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/tab_unselected_conversations.9.png diff --git a/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png Binary files differnew file mode 100644 index 00000000..432e68c4 --- /dev/null +++ b/src/main/res/drawable-xxhdpi/tab_unselected_focused_conversations.9.png diff --git a/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png b/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png Binary files differnew file mode 100644 index 00000000..8dd01d5c --- /dev/null +++ b/src/main/res/drawable-xxhdpi/tab_unselected_pressed_conversations.9.png diff --git a/src/main/res/drawable/actionbar_tab_indicator.xml b/src/main/res/drawable/actionbar_tab_indicator.xml new file mode 100644 index 00000000..5598ee42 --- /dev/null +++ b/src/main/res/drawable/actionbar_tab_indicator.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- Non focused states --> + <item android:drawable="@android:color/transparent" android:state_focused="false" android:state_pressed="false" android:state_selected="false"/> + <item android:drawable="@drawable/tab_selected_conversations" android:state_focused="false" android:state_pressed="false" android:state_selected="true"/> + + <!-- Focused states --> + <item android:drawable="@drawable/tab_unselected_focused_conversations" android:state_focused="true" android:state_pressed="false" android:state_selected="false"/> + <item android:drawable="@drawable/tab_selected_focused_conversations" android:state_focused="true" android:state_pressed="false" android:state_selected="true"/> + + <!-- Pressed --> + <!-- Non focused states --> + <item android:drawable="@drawable/tab_unselected_pressed_conversations" android:state_focused="false" android:state_pressed="true" android:state_selected="false"/> + <item android:drawable="@drawable/tab_selected_pressed_conversations" android:state_focused="false" android:state_pressed="true" android:state_selected="true"/> + + <!-- Focused states --> + <item android:drawable="@drawable/tab_unselected_pressed_conversations" android:state_focused="true" android:state_pressed="true" android:state_selected="false"/> + <item android:drawable="@drawable/tab_selected_pressed_conversations" android:state_focused="true" android:state_pressed="true" android:state_selected="true"/> + +</selector>
\ No newline at end of file diff --git a/src/main/res/drawable/es_slidingpane_shadow.xml b/src/main/res/drawable/es_slidingpane_shadow.xml new file mode 100644 index 00000000..44ffd4ea --- /dev/null +++ b/src/main/res/drawable/es_slidingpane_shadow.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" > + + <gradient + android:endColor="@color/divider" + android:startColor="@android:color/transparent" /> + + <size + android:height="0.5dp" + android:width="3.0dp" /> + +</shape>
\ No newline at end of file diff --git a/src/main/res/drawable/grey.xml b/src/main/res/drawable/grey.xml new file mode 100644 index 00000000..2e90d96d --- /dev/null +++ b/src/main/res/drawable/grey.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" > + + <solid android:color="#ffdddddd" /> + +</shape>
\ No newline at end of file diff --git a/src/main/res/drawable/greybackground.xml b/src/main/res/drawable/greybackground.xml new file mode 100644 index 00000000..bedc4b17 --- /dev/null +++ b/src/main/res/drawable/greybackground.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:drawable="@drawable/grey" android:state_pressed="true"/> + +</selector>
\ No newline at end of file diff --git a/src/main/res/drawable/infocard_border.xml b/src/main/res/drawable/infocard_border.xml new file mode 100644 index 00000000..7c7ded57 --- /dev/null +++ b/src/main/res/drawable/infocard_border.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" > + + <solid android:color="@color/primarybackground" /> + + <corners android:radius="2dp" /> + + <stroke + android:width="0.5dp" + android:color="@color/divider" > + </stroke> + + <padding + android:bottom="0dp" + android:left="0dp" + android:right="0dp" + android:top="0dp" /> + +</shape>
\ No newline at end of file diff --git a/src/main/res/drawable/message_border.xml b/src/main/res/drawable/message_border.xml new file mode 100644 index 00000000..b463d788 --- /dev/null +++ b/src/main/res/drawable/message_border.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" > + + <corners android:radius="2dp" /> + + <padding + android:bottom="1.5dp" + android:left="1.5dp" + android:right="1.5dp" + android:top="1.5dp" /> + + <solid android:color="@color/divider" /> + +</shape>
\ No newline at end of file diff --git a/src/main/res/drawable/snackbar.xml b/src/main/res/drawable/snackbar.xml new file mode 100644 index 00000000..951d7aee --- /dev/null +++ b/src/main/res/drawable/snackbar.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" > + + <solid android:color="@color/darkbackground" /> + + <corners android:radius="8dip" /> + + <padding + android:bottom="0dip" + android:left="0dip" + android:right="0dip" + android:top="0dip" /> + +</shape>
\ No newline at end of file diff --git a/src/main/res/layout-w360dp/fragment_conversations_overview.xml b/src/main/res/layout-w360dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..a600118d --- /dev/null +++ b/src/main/res/layout-w360dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ +<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/content_view_spl" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="300dp" + android:layout_height="match_parent" + android:background="@color/primarybackground" + android:orientation="vertical" > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@color/primarybackground" + android:divider="@color/divider" + android:dividerHeight="1dp" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="fill_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical" > + </LinearLayout> + +</android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file diff --git a/src/main/res/layout-w384dp/fragment_conversations_overview.xml b/src/main/res/layout-w384dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..c3aa67ae --- /dev/null +++ b/src/main/res/layout-w384dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ +<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/content_view_spl" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="345dp" + android:layout_height="match_parent" + android:background="@color/primarybackground" + android:orientation="vertical" > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@color/primarybackground" + android:divider="@color/divider" + android:dividerHeight="1dp" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="fill_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical" > + </LinearLayout> + +</android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file diff --git a/src/main/res/layout-w600dp/fragment_conversations_overview.xml b/src/main/res/layout-w600dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..331fb1f0 --- /dev/null +++ b/src/main/res/layout-w600dp/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ +<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/content_view_spl" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="400dp" + android:layout_height="match_parent" + android:background="@color/primarybackground" + android:orientation="vertical" > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@color/primarybackground" + android:divider="@color/divider" + android:dividerHeight="1dp" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="fill_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical" > + </LinearLayout> + +</android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file diff --git a/src/main/res/layout-w960dp/fragment_conversations_overview.xml b/src/main/res/layout-w960dp/fragment_conversations_overview.xml new file mode 100644 index 00000000..2744f38e --- /dev/null +++ b/src/main/res/layout-w960dp/fragment_conversations_overview.xml @@ -0,0 +1,32 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/content_view_ll" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:baselineAligned="false"> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:background="@color/primarybackground" + android:orientation="vertical" > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@color/primarybackground" + android:divider="@color/divider" + android:dividerHeight="1dp" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="0dp" + android:layout_weight="2" + android:layout_height="match_parent" + android:orientation="vertical" > + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml new file mode 100644 index 00000000..2d1190a3 --- /dev/null +++ b/src/main/res/layout/account_row.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/activatedBackgroundIndicator" + android:padding="8dp" > + + <ImageView + android:id="@+id/account_image" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignParentLeft="true" + android:src="@drawable/ic_profile" > + </ImageView> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_toRightOf="@+id/account_image" + android:orientation="vertical" + android:paddingLeft="8dp" > + + <TextView + android:id="@+id/account_jid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scrollHorizontally="false" + android:singleLine="true" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" /> + + <TextView + android:id="@+id/account_status" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/account_status_unknown" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" + android:textStyle="bold" /> + </LinearLayout> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/actionview_search.xml b/src/main/res/layout/actionview_search.xml new file mode 100644 index 00000000..64b75f0e --- /dev/null +++ b/src/main/res/layout/actionview_search.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:addStatesFromChildren="true" + android:focusable="true" + android:gravity="center" + android:paddingLeft="5dp" + android:paddingRight="5dp" > + + <EditText + android:id="@+id/search_field" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:focusable="true" + android:inputType="textEmailAddress|textNoSuggestions" + android:textColor="@color/ondarktext" /> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/activity_choose_contact.xml b/src/main/res/layout/activity_choose_contact.xml new file mode 100644 index 00000000..248a7822 --- /dev/null +++ b/src/main/res/layout/activity_choose_contact.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <ListView + android:id="@+id/choose_contact_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:listitem="@layout/contact" /> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml new file mode 100644 index 00000000..f7cb2198 --- /dev/null +++ b/src/main/res/layout/activity_contact_details.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@color/secondarybackground" > + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <RelativeLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:padding="16dp" > + + <QuickContactBadge + android:id="@+id/details_contact_badge" + android:layout_width="72dp" + android:layout_height="72dp" + android:layout_alignParentTop="true" + android:scaleType="centerCrop" /> + + <LinearLayout + android:id="@+id/details_jidbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="16dp" + android:layout_toRightOf="@+id/details_contact_badge" + android:orientation="vertical" > + + <TextView + android:id="@+id/details_contactjid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/account_settings_example_jabber_id" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" > + + <TextView + android:id="@+id/details_contactstatus" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text=" · " + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/details_lastseen" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + </LinearLayout> + + <CheckBox + android:id="@+id/details_send_presence" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/send_presence_updates" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <CheckBox + android:id="@+id/details_receive_presence" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/receive_presence_updates" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + </LinearLayout> + + <TextView + android:id="@+id/details_account" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_below="@+id/details_jidbox" + android:layout_marginTop="32dp" + android:text="@string/using_account" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" /> + </RelativeLayout> + + <LinearLayout + android:id="@+id/details_contact_keys" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:divider="?android:dividerHorizontal" + android:orientation="vertical" + android:padding="8dp" + android:showDividers="middle" > + </LinearLayout> + </LinearLayout> + +</ScrollView>
\ No newline at end of file diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml new file mode 100644 index 00000000..97289628 --- /dev/null +++ b/src/main/res/layout/activity_edit_account.xml @@ -0,0 +1,272 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/secondarybackground" > + + <ScrollView + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_above="@+id/button_bar" + android:layout_alignParentTop="true" > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <LinearLayout + android:id="@+id/editor" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="16dp" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/account_settings_jabber_id" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <AutoCompleteTextView + android:id="@+id/account_jid" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/account_settings_example_jabber_id" + android:inputType="textEmailAddress" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/account_settings_password" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <EditText + android:id="@+id/account_password" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/password" + android:inputType="textPassword" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + + <CheckBox + android:id="@+id/account_register_new" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/register_account" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/account_confirm_password_desc" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/account_settings_confirm_password" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" + android:visibility="gone" /> + + <EditText + android:id="@+id/account_password_confirm" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:hint="@string/confirm_password" + android:inputType="textPassword" + android:visibility="gone" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/stats" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="16dp" + android:visibility="gone" > + + <TableLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:stretchColumns="1" > + + <TableRow + android:layout_width="fill_parent" + android:layout_height="wrap_content" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/server_info_session_established" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/session_est" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + </TableRow> + + <TableRow + android:layout_width="fill_parent" + android:layout_height="wrap_content" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/server_info_pep" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/server_info_pep" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + </TableRow> + + <TableRow + android:layout_width="fill_parent" + android:layout_height="wrap_content" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/server_info_stream_management" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/server_info_sm" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + </TableRow> + + <TableRow + android:layout_width="fill_parent" + android:layout_height="wrap_content" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/server_info_carbon_messages" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/server_info_carbons" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + </TableRow> + </TableLayout> + + + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:id="@+id/otr_fingerprint_box" + android:layout_marginTop="32dp"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/action_copy_to_clipboard" + android:orientation="vertical"> + + <TextView + android:id="@+id/otr_fingerprint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" + android:typeface="monospace" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" + android:text="@string/otr_fingerprint"/> + </LinearLayout> + + <ImageButton + android:id="@+id/action_copy_to_clipboard" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:background="?android:selectableItemBackground" + android:padding="8dp" + android:src="@drawable/ic_action_copy" + android:visibility="visible" /> + </RelativeLayout> + + + </LinearLayout> + </LinearLayout> + </ScrollView> + + <LinearLayout + android:id="@+id/button_bar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" > + + <Button + android:id="@+id/cancel_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/cancel" + android:textColor="@color/primarytext" /> + + <View + android:layout_width="1dp" + android:layout_height="fill_parent" + android:layout_marginBottom="7dp" + android:layout_marginTop="7dp" + android:background="@color/divider" /> + + <Button + android:id="@+id/save_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:enabled="false" + android:text="@string/save" + android:textColor="@color/secondarytext" /> + </LinearLayout> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml new file mode 100644 index 00000000..f689f10d --- /dev/null +++ b/src/main/res/layout/activity_muc_details.xml @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="utf-8"?> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@color/secondarybackground" > + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="16dp" > + + <TextView + android:id="@+id/muc_jabberid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/account_settings_example_jabber_id" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold" + android:layout_marginBottom="16dp"/> + + <RelativeLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/your_photo" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignParentLeft="true" + android:src="@drawable/ic_profile" > + </ImageView> + + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_toRightOf="@+id/your_photo" + android:orientation="vertical" + android:paddingLeft="8dp" > + + <TextView + android:id="@+id/muc_your_nick" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" /> + + <TextView + android:id="@+id/muc_role" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + </LinearLayout> + + <ImageButton + android:id="@+id/edit_nick_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:background="?android:selectableItemBackground" + android:padding="8dp" + android:src="@drawable/ic_action_edit_dark" /> + </RelativeLayout> + <TextView + android:id="@+id/details_account" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:layout_marginTop="32dp" + android:text="@string/using_account" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/muc_more_details" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="8dp" > + + + <LinearLayout + android:id="@+id/muc_members" + android:layout_width="fill_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:divider="?android:dividerHorizontal" + android:orientation="vertical" + android:showDividers="middle" > + </LinearLayout> + + <Button + android:id="@+id/invite" + style="?android:attr/buttonStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_marginTop="24dp" + android:text="@string/invite_contact" /> + </LinearLayout> + +</LinearLayout> +</ScrollView>
\ No newline at end of file diff --git a/src/main/res/layout/activity_publish_profile_picture.xml b/src/main/res/layout/activity_publish_profile_picture.xml new file mode 100644 index 00000000..fac499bc --- /dev/null +++ b/src/main/res/layout/activity_publish_profile_picture.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/primarybackground" > + + <LinearLayout + android:id="@+id/account_image_wrapper" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:layout_marginBottom="8dp" + android:layout_marginTop="24dp" + android:background="@drawable/message_border" > + + <ImageView + android:id="@+id/account_image" + android:layout_width="194dp" + android:layout_height="194dp" /> + </LinearLayout> + + <TextView + android:id="@+id/hint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/account_image_wrapper" + android:layout_centerHorizontal="true" + android:text="@string/touch_to_choose_picture" + android:textColor="@color/secondarytext" /> + + <TextView + android:id="@+id/secondary_hint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/hint" + android:layout_centerHorizontal="true" + android:text="@string/or_long_press_for_default" + android:textColor="@color/secondarytext" /> + + <LinearLayout + android:id="@+id/button_bar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" > + + <Button + android:id="@+id/cancel_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/cancel" + android:textColor="@color/primarytext" /> + + <View + android:layout_width="1dp" + android:layout_height="fill_parent" + android:layout_marginBottom="7dp" + android:layout_marginTop="7dp" + android:background="@color/divider" /> + + <Button + android:id="@+id/publish_button" + style="?android:attr/borderlessButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:enabled="false" + android:text="@string/publish" + android:textColor="@color/secondarytext" /> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:layout_above="@+id/button_bar" + android:layout_alignParentLeft="true" + android:layout_alignParentRight="true" + android:layout_below="@+id/secondary_hint" + android:gravity="center_vertical" + android:orientation="vertical" + android:paddingLeft="8dp" + android:paddingRight="8dp" > + + <TextView + android:id="@+id/account" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" /> + + <TextView + android:id="@+id/hint_or_warning" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:minLines="3" + android:text="@string/publish_avatar_explanation" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + </LinearLayout> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/activity_start_conversation.xml b/src/main/res/layout/activity_start_conversation.xml new file mode 100644 index 00000000..f9c98529 --- /dev/null +++ b/src/main/res/layout/activity_start_conversation.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/start_conversation_view_pager" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/primarybackground" > + +</android.support.v4.view.ViewPager>
\ No newline at end of file diff --git a/src/main/res/layout/contact.xml b/src/main/res/layout/contact.xml new file mode 100644 index 00000000..12ab3da1 --- /dev/null +++ b/src/main/res/layout/contact.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/activatedBackgroundIndicator" + android:padding="8dp" > + + <ImageView + android:id="@+id/contact_photo" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignParentLeft="true" + android:scaleType="centerCrop" + android:src="@drawable/ic_profile" > + </ImageView> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_toRightOf="@+id/contact_photo" + android:orientation="vertical" + android:paddingLeft="8dp" > + + <TextView + android:id="@+id/contact_display_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" /> + + <TextView + android:id="@+id/contact_jid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/key" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" + android:typeface="monospace" + android:visibility="gone" /> + </LinearLayout> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml new file mode 100644 index 00000000..7053857f --- /dev/null +++ b/src/main/res/layout/contact_key.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="match_parent" > + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/button_remove" + android:orientation="vertical" + android:padding="8dp" > + + <TextView + android:id="@+id/key" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" + android:typeface="monospace" /> + + <TextView + android:id="@+id/key_type" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo"/> + </LinearLayout> + + <ImageButton + android:id="@+id/button_remove" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:background="?android:selectableItemBackground" + android:padding="8dp" + android:src="@drawable/ic_action_remove" + android:visibility="invisible" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/conversation_list_row.xml b/src/main/res/layout/conversation_list_row.xml new file mode 100644 index 00000000..21147b4a --- /dev/null +++ b/src/main/res/layout/conversation_list_row.xml @@ -0,0 +1,68 @@ +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="8dp" > + + <ImageView + android:id="@+id/conversation_image" + android:layout_width="56dp" + android:layout_height="56dp" + android:layout_alignParentLeft="true" + android:scaleType="centerCrop" /> + + <RelativeLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_toRightOf="@+id/conversation_image" + android:paddingLeft="8dp" > + + <TextView + android:id="@+id/conversation_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignLeft="@+id/conversation_lastwrapper" + android:layout_toLeftOf="@+id/conversation_lastupdate" + android:singleLine="true" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" + android:typeface="sans" /> + + <LinearLayout + android:id="@+id/conversation_lastwrapper" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_below="@id/conversation_name" + android:orientation="vertical" + android:paddingTop="3dp" > + + <TextView + android:id="@+id/conversation_lastmsg" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:scrollHorizontally="false" + android:singleLine="true" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <ImageView + android:id="@+id/conversation_lastimage" + android:layout_width="fill_parent" + android:layout_height="36dp" + android:background="@color/primarytext" + android:scaleType="centerCrop" /> + </LinearLayout> + + <TextView + android:id="@+id/conversation_lastupdate" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBaseline="@+id/conversation_name" + android:layout_alignParentRight="true" + android:gravity="right" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" /> + </RelativeLayout> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/create_contact_dialog.xml b/src/main/res/layout/create_contact_dialog.xml new file mode 100644 index 00000000..1ab4b686 --- /dev/null +++ b/src/main/res/layout/create_contact_dialog.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="8dp" > + + <TextView + android:id="@+id/your_account" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/your_account" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <Spinner + android:id="@+id/account" + android:layout_width="fill_parent" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/jabber_id" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/account_settings_jabber_id" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <AutoCompleteTextView + android:id="@+id/jid" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:hint="@string/account_settings_example_jabber_id" + android:inputType="textEmailAddress" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" /> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/dialog_clear_history.xml b/src/main/res/layout/dialog_clear_history.xml new file mode 100644 index 00000000..252808c8 --- /dev/null +++ b/src/main/res/layout/dialog_clear_history.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="8dp" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="8dp" + android:text="@string/clear_histor_msg" + android:textSize="?attr/TextSizeBody" /> + + <CheckBox + android:id="@+id/end_conversation_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/also_end_conversation" /> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/dialog_verify_otr.xml b/src/main/res/layout/dialog_verify_otr.xml new file mode 100644 index 00000000..499ef6cd --- /dev/null +++ b/src/main/res/layout/dialog_verify_otr.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingBottom="16dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:text="@string/account_settings_jabber_id" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" /> + + <TextView + android:id="@+id/verify_otr_jid" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="8dp" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:text="@string/otr_fingerprint" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" /> + + <TextView + android:id="@+id/verify_otr_fingerprint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="8dp" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" + android:typeface="monospace" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:text="@string/your_fingerprint" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeHeadline" /> + + <TextView + android:id="@+id/verify_otr_yourprint" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingLeft="8dp" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" + android:typeface="monospace" /> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/fragment_conversation.xml b/src/main/res/layout/fragment_conversation.xml new file mode 100644 index 00000000..f9aae10a --- /dev/null +++ b/src/main/res/layout/fragment_conversation.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/secondarybackground" > + + <ListView + android:id="@+id/messages_view" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_above="@+id/snackbar" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:background="@color/secondarybackground" + android:divider="@null" + android:dividerHeight="0dp" + android:listSelector="@android:color/transparent" + android:stackFromBottom="true" + android:transcriptMode="normal" + tools:listitem="@layout/message_sent" > + </ListView> + + <RelativeLayout + android:id="@+id/textsend" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:background="@color/primarybackground" > + + <eu.siacs.conversations.ui.EditMessage + android:id="@+id/textinput" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@+id/textSendButton" + android:background="@color/primarybackground" + android:ems="10" + android:imeOptions="flagNoExtractUi|actionSend" + android:inputType="textShortMessage|textMultiLine|textCapSentences" + android:minHeight="48dp" + android:minLines="1" + android:paddingBottom="12dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="12dp" + android:textColor="@color/primarytext" > + + <requestFocus /> + </eu.siacs.conversations.ui.EditMessage> + + <ImageButton + android:id="@+id/textSendButton" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:background="?android:selectableItemBackground" + android:src="@drawable/ic_action_send_now_offline" /> + </RelativeLayout> + + <RelativeLayout + android:id="@+id/snackbar" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_above="@+id/textsend" + android:layout_marginBottom="4dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" + android:background="@drawable/snackbar" + android:minHeight="48dp" + android:visibility="gone" > + + <TextView + android:id="@+id/snackbar_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_centerVertical="true" + android:layout_toLeftOf="@+id/snackbar_action" + android:paddingLeft="24dp" + android:textColor="@color/ondarktext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/snackbar_action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:paddingBottom="16dp" + android:paddingLeft="24dp" + android:paddingRight="24dp" + android:paddingTop="16dp" + android:textAllCaps="true" + android:textColor="@color/ondarktext" + android:textSize="?attr/TextSizeBody" + android:textStyle="bold" /> + </RelativeLayout> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/fragment_conversations_overview.xml b/src/main/res/layout/fragment_conversations_overview.xml new file mode 100644 index 00000000..d4145761 --- /dev/null +++ b/src/main/res/layout/fragment_conversations_overview.xml @@ -0,0 +1,30 @@ +<android.support.v4.widget.SlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/content_view_spl" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="288dp" + android:layout_height="match_parent" + android:background="@color/primarybackground" + android:orientation="vertical" > + + <ListView + android:id="@+id/list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:background="@color/primarybackground" + android:divider="@color/divider" + android:dividerHeight="1dp" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/selected_conversation" + android:layout_width="fill_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical" > + </LinearLayout> + +</android.support.v4.widget.SlidingPaneLayout>
\ No newline at end of file diff --git a/src/main/res/layout/join_conference_dialog.xml b/src/main/res/layout/join_conference_dialog.xml new file mode 100644 index 00000000..95c9d24c --- /dev/null +++ b/src/main/res/layout/join_conference_dialog.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="8dp" > + + <TextView + android:id="@+id/your_account" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/your_account" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <Spinner + android:id="@+id/account" + android:layout_width="fill_parent" + android:layout_height="wrap_content" /> + + <TextView + android:id="@+id/jabber_id" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="@string/conference_address" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <AutoCompleteTextView + android:id="@+id/jid" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:hint="@string/conference_address_example" + android:inputType="textEmailAddress" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" /> + + <CheckBox + android:id="@+id/bookmark" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:checked="true" + android:text="@string/save_as_bookmark" /> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/manage_accounts.xml b/src/main/res/layout/manage_accounts.xml new file mode 100644 index 00000000..11ce35b2 --- /dev/null +++ b/src/main/res/layout/manage_accounts.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@color/primarybackground" > + + <ListView + android:id="@+id/account_list" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:divider="@color/divider" + android:dividerHeight="1dp" > + </ListView> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/message_null.xml b/src/main/res/layout/message_null.xml new file mode 100644 index 00000000..0e0f1c92 --- /dev/null +++ b/src/main/res/layout/message_null.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="0dp" + android:background="#00000000"> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/message_received.xml b/src/main/res/layout/message_received.xml new file mode 100644 index 00000000..39bb842a --- /dev/null +++ b/src/main/res/layout/message_received.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="4dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="4dp" > + + <LinearLayout + android:id="@+id/message_box" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_toRightOf="@+id/message_photo" + android:background="@drawable/message_border" + android:minHeight="48dp" + android:longClickable="true"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:background="@color/primarybackground" + android:gravity="center_vertical" + android:orientation="vertical" + android:paddingBottom="4dp" + android:paddingLeft="5dp" + android:paddingRight="5dp" + android:paddingTop="4dp" > + + <ImageView + android:id="@+id/message_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:background="@color/primarytext" + android:paddingBottom="2dp" + android:scaleType="centerCrop" /> + + <TextView + android:id="@+id/message_body" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:autoLink="web" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <Button + android:id="@+id/download_button" + style="?android:attr/buttonStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/download_image" + android:visibility="gone" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingTop="1dp" > + + <ImageView + android:id="@+id/security_indicator" + android:layout_width="?attr/TextSizeInfo" + android:layout_height="?attr/TextSizeInfo" + android:layout_gravity="center_vertical" + android:layout_marginRight="4sp" + android:alpha="0.54" + android:gravity="center_vertical" + android:src="@drawable/ic_secure_indicator" /> + + <TextView + android:id="@+id/message_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:gravity="center_vertical" + android:text="@string/sending" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" /> + </LinearLayout> + </LinearLayout> + </LinearLayout> + + <ImageView + android:id="@+id/message_photo" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_marginRight="-1.5dp" + android:padding="0dp" + android:scaleType="fitXY" + android:src="@drawable/ic_profile" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml new file mode 100644 index 00000000..3e854643 --- /dev/null +++ b/src/main/res/layout/message_sent.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="4dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="4dp" > + + <LinearLayout + android:id="@+id/message_box" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_toLeftOf="@+id/message_photo" + android:background="@drawable/message_border" + android:minHeight="48dp" + android:longClickable="true"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:background="@color/primarybackground" + android:gravity="center_vertical" + android:orientation="vertical" + android:paddingBottom="4dp" + android:paddingLeft="5dp" + android:paddingRight="5dp" + android:paddingTop="4dp" > + + <ImageView + android:id="@+id/message_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:adjustViewBounds="true" + android:background="@color/primarytext" + android:paddingBottom="2dp" + android:scaleType="centerCrop" /> + + <TextView + android:id="@+id/message_body" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:autoLink="web" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> + + <Button + android:id="@+id/download_button" + style="?android:attr/buttonStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/download_image" + android:visibility="gone" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="right" + android:orientation="horizontal" + android:paddingTop="1dp" > + + <TextView + android:id="@+id/message_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:gravity="center_vertical" + android:text="@string/sending" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" /> + + <ImageView + android:id="@+id/security_indicator" + android:layout_width="?attr/TextSizeInfo" + android:layout_height="?attr/TextSizeInfo" + android:layout_gravity="center_vertical" + android:layout_marginLeft="4sp" + android:alpha="0.54" + android:gravity="center_vertical" + android:src="@drawable/ic_secure_indicator" /> + + <ImageView + android:id="@+id/indicator_received" + android:layout_width="?attr/TextSizeInfo" + android:layout_height="?attr/TextSizeInfo" + android:layout_gravity="center_vertical" + android:layout_marginLeft="4sp" + android:alpha="0.54" + android:gravity="center_vertical" + android:src="@drawable/ic_received_indicator" /> + </LinearLayout> + </LinearLayout> + </LinearLayout> + + <ImageView + android:id="@+id/message_photo" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:layout_marginLeft="-1.5dp" + android:padding="0dp" + android:scaleType="fitXY" + android:src="@drawable/ic_profile" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/message_status.xml b/src/main/res/layout/message_status.xml new file mode 100644 index 00000000..d5f8bb33 --- /dev/null +++ b/src/main/res/layout/message_status.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="6dp" + android:paddingLeft="8dp" + android:paddingRight="6dp" + android:paddingTop="6dp" > + + <ImageView + android:id="@+id/message_photo" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_marginRight="-1.5dp" + android:padding="0dp" + android:scaleType="fitXY" + android:src="@drawable/ic_profile" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/src/main/res/layout/quickedit.xml b/src/main/res/layout/quickedit.xml new file mode 100644 index 00000000..20a2868a --- /dev/null +++ b/src/main/res/layout/quickedit.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="16dp" > + + <EditText + android:id="@+id/editor" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:inputType="textPersonName" + android:textColor="@color/primarytext" > + + <requestFocus /> + </EditText> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/layout/share_with.xml b/src/main/res/layout/share_with.xml new file mode 100644 index 00000000..41b6033d --- /dev/null +++ b/src/main/res/layout/share_with.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <ListView + android:id="@+id/choose_conversation_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:listitem="@layout/conversation_list_row" /> + +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/menu/attachment_choices.xml b/src/main/res/menu/attachment_choices.xml new file mode 100644 index 00000000..20932489 --- /dev/null +++ b/src/main/res/menu/attachment_choices.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/attach_choose_picture" + android:title="@string/attach_choose_picture"/> + <item + android:id="@+id/attach_take_picture" + android:title="@string/attach_take_picture"/> + <item + android:id="@+id/attach_record_voice" + android:title="@string/attach_record_voice" + android:visible="false"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/choose_contact.xml b/src/main/res/menu/choose_contact.xml new file mode 100644 index 00000000..e39091b3 --- /dev/null +++ b/src/main/res/menu/choose_contact.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/action_search" + android:actionLayout="@layout/actionview_search" + android:icon="@drawable/ic_action_search" + android:showAsAction="collapseActionView|always" + android:title="@string/search"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/conference_context.xml b/src/main/res/menu/conference_context.xml new file mode 100644 index 00000000..fd898580 --- /dev/null +++ b/src/main/res/menu/conference_context.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/context_join_conference" + android:title="@string/join_conference"/> + <item + android:id="@+id/context_delete_conference" + android:title="@string/delete_bookmark"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/contact_context.xml b/src/main/res/menu/contact_context.xml new file mode 100644 index 00000000..11ac7d7c --- /dev/null +++ b/src/main/res/menu/contact_context.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/context_start_conversation" + android:title="@string/start_conversation"/> + <item + android:id="@+id/context_contact_details" + android:title="@string/view_contact_details"/> + <item + android:id="@+id/context_delete_contact" + android:title="@string/delete_contact"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/contact_details.xml b/src/main/res/menu/contact_details.xml new file mode 100644 index 00000000..02f2e813 --- /dev/null +++ b/src/main/res/menu/contact_details.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/action_edit_contact" + android:icon="@drawable/ic_action_edit" + android:orderInCategory="10" + android:showAsAction="always" + android:title="@string/action_edit_contact"/> + <item + android:id="@+id/action_delete_contact" + android:icon="@drawable/ic_action_discard" + android:orderInCategory="10" + android:showAsAction="always" + android:title="@string/action_delete_contact"/> + <item + android:id="@+id/action_accounts" + android:orderInCategory="90" + android:showAsAction="never" + android:title="@string/action_accounts"/> + <item + android:id="@+id/action_settings" + android:orderInCategory="100" + android:showAsAction="never" + android:title="@string/action_settings"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/conversations.xml b/src/main/res/menu/conversations.xml new file mode 100644 index 00000000..3edee120 --- /dev/null +++ b/src/main/res/menu/conversations.xml @@ -0,0 +1,63 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/action_add" + android:icon="@drawable/ic_action_new" + android:orderInCategory="10" + android:showAsAction="always" + android:title="@string/action_add"/> + <item + android:id="@+id/action_security" + android:icon="@drawable/ic_action_not_secure" + android:orderInCategory="20" + android:showAsAction="always" + android:title="@string/action_secure"/> + <item + android:id="@+id/action_attach_file" + android:icon="@drawable/ic_action_new_attachment" + android:orderInCategory="30" + android:showAsAction="always" + android:title="@string/attach_file"/> + <item + android:id="@+id/action_contact_details" + android:orderInCategory="40" + android:showAsAction="never" + android:title="@string/action_contact_details"/> + <item + android:id="@+id/action_muc_details" + android:icon="@drawable/ic_action_group" + android:orderInCategory="40" + android:showAsAction="ifRoom" + android:title="@string/action_muc_details"/> + <item + android:id="@+id/action_invite" + android:orderInCategory="45" + android:showAsAction="never" + android:title="@string/invite_contact"/> + <item + android:id="@+id/action_clear_history" + android:orderInCategory="50" + android:showAsAction="never" + android:title="@string/action_clear_history"/> + <item + android:id="@+id/action_archive" + android:orderInCategory="60" + android:showAsAction="never" + android:title="@string/action_end_conversation"/> + <item + android:id="@+id/action_mute" + android:orderInCategory="70" + android:showAsAction="never" + android:title="@string/disable_notifications"/> + <item + android:id="@+id/action_accounts" + android:orderInCategory="90" + android:showAsAction="never" + android:title="@string/action_accounts"/> + <item + android:id="@+id/action_settings" + android:orderInCategory="100" + android:showAsAction="never" + android:title="@string/action_settings"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/encryption_choices.xml b/src/main/res/menu/encryption_choices.xml new file mode 100644 index 00000000..adf0ad8d --- /dev/null +++ b/src/main/res/menu/encryption_choices.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <group android:checkableBehavior="single" > + <item + android:id="@+id/encryption_choice_none" + android:title="@string/encryption_choice_none"/> + <item + android:id="@+id/encryption_choice_otr" + android:title="@string/encryption_choice_otr"/> + <item + android:id="@+id/encryption_choice_pgp" + android:title="@string/encryption_choice_pgp"/> + </group> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/manageaccounts.xml b/src/main/res/menu/manageaccounts.xml new file mode 100644 index 00000000..b5cd9b50 --- /dev/null +++ b/src/main/res/menu/manageaccounts.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/action_add_account" + android:icon="@drawable/ic_action_add_person" + android:showAsAction="always" + android:title="@string/action_add_account"/> + <item + android:id="@+id/action_settings" + android:orderInCategory="100" + android:showAsAction="never" + android:title="@string/action_settings"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/manageaccounts_context.xml b/src/main/res/menu/manageaccounts_context.xml new file mode 100644 index 00000000..7a7cc0a2 --- /dev/null +++ b/src/main/res/menu/manageaccounts_context.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/mgmt_account_enable" + android:title="@string/mgmt_account_enable"/> + <item + android:id="@+id/mgmt_account_publish_avatar" + android:title="@string/mgmt_account_publish_avatar"/> + <item + android:id="@+id/mgmt_account_announce_pgp" + android:title="@string/mgmt_account_publish_pgp"/> + <item + android:id="@+id/mgmt_account_disable" + android:showAsAction="never" + android:title="@string/mgmt_account_disable"/> + <item + android:id="@+id/mgmt_account_delete" + android:title="@string/mgmt_account_delete"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml new file mode 100644 index 00000000..80d4d196 --- /dev/null +++ b/src/main/res/menu/message_context.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/copy_text" + android:title="@string/copy_text"/> + <item + android:id="@+id/share_image" + android:title="@string/share_image"/> + <item + android:id="@+id/copy_url" + android:title="@string/copy_original_url"/> + <item + android:id="@+id/send_again" + android:title="@string/send_again"/> + <item + android:id="@+id/download_image" + android:title="@string/download_image"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/muc_details.xml b/src/main/res/menu/muc_details.xml new file mode 100644 index 00000000..97369098 --- /dev/null +++ b/src/main/res/menu/muc_details.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/action_edit_subject" + android:icon="@drawable/ic_action_edit" + android:orderInCategory="10" + android:showAsAction="always" + android:title="@string/action_edit_subject"/> + <item + android:id="@+id/action_accounts" + android:orderInCategory="90" + android:showAsAction="never" + android:title="@string/action_accounts"/> + <item + android:id="@+id/action_settings" + android:orderInCategory="100" + android:showAsAction="never" + android:title="@string/action_settings"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/share_with.xml b/src/main/res/menu/share_with.xml new file mode 100644 index 00000000..cbd15c11 --- /dev/null +++ b/src/main/res/menu/share_with.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/action_add" + android:icon="@drawable/ic_action_new" + android:orderInCategory="10" + android:showAsAction="always" + android:title="@string/action_add"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/menu/start_conversation.xml b/src/main/res/menu/start_conversation.xml new file mode 100644 index 00000000..f7230169 --- /dev/null +++ b/src/main/res/menu/start_conversation.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/action_search" + android:actionLayout="@layout/actionview_search" + android:icon="@drawable/ic_action_search" + android:showAsAction="collapseActionView|always" + android:title="@string/search"/> + <item + android:id="@+id/action_create_contact" + android:icon="@drawable/ic_action_add_person" + android:showAsAction="always" + android:title="@string/create_contact"/> + <item + android:id="@+id/action_join_conference" + android:icon="@drawable/ic_action_add_group" + android:showAsAction="always" + android:title="@string/join_conference"/> + <item + android:id="@+id/action_accounts" + android:orderInCategory="90" + android:showAsAction="never" + android:title="@string/action_accounts"/> + <item + android:id="@+id/action_settings" + android:orderInCategory="100" + android:showAsAction="never" + android:title="@string/action_settings"/> + +</menu>
\ No newline at end of file diff --git a/src/main/res/values-ca/arrays.xml b/src/main/res/values-ca/arrays.xml new file mode 100644 index 00000000..ff1a0d4b --- /dev/null +++ b/src/main/res/values-ca/arrays.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Mòbil</item> + <item>Telèfon</item> + <item>Tauleta</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>mai</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml new file mode 100644 index 00000000..cfbe428b --- /dev/null +++ b/src/main/res/values-ca/strings.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Preferències</string> + <string name="action_add">Nova conversa</string> + <string name="action_accounts">Gestionar comptes</string> + <string name="action_end_conversation">Finalitzar conversa</string> + <string name="action_contact_details">Detalls del contacte</string> + <string name="action_muc_details">Detalls de la conferència</string> + <string name="action_secure">Conversa segura</string> + <string name="action_add_account">Afegir compte</string> + <string name="just_now">ara</string> + <string name="sending">enviant…</string> + <string name="encrypted_message">Desxifrant missatge. Espera si us plau…</string> + <string name="nick_in_use">El sobrenom ja està en ús</string> + <string name="moderator">Moderador</string> + <string name="participant">Participant</string> + <string name="visitor">Visitant</string> + <string name="remove_contact_text">Vols eliminar a %s de la teva llista?. La conversa associada a aquest compte no s\'eliminarà.</string> + <string name="register_account">Registrar nou compte al servidor</string> + <string name="share_with">Compartir amb</string> + <string name="start_conversation">Començar conversa</string> + <string name="cancel">Cancel·lar</string> + <string name="crash_report_title">Conversations s\'ha aturat.</string> + <string name="crash_report_message">Enviant bolcats de piles ajudes al desenvolupament de Conversations\n<b>Avís:</b> Això usarà el teu compte XMPP per enviar el bolcat de pila al desenvolupador.</string> + <string name="send_now">Enviar ara</string> + <string name="send_never">No preguntar de nou</string> + <string name="problem_connecting_to_account">No s\'ha pogut connectar al compte</string> + <string name="problem_connecting_to_accounts">No s\'ha pogut connectar a múltiples comptes</string> + <string name="touch_to_fix">Prem aqui per gestionar els teus comptes</string> + <string name="attach_file">Enviar arxiu</string> + <string name="not_in_roster">El contacte no està a la teva llista. Vols afegir-lo?</string> + <string name="add_contact">Afefgir contacte</string> + <string name="send_failed">Error a l\'enviar</string> + <string name="send_rejected">rebutjat</string> + <string name="receiving_image">Rebent arxiu d\'imatge. Espera si us plau…</string> + <string name="preparing_image">Preparant imatge per enviar</string> + <string name="action_clear_history">Netejar historial</string> + <string name="clear_conversation_history">Netejar historial de conversa</string> + <string name="clear_histor_msg">Vols esborrar tots els missatges d\'aquesta conversa?\n\n<b>Avís:</b> Això no afectarà els missatges desats en altres dispositius o servidors.</string> + <string name="delete_messages">Esborrar missatges</string> + <string name="also_end_conversation">Finalitzar aquesta conversa més tard</string> + <string name="choose_presence">Selecciona recurs del contacte</string> + <string name="send_plain_text_message">Enviar missatge de text</string> + <string name="send_otr_message">Enviar missatge xifrat amb OTR</string> + <string name="send_pgp_message">Enviar missatge xifrat amb OpenPGP</string> + <string name="your_nick_has_been_changed">El teu sobrenom s\'ha modificat</string> + <string name="download_image">Descarregar imatge</string> + <string name="image_offered_for_download"><i>Fitxer d\'imatge ofert per a descàrrega</i></string> + <string name="send_unencrypted">Enviar sense xifrar</string> + <string name="decryption_failed">Ha fallat el desxiframent. Potser no tinguis la clau privada apropiada.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations utilitza una aplicació de tercers anomenada <b>OpenKeychain</b> per xifrar i desxifrar missatges i gestionar les teves claus públiques..\n\nOpenKeychain està publicat sota llicència GPLv3 i disponible a la F-Droid i Google Play.\n\n<small>(Si us plau, reinicieu Conversations després.)</small></string> + <string name="restart">Reiniciar</string> + <string name="install">Instal·lar</string> + <string name="offering">oferint…</string> + <string name="no_pgp_key">Clau OpenPGP no trobada</string> + <string name="contact_has_no_pgp_key">Conversations no ha pogut xifrar els teus missatges perquè el teu contacte no està anunciant la seva clau pública.\n\n<small>Si us plau, demana al teu contacte que configuri OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Missatge xifrat rebut. Prem per desxifrar i veure-ho.</i></string> + <string name="encrypted_image_received"><i>Imatge xifrada rebuda. Prem per desxifrar i veure-la.</i></string> + <string name="image_file"><i>Imatge rebuda. Prem per veure</i></string> + <string name="pref_xmpp_resource">Recursos XMPP</string> + <string name="pref_xmpp_resource_summary">El nom que identifica aquest client amb</string> + <string name="pref_accept_files">Acceptar fitxers</string> + <string name="pref_accept_files_summary">Accepta fitxers automàticament amb una mida menor a…</string> + <string name="pref_notification_settings">Ajustos de notificacions</string> + <string name="pref_notifications">Notificacions</string> + <string name="pref_notifications_summary">Notifica quan arriba un nou missatge</string> + <string name="pref_vibrate">Vibra</string> + <string name="pref_vibrate_summary">Vibra quan arriba un nou missatge</string> + <string name="pref_sound">So</string> + <string name="pref_sound_summary">Reprodueix el to de trucada amb la notificació</string> + <string name="pref_conference_notifications">Notificacions de conferència</string> + <string name="pref_conference_notifications_summary">Sempre notifica quan arriba un nou missatge de conferència en comptes de només quan està destacat</string> + <string name="pref_notification_grace_period">Notificació del període d\'espera</string> + <string name="pref_notification_grace_period_summary">Desactiva les notificacions durant un breu termini després de rebre una còpia de missatges carbon</string> + <string name="pref_advanced_options">Opcions avançades</string> + <string name="pref_never_send_crash">Mai enviïs informes d\'errors</string> + <string name="pref_never_send_crash_summary">Enviant traces d\'execució ajudes al futur desenvolupament del Conversations.</string> + <string name="pref_ui_options">Opcions de UI</string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-cs/arrays.xml b/src/main/res/values-cs/arrays.xml new file mode 100644 index 00000000..4510cf84 --- /dev/null +++ b/src/main/res/values-cs/arrays.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Mobil</item> + <item>Telefon</item> + <item>Tablet</item> + <item>Konverzace</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>nikdy</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + <string-array name="mute_options_descriptions"> + <item>30 minut</item> + <item>jedna hodina</item> + <item>2 hodiny</item> + <item>8 hodin</item> + <item>než opět změním</item> + </string-array> + + <integer-array name="mute_options_durations"> + <item>1800</item> + <item>3600</item> + <item>7200</item> + <item>28800</item> + <item>-1</item> + </integer-array> + +</resources> diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml new file mode 100644 index 00000000..6c68fe75 --- /dev/null +++ b/src/main/res/values-cs/strings.xml @@ -0,0 +1,265 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Nastavení</string> + <string name="action_add">Nová konverzace</string> + <string name="action_accounts">Nastavení účtů</string> + <string name="action_end_conversation">Ukončit tuto konverzaci</string> + <string name="action_contact_details">Detaily kontaktu</string> + <string name="action_muc_details">Detaily konference</string> + <string name="action_secure">Zabezpečená konverzace</string> + <string name="action_add_account">Přidat účet</string> + <string name="action_edit_contact">Upravit jméno</string> + <string name="action_add_phone_book">Přidat do telefonního seznamu</string> + <string name="action_delete_contact">Smazat ze seznamu</string> + <string name="title_activity_manage_accounts">Nastavení účtů</string> + <string name="title_activity_settings">Nastavení</string> + <string name="title_activity_conference_details">Detaily konference</string> + <string name="title_activity_contact_details">Detaily kontaktu</string> + <string name="title_activity_conversations">Konverzace</string> + <string name="title_activity_sharewith">Sdílet s konverzací</string> + <string name="title_activity_start_conversation">Začít konverzaci</string> + <string name="title_activity_choose_contact">Vybrat kontakt</string> + <string name="just_now">právě teď</string> + <string name="minute_ago">před 1 minutou</string> + <string name="minutes_ago">před %d minutami</string> + <string name="unread_conversations">nepřečtené konverzace</string> + <string name="sending">odesílám…</string> + <string name="encrypted_message">Dešifruji zprávu. Chvíli strpení…</string> + <string name="nick_in_use">Přezdívka se již používá</string> + <string name="admin">Administrátor</string> + <string name="owner">Vlastník</string> + <string name="moderator">Moderátor</string> + <string name="participant">Účastník</string> + <string name="visitor">Návštěvník</string> + <string name="remove_contact_text">Chcete odstranit %s ze svého seznamu? Konverzace spojené s tímto kontaktem nebudou odstraněny.</string> + <string name="remove_bookmark_text">Chcete odstranit %s ze záložek? Konverzace spojené s touto záložkou nebudou odstraněny.</string> + <string name="register_account">Registrovat nový účet na serveru</string> + <string name="share_with">Sdílet s</string> + <string name="start_conversation">Začít konverzaci</string> + <string name="invite_contact">Pozvat kontakt</string> + <string name="contacts">Kontakty</string> + <string name="cancel">Zrušit</string> + <string name="add">Přidat</string> + <string name="edit">Upravit</string> + <string name="delete">Smazat</string> + <string name="save">Uložit</string> + <string name="ok">OK</string> + <string name="crash_report_title">Aplikace Konverzace přestala reagovat</string> + <string name="crash_report_message">Zasláním detailů o důvodu selhání pomůžete dalšímu vývoji aplikace Konverzace\n<b>Varování:</b> Toto použije nastavený XMPP účet pro zaslání detailů vývojářům.</string> + <string name="send_now">Odeslat teď</string> + <string name="send_never">Již se neptat</string> + <string name="problem_connecting_to_account">Připojení k účtu se nezdařilo</string> + <string name="problem_connecting_to_accounts">Připojení k několika účtům se nezdařilo</string> + <string name="touch_to_fix">Pro nastavení účtů tapni zde</string> + <string name="attach_file">Přiložit soubor</string> + <string name="not_in_roster">Kontakt není v seznamu. Chcete ho přidat?</string> + <string name="add_contact">Přidat kontakt</string> + <string name="send_failed">doručení selhalo</string> + <string name="send_rejected">zamítnuto</string> + <string name="receiving_image">Přijímám obrázek. Chvíli strpení…</string> + <string name="preparing_image">Připravuji obrázek na přenos</string> + <string name="action_clear_history">Smazat historii</string> + <string name="clear_conversation_history">Smaže historii konverzací</string> + <string name="clear_histor_msg">Chcete smazat všechny zprávy v této konverzaci?\n\n<b>Varování:</b> Toto neovlivní zprávy uložené na jiných přístrojích nebo serverech.</string> + <string name="delete_messages">Smazat zprávy</string> + <string name="also_end_conversation">Poté ukončit i tuto konverzaci</string> + <string name="choose_presence">Vybrat aktualizaci stavu pro kontakt</string> + <string name="send_plain_text_message">Poslat textovou zprávu</string> + <string name="send_otr_message">Poslat OTR šifrovanou zprávu</string> + <string name="send_pgp_message">Poslat OpenPGP šifrovanou zprávu</string> + <string name="your_nick_has_been_changed">Přezdívka byla změněna</string> + <string name="download_image">Stáhnout obrázek</string> + <string name="image_offered_for_download"><i>Byl nabídnut obrázek ke stažení</i></string> + <string name="send_unencrypted">Poslat nešifrované</string> + <string name="decryption_failed">Zašifrování se nezdařilo. Možná nemáte správný privátní klíč.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Konverzace využívá aplikaci třetí strany, <b>OpenKeychain</b>, k šifrování a dešifrování zpráv a ke správě veřejných klíčů.\n\nOpenKeychain je licencován pod GPLv3 a dostupný na F-Droid a Google Play.\n\n<small>(Po instalaci prosím restartujte aplikaci Konverzace.)</small></string> + <string name="restart">Restartovat</string> + <string name="install">Instalovat</string> + <string name="offering">nabízí…</string> + <string name="waiting">čekám…</string> + <string name="no_pgp_key">Nebyl nalezen žádný OpenPGP klíč</string> + <string name="contact_has_no_pgp_key">Není možné zašifrovat zprávu v aplikaci Konverzace, protože druhá strana neoznamuje svůj veřejný klíč.\n\n<small>Požádejte svůj kontakt ať si nastaví OpenPGP.</small></string> + <string name="no_pgp_keys">Nebyly nalezeny žádné OpenPGP klíče</string> + <string name="contacts_have_no_pgp_keys">Není možné zašifrovat zprávy v aplikaci Konverzace, protože kontakty neoznamují svůj veřejný klíč.\n\n<small>Požádejte své kontakty ať si nastaví OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Byla přijata šifrovaná zpráva. Tapni pro dešifrování a přečtení.</i></string> + <string name="encrypted_image_received"><i>Byl přijat šifrovaný obrázek. Tapni pro dešifrování a prohlédnutí.</i></string> + <string name="image_file"><i>Byl přijat obrázek. Tapni pro prohlédnutí</i></string> + <string name="pref_general">Obecné</string> + <string name="pref_xmpp_resource">XMPP zdroj</string> + <string name="pref_xmpp_resource_summary">Jméno se kterým se tento klient identifikuje</string> + <string name="pref_accept_files">Přijímat soubory</string> + <string name="pref_accept_files_summary">Automaticky přijímat soubory menší než…</string> + <string name="pref_notification_settings">Nastavení upozornění</string> + <string name="pref_notifications">Upozornění</string> + <string name="pref_notifications_summary">Upozornit při přijetí nové zprávy</string> + <string name="pref_vibrate">Vibrovat</string> + <string name="pref_vibrate_summary">Vibrovat při přijetí nové zprávy</string> + <string name="pref_sound">Zvuk</string> + <string name="pref_sound_summary">Přehrát zvuk společně s upozorněním</string> + <string name="pref_conference_notifications">Upozornění při konferencích</string> + <string name="pref_conference_notifications_summary">Vždy upozorňovat při nové konferenční zprávě, nejen pokud je vybrána</string> + <string name="pref_notification_grace_period">Četnost upozornění</string> + <string name="pref_notification_grace_period_summary">Neupozorňovat krátce poté co byla obdržena kopie zprávy</string> + <string name="pref_advanced_options">Pokročilé nastavení</string> + <string name="pref_never_send_crash">Neodesílat detaily o pádu aplikace</string> + <string name="pref_never_send_crash_summary">Zasláním detailů o důvodu selhání pomůžete dalšímu vývoji aplikace Konverzace</string> + <string name="pref_confirm_messages">Potvrzovat zprávy</string> + <string name="pref_confirm_messages_summary">Dá vědět kontaktům, že zpráva byla přijata a přečtena</string> + <string name="pref_ui_options">Nastavení UI</string> + <string name="openpgp_error">OpenKeychain nahlásil chybu</string> + <string name="error_decrypting_file">I/O chyba dešifrování souboru</string> + <string name="accept">Přijmout</string> + <string name="error">Došlo k chybě</string> + <string name="pref_grant_presence_updates">Povolit aktualizace stavu</string> + <string name="pref_grant_presence_updates_summary">Aktivně povolovat a žádat o zasílání změn stavu pro vytvářené kontakty</string> + <string name="subscriptions">Odběry</string> + <string name="your_account">Váš účet</string> + <string name="keys">Klíče</string> + <string name="send_presence_updates">Zasílat změny stavu</string> + <string name="receive_presence_updates">Přijímat změny stavu</string> + <string name="ask_for_presence_updates">Zažádat o změny stavu</string> + <string name="attach_choose_picture">Vybrat obrázek</string> + <string name="attach_take_picture">Vyfotit obrázek</string> + <string name="preemptively_grant">Aktivně povolovat vyžádání změnu stavu</string> + <string name="error_not_an_image_file">Vybraný soubor není obrázek</string> + <string name="error_compressing_image">Chyba při konverzi obrázkového souboru</string> + <string name="error_file_not_found">Soubor nenalezen</string> + <string name="error_io_exception">Obecná I/O chyba. Že by již nebylo volné místo?</string> + <string name="error_security_exception_during_image_copy">Aplikace, která byla vybrána pro výběr obrázku, nepovolila přečtení souboru.\n\n<small>Zkuste použít jiného správce souborů pro výběr obrázku</small></string> + <string name="account_status_unknown">Neznámý</string> + <string name="account_status_disabled">Dočasně vypnuto</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">Připojuji\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Nepřihlášen</string> + <string name="account_status_not_found">Server nenalezen</string> + <string name="account_status_no_internet">Žádné připojení</string> + <string name="account_status_regis_fail">Registrace selhala</string> + <string name="account_status_regis_conflict">Uživatelské jméno se již používá</string> + <string name="account_status_regis_success">Registrace dokončena</string> + <string name="account_status_regis_not_sup">Server nepodporuje registrace</string> + <string name="encryption_choice_none">Čistý text</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Upravit účet</string> + <string name="mgmt_account_delete">Smazat účet</string> + <string name="mgmt_account_disable">Dočasně vypnout</string> + <string name="mgmt_account_publish_avatar">Zveřejnit avatar</string> + <string name="mgmt_account_publish_pgp">Zveřejnit OpenPGP klíč</string> + <string name="mgmt_account_enable">Povolit účet</string> + <string name="mgmt_account_are_you_sure">Jste si jisti?</string> + <string name="mgmt_account_delete_confirm_text">Pokud smažete svůj účet celá historie konverzací bude ztracena</string> + <string name="attach_record_voice">Nahrát hlas</string> + <string name="account_settings_jabber_id">Jabber ID</string> + <string name="account_settings_password">Heslo</string> + <string name="account_settings_example_jabber_id">jmeno@server.cz</string> + <string name="account_settings_confirm_password">Potvrdit heslo</string> + <string name="password">Heslo</string> + <string name="confirm_password">Potvrdit heslo</string> + <string name="passwords_do_not_match">Hesla nesouhlasí</string> + <string name="invalid_jid">Toto není platné Jabber ID</string> + <string name="error_out_of_memory">Nedostatek paměti. Obrázek je příliš velký</string> + <string name="add_phone_book_text">Chcete přidat %s do svého telefonního seznamu?</string> + <string name="contact_status_online">online</string> + <string name="contact_status_free_to_chat">volný pro chat</string> + <string name="contact_status_away">pryč</string> + <string name="contact_status_extended_away">rozšířené pryč</string> + <string name="contact_status_do_not_disturb">nerušit</string> + <string name="contact_status_offline">offline</string> + <string name="muc_details_conference">Konference</string> + <string name="muc_details_other_members">Ostatní členové</string> + <string name="server_info_carbon_messages">XEP-0280: Kopie zpráv</string> + <string name="server_info_stream_management">XEP-0198: Nastavení proudu</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="server_info_available">dostupný</string> + <string name="server_info_unavailable">nedostupný</string> + <string name="missing_public_keys">Chybí oznámení o veřejném klíči</string> + <string name="last_seen_now">právě spatřen</string> + <string name="last_seen_min">naposledy spatřen před 1 minutou</string> + <string name="last_seen_mins">naposledy spatřen před %d minutami</string> + <string name="last_seen_hour">naposledy spatřen před 1 hodinou</string> + <string name="last_seen_hours">naposledy spatřen před %d hodinami</string> + <string name="last_seen_day">naposledy spatřen před 1 dnem</string> + <string name="last_seen_days">naposledy spatřen před %d dny</string> + <string name="never_seen">nebyl nikdy spatřen</string> + <string name="install_openkeychain">Šifrovaná zpráva. Nainstaluje prosím OpenKeychain pro dešifrování.</string> + <string name="unknown_otr_fingerprint">Neznámý OTR identifikátor</string> + <string name="openpgp_messages_found">Nalezena OpenPGP šifrovaná zpráva</string> + <string name="reception_failed">Příjem selhal</string> + <string name="your_fingerprint">Váš identifikátor</string> + <string name="otr_fingerprint">OTR identifikátor</string> + <string name="verify">Ověřit</string> + <string name="decrypt">Dešifrovat</string> + <string name="conferences">Konference</string> + <string name="search">Hledat</string> + <string name="create_contact">Vytvořit kontakt</string> + <string name="join_conference">Připojit ke konferenci</string> + <string name="delete_contact">Smazat kontakt</string> + <string name="view_contact_details">Zobrazit detaily kontaktu</string> + <string name="create">Vytvořit</string> + <string name="contact_already_exists">Kontakt již existuje</string> + <string name="join">Vstoupit</string> + <string name="conference_address">Adresa konference</string> + <string name="conference_address_example">mistnost@konference.server.cz</string> + <string name="save_as_bookmark">Uložit jako záložku</string> + <string name="delete_bookmark">Smazat záložku</string> + <string name="bookmark_already_exists">Tato záložka již existuje</string> + <string name="you">Já</string> + <string name="action_edit_subject">Upravit jméno konference</string> + <string name="conference_not_found">Konference nenalezena</string> + <string name="leave">Odejít</string> + <string name="contact_added_you">Kontakt přidán do seznamu</string> + <string name="add_back">Opět přidat</string> + <string name="contact_has_read_up_to_this_point">%s dočetl až sem</string> + <string name="publish">Zveřejnit</string> + <string name="touch_to_choose_picture">Tapnout na avatar a vybrat obrázek z galerie</string> + <string name="publish_avatar_explanation">Pozor: Každý s povolením vidět změny stavu uvidí tento obrázek.</string> + <string name="publishing">Zveřejňuji…</string> + <string name="error_publish_avatar_server_reject">Server odmítl toto zveřejnění</string> + <string name="error_publish_avatar_converting">Při konverzi obrázku se něco nezdařilo</string> + <string name="error_saving_avatar">Nepodařilo se uložit avatar na disk</string> + <string name="or_long_press_for_default">(Stisknout dlouze pro obnovení výchozího stavu)</string> + <string name="error_publish_avatar_no_server_support">Váš server nepodporuje zveřejňování avataru</string> + <string name="private_message">šeptem</string> + <string name="private_message_to">pro %s</string> + <string name="send_private_message_to">Zaslat soukromou zprávu pro %s</string> + <string name="connect">Připojit</string> + <string name="account_already_exists">Tentou účet již existuje</string> + <string name="next">Další</string> + <string name="server_info_session_established">Současné sezení vytvořeno</string> + <string name="additional_information">Dodatečné informace</string> + <string name="skip">Přeskočit</string> + <string name="disable_notifications">Vypnout upozornění</string> + <string name="disable_notifications_for_this_conversation">Vypnout upozornění pro tuto konverzaci</string> + <string name="notifications_disabled">Upozornění jsou vypnuta</string> + <string name="enable">Povolit</string> + <string name="conference_requires_password">Konference vyžaduje heslo</string> + <string name="enter_password">Vložit heslo</string> + <string name="missing_presence_updates">Kontakt nezasílá informace o změně stavu</string> + <string name="request_presence_updates">Nejdříve si prosím vyžádejte povolení o zasílání změn stavu kontatku.\n\n<small>To bude poté použito pro zjištění jakou aplikaci tento kontakt používá.</small></string> + <string name="request_now">Ihned vyžádat</string> + <string name="delete_fingerprint">Smazat identifikátor</string> + <string name="sure_delete_fingerprint">Chcete opravdu smazat tento identifikátor?</string> + <string name="ignore">Ignorovat</string> + <string name="without_mutual_presence_updates"><b>Varování:</b> Odeslání bez povolení změn stavu může způsobit nečekané problémy na obou stranách.\n\n<small>Přejdi na detaily kontaktu pro ověření povolení o změnách stavu.</small></string> + <string name="pref_encryption_settings">Nastavení šifrování</string> + <string name="pref_force_encryption">Vynutit šifrování</string> + <string name="pref_force_encryption_summary">Vždy zasílat šifrované zprávy (mimo konference)</string> + <string name="pref_dont_save_encrypted">Neukládat šifrované zprávy</string> + <string name="pref_dont_save_encrypted_summary">Varování: Toto může vést ke ztrátě zpráv</string> + <string name="pref_expert_options">Expertní nastavení</string> + <string name="pref_expert_options_summary">S tímto zacházejte velmi opatrně</string> + <string name="pref_use_larger_font">Zvětšit velikost písma</string> + <string name="pref_use_larger_font_summary">Použít větší písmo v celé aplikaci</string> + <string name="pref_use_send_button_to_indicate_status">Tlačítko pro odeslání zobrazuje stav</string> + <string name="pref_use_indicate_received">Požadovat oznámení o přijetí</string> + <string name="pref_use_indicate_received_summary">Přijaté zprávy budou označeny zeleně. Toto nemusí vždy plně fungovat.</string> + <string name="pref_use_send_button_to_indicate_status_summary">Obarvit tlačítko pro odeslání barvou stavu kontaktu</string> + <string name="pref_expert_options_other">Další</string> + <string name="pref_conference_name">Jméno konference</string> + <string name="pref_conference_name_summary">Pro identifikaci konferencí použít téma místnosti místo jejího JID</string> + +</resources> diff --git a/src/main/res/values-de/arrays.xml b/src/main/res/values-de/arrays.xml new file mode 100644 index 00000000..ed5d47b5 --- /dev/null +++ b/src/main/res/values-de/arrays.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Mobile</item> + <item>Phone</item> + <item>Tablet</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>nie</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + <string-array name="mute_options_descriptions"> + <item>30 Minuten</item> + <item>1 Stunde</item> + <item>2 Stunden</item> + <item>8 Stunden</item> + <item>bis auf Widerruf</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..ca190deb --- /dev/null +++ b/src/main/res/values-de/strings.xml @@ -0,0 +1,285 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Einstellungen</string> + <string name="action_add">Neue Unterhaltung</string> + <string name="action_accounts">Konten verwalten</string> + <string name="action_end_conversation">Unterhaltung beenden</string> + <string name="action_contact_details">Kontaktdetails</string> + <string name="action_muc_details">Konferenzdetails</string> + <string name="action_secure">Verschlüsselte Unterhaltung</string> + <string name="action_add_account">Konto hinzufügen</string> + <string name="action_edit_contact">Name bearbeiten</string> + <string name="action_add_phone_book">Zum Telefonbuch hinzufügen</string> + <string name="action_delete_contact">Aus Kontaktliste entfernen</string> + <string name="title_activity_manage_accounts">Konten verwalten</string> + <string name="title_activity_settings">Einstellungen</string> + <string name="title_activity_conference_details">Konferenzdetails</string> + <string name="title_activity_contact_details">Kontaktdetails</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Mit Unterhaltung teilen</string> + <string name="title_activity_start_conversation">Beginne Unterhaltung</string> + <string name="title_activity_choose_contact">Kontakt auswählen</string> + <string name="just_now">gerade</string> + <string name="minute_ago">vor einer Minute</string> + <string name="minutes_ago">vor %d Minuten</string> + <string name="unread_conversations">ungelesene Unterhaltungen</string> + <string name="sending">senden…</string> + <string name="encrypted_message">Entschlüssele Nachricht. Bitte warten…</string> + <string name="nick_in_use">Nickname wird bereits verwendet</string> + <string name="admin">Administrator</string> + <string name="owner">Eigentümer</string> + <string name="moderator">Moderator</string> + <string name="participant">Teilnehmer</string> + <string name="visitor">Besucher</string> + <string name="remove_contact_text">Möchtest du %s von deiner Kontaktliste entfernen? Die Unterhaltung mit diesem Kontakt wird dabei nicht entfernt.</string> + <string name="remove_bookmark_text">Möchtest du das Lesezeichen %s entfernen? Die Unterhaltung mit diesem Lesezeichen wird dabei nicht entfernt.</string> + <string name="register_account">Neues Konto auf dem Server erstellen</string> + <string name="share_with">Teile mit…</string> + <string name="start_conversation">Beginne Unterhaltung</string> + <string name="invite_contact">Kontakt einladen</string> + <string name="contacts">Kontakte</string> + <string name="cancel">Abbrechen</string> + <string name="add">Hinzufügen</string> + <string name="edit">Bearbeiten</string> + <string name="delete">Entfernen</string> + <string name="save">Speichern</string> + <string name="ok">OK</string> + <string name="crash_report_title">Conversations ist abgestürzt</string> + <string name="crash_report_message">Durch das Einsenden von Fehlerberichten hilfst du bei der stetigen Verbesserung von Conversations.\n<b>Achtung:</b> Dies wird eines deiner XMPP-Konten benutzen, um den Entwickler zu kontaktieren.</string> + <string name="send_now">Jetzt abschicken</string> + <string name="send_never">Nie mehr nachfragen</string> + <string name="problem_connecting_to_account">Es gibt Probleme beim Verbindungsaufbau mit einem Konto</string> + <string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Konten</string> + <string name="touch_to_fix">Drücke hier, um das Konto zu verwalten</string> + <string name="attach_file">Datei anfügen</string> + <string name="not_in_roster">Der Kontakt ist nicht in deiner Kontaktliste. Möchtest du ihn hinzufügen?</string> + <string name="add_contact">Kontakt hinzufügen</string> + <string name="send_failed">Zustellung nicht erfolgreich</string> + <string name="send_rejected">abgelehnt</string> + <string name="receiving_image">Empfange Bild. Bitte warten…</string> + <string name="preparing_image">Bereite Bild für die Übertragung vor</string> + <string name="action_clear_history">Verlauf löschen</string> + <string name="clear_conversation_history">Unterhaltungsverlauf löschen</string> + <string name="clear_histor_msg">Möchtest du alle Nachrichten in dieser Unterhaltung löschen?\n\n<b>Achtung:</b> Dies beeinflusst nicht Nachrichten, die auf anderen Geräten oder Servern gespeichert sind.</string> + <string name="delete_messages">Nachrichten löschen</string> + <string name="also_end_conversation">Diese Unterhaltung danach beenden</string> + <string name="choose_presence">Ressource des Kontakts auswählen</string> + <string name="send_plain_text_message">Unverschlüsselt schreiben</string> + <string name="send_otr_message">OTR-verschlüsselt schreiben</string> + <string name="send_pgp_message">OpenPGP-verschlüsselt schreiben</string> + <string name="your_nick_has_been_changed">Dein Nickname wurde geändert</string> + <string name="download_image">Bild herunterladen</string> + <string name="image_offered_for_download"><i>Bilddatei zum Download angeboten</i></string> + <string name="send_unencrypted">Unverschlüsselt verschicken</string> + <string name="decryption_failed">Entschlüsselung fehlgeschlagen. Vielleicht hast du nicht den richtigen privaten Schlüssel.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations benutzt eine Drittanwendung namens <b>OpenKeychain</b>, um Nachrichten zu ver- und entschlüsseln und um deine Schlüssel zu verwalten.\n\nOpenKeychain ist GPLv3-lizenziert und kann über F-Droid oder Google Play bezogen werden.\n\n<small>(Bitte starte Conversations danach neu.)</small></string> + <string name="restart">Neustarten</string> + <string name="install">Installieren</string> + <string name="offering">angeboten…</string> + <string name="waiting">warten…</string> + <string name="no_pgp_key">Kein OpenPGP-Schlüssel gefunden</string> + <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge OpenPGP einrichten.</small></string> + <string name="no_pgp_keys">Keine OpenPGP-Schlüssel gefunden</string> + <string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil deine Kontakte ihre Schlüssel nicht preisgeben.\n\n<small>Bitte sag deinen Kontakten, sie mögen OpenPGP einrichten.</small></string> + <string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier, um sie anzuzeigen und zu entschlüsseln.</i></string> + <string name="encrypted_image_received"><i>Verschlüsseltes Bild erhalten. Drücke hier, um es anzuzeigen und zu entschlüsseln.</i></string> + <string name="image_file"><i>Bild erhalten. Drücke hier, um es anzuzeigen.</i></string> + <string name="pref_general">Allgemein</string> + <string name="pref_xmpp_resource">XMPP-Ressource</string> + <string name="pref_xmpp_resource_summary">Der Name, mit dem sich der Client selbst identifiziert</string> + <string name="pref_accept_files">Dateiannahme</string> + <string name="pref_accept_files_summary">Dateien, die kleiner sind als …, automatisch annehmen</string> + <string name="pref_notification_settings">Benachrichtigungseinstellungen</string> + <string name="pref_notifications">Benachrichtigungen</string> + <string name="pref_notifications_summary">Benachrichtige mich, wenn eine neue Nachricht ankommt</string> + <string name="pref_vibrate">Vibrieren</string> + <string name="pref_vibrate_summary">Vibriere, wenn eine neue Nachricht ankommt</string> + <string name="pref_sound">Klingelton</string> + <string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</string> + <string name="pref_conference_notifications">Konferenz-Benachrichtigungen</string> + <string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde</string> + <string name="pref_notification_grace_period">Gnadenfrist</string> + <string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.</string> + <string name="pref_advanced_options">Erweiterte Optionen</string> + <string name="pref_never_send_crash">Sende niemals Absturzberichte</string> + <string name="pref_never_send_crash_summary">Wenn du Absturzberichte einschickst, hilfst du Conversations stetig zu verbessern</string> + <string name="pref_confirm_messages">Lesebestätigung senden</string> + <string name="pref_confirm_messages_summary">Informiere deine Kontakte, wenn du eine Nachricht empfängst oder liest</string> + <string name="openpgp_error">Fehler mit OpenKeychain</string> + <string name="error_decrypting_file">Fehler beim Entschlüsseln der Datei</string> + <string name="accept">Annehmen</string> + <string name="error">Ein unbekannter Fehler ist aufgetreten</string> + <string name="pref_grant_presence_updates">Online-Status</string> + <string name="pref_grant_presence_updates_summary">Erlaube Kontakten, die von dir erstellt wurden, deinen Status zu sehen und frage um Erlaubnis, ihren sehen zu dürfen</string> + <string name="subscriptions">Abonnements</string> + <string name="your_account">Dein Konto</string> + <string name="keys">Schlüssel</string> + <string name="send_presence_updates">Anwesenheitsbenachrichtigungen senden</string> + <string name="receive_presence_updates">Empfange Anwesenheitsbenachrichtigungen</string> + <string name="ask_for_presence_updates">Frage um Erlaubnis, Anwesenheitsbenachrichtigungen sehen zu dürfen</string> + <string name="attach_choose_picture">Foto auswählen</string> + <string name="attach_take_picture">Foto aufnehmen</string> + <string name="preemptively_grant">Erlaube Statusanfrage vorab</string> + <string name="error_not_an_image_file">Die ausgewählte Datei ist kein Bild</string> + <string name="error_compressing_image">Fehler beim Umwandeln des Bildes</string> + <string name="error_file_not_found">Datei nicht gefunden</string> + <string name="error_io_exception">Allgemeiner Fehler. Vielleicht hast du keinen Speicherplatz mehr?</string> + <string name="error_security_exception_during_image_copy">Die App, mit der du das Bild ausgesucht hast, hat uns keine Rechte eingeräumt, das Bild zu betrachten.\n\n<small>Benutze einen anderen Dateimanager</small></string> + <string name="account_status_unknown">Unbekannt</string> + <string name="account_status_disabled">Vorübergehend abgeschaltet</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">Verbinde\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Ungültige Zugangsdaten</string> + <string name="account_status_not_found">Server nicht gefunden</string> + <string name="account_status_no_internet">Keine Internetverbindung</string> + <string name="account_status_regis_fail">Registrierung fehlgeschlagen</string> + <string name="account_status_regis_conflict">Benutzername wird bereits verwendet</string> + <string name="account_status_regis_success">Registrierung abgeschlossen</string> + <string name="account_status_regis_not_sup">Der Server unterstützt keine Registrierung</string> + <string name="encryption_choice_none">Klartext</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Konto bearbeiten</string> + <string name="mgmt_account_delete">Löschen</string> + <string name="mgmt_account_disable">Vorübergehend abschalten</string> + <string name="mgmt_account_publish_avatar">Avatar veröffentlichen</string> + <string name="mgmt_account_publish_pgp">Öffentlichen OpenPGP-Schlüssel veröffentlichen</string> + <string name="mgmt_account_enable">Anschalten</string> + <string name="mgmt_account_are_you_sure">Bist du dir sicher?</string> + <string name="mgmt_account_delete_confirm_text">Wenn du dein Konto löscht, gehen alle Gesprächsverläufe verloren</string> + <string name="attach_record_voice">Sprache aufzeichnen</string> + <string name="account_settings_jabber_id">Jabber-ID:</string> + <string name="account_settings_password">Passwort:</string> + <string name="account_settings_example_jabber_id">benutzer@domain.de</string> + <string name="account_settings_confirm_password">Passwort bestätigen</string> + <string name="password">Passwort</string> + <string name="confirm_password">Passwort bestätigen</string> + <string name="passwords_do_not_match">Passwörter stimmen nicht überein</string> + <string name="invalid_jid">Ungültige Jabber-ID</string> + <string name="error_out_of_memory">Zu wenig Speicher vorhanden. Das Bild ist zu groß</string> + <string name="add_phone_book_text">Möchtest du %s zum Telefonbuch hinzufügen?</string> + <string name="contact_status_online">Online</string> + <string name="contact_status_free_to_chat">Bereit</string> + <string name="contact_status_away">Abwesend</string> + <string name="contact_status_extended_away">Abwesend (erweitert)</string> + <string name="contact_status_do_not_disturb">Nicht stören</string> + <string name="contact_status_offline">Offline</string> + <string name="muc_details_conference">Konferenz</string> + <string name="muc_details_other_members">Andere Mitglieder</string> + <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> + <string name="server_info_stream_management">XEP-0198: Stream Management</string> + <string name="server_info_pep">XEP-0163: PEP (Avatare)</string> + <string name="server_info_available">verfügbar</string> + <string name="server_info_unavailable">nicht verfügbar</string> + <string name="missing_public_keys">Öffentlicher Schlüssel fehlt</string> + <string name="last_seen_now">Gerade online</string> + <string name="last_seen_min">Vor einer Minute gesehen</string> + <string name="last_seen_mins">Vor %d Minuten gesehen</string> + <string name="last_seen_hour">Vor einer Stunde gesehen</string> + <string name="last_seen_hours">Vor %d Stunden gesehen</string> + <string name="last_seen_day">Vor einem Tag gesehen</string> + <string name="last_seen_days">Vor %d Tagen gesehen</string> + <string name="never_seen">Noch nie gesehen</string> + <string name="install_openkeychain">Verschlüsselte Nachricht. Bitte installiere OpenKeychain zur Entschlüsselung.</string> + <string name="unknown_otr_fingerprint">Unbekannter OTR-Fingerabdruck</string> + <string name="openpgp_messages_found">Verschlüsselte OpenPGP-Nachricht gefunden</string> + <string name="reception_failed">Empfang ist fehlgeschlagen</string> + <string name="your_fingerprint">Dein Fingerabdruck</string> + <string name="otr_fingerprint">OTR-Fingerabdruck</string> + <string name="verify">Verifizieren</string> + <string name="decrypt">Entschlüsseln</string> + <string name="conferences">Konferenzen</string> + <string name="search">Suche</string> + <string name="create_contact">Kontakt erstellen</string> + <string name="join_conference">Konferenz beitreten</string> + <string name="delete_contact">Kontakt löschen</string> + <string name="view_contact_details">Kontaktdetails anzeigen</string> + <string name="create">Erstellen</string> + <string name="contact_already_exists">Der Kontakt existiert bereits</string> + <string name="join">Beitreten</string> + <string name="conference_address">Konferenzadresse</string> + <string name="conference_address_example">raum@conference.example.com</string> + <string name="save_as_bookmark">Als Lesezeichen speichern</string> + <string name="delete_bookmark">Lesezeichen löschen</string> + <string name="bookmark_already_exists">Das Lesezeichen existiert bereits</string> + <string name="you">Du</string> + <string name="action_edit_subject">Konferenzthema anpassen</string> + <string name="conference_not_found">Konferenz nicht gefunden</string> + <string name="leave">Verlassen</string> + <string name="contact_added_you">Der Kontakt hat dich zur Kontaktliste hinzugefügt</string> + <string name="add_back">Auch hinzufügen</string> + <string name="contact_has_read_up_to_this_point">%s hat bis zu diesem Punkt gelesen</string> + <string name="publish">Veröffentlichen</string> + <string name="touch_to_choose_picture">Klicke hier, um einen Avatar auszuwählen</string> + <string name="publish_avatar_explanation">Achtung: Jeder, der deinen Status sehen darf, sieht auch deinen Avatar.</string> + <string name="publishing">Veröffentliche…</string> + <string name="error_publish_avatar_server_reject">Der Server hat die Veröffentlichung des Avatars abgelehnt.</string> + <string name="error_publish_avatar_converting">Bei der Konvertierung des Avatars lief etwas schief.</string> + <string name="error_saving_avatar">Kann Avatar nicht speichern.</string> + <string name="or_long_press_for_default">(Oder klicke lange, um Standard wiederherzustellen)</string> + <string name="error_publish_avatar_no_server_support">Dein Server unterstützt die Veröffentlichung von Avataren nicht.</string> + <string name="private_message">private Nachricht</string> + <string name="private_message_to">an %s</string> + <string name="send_private_message_to">Sende private Nachricht an %s</string> + <string name="connect">Verbinden</string> + <string name="account_already_exists">Das Konto existiert bereits</string> + <string name="next">Weiter</string> + <string name="server_info_session_established">Aktuelle Sitzung wiederhergestellt</string> + <string name="additional_information">Zusätzliche Informationen</string> + <string name="skip">Überspringen</string> + <string name="pref_ui_options">Benutzeroberfläche</string> + <string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string> + <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string> + <string name="disable_notifications">Benachrichtigungen deaktivieren</string> + <string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string> + <string name="notifications_disabled">Benachrichtigungen sind deaktiviert</string> + <string name="enable">Aktivieren</string> + <string name="conference_requires_password">Konferenz ist passwortgeschützt</string> + <string name="enter_password">Passwort eingeben</string> + <string name="missing_presence_updates">Fehlender Online-Status vom Kontakt</string> + <string name="request_presence_updates">Bitte erst Anwesenheitsbenachrichtigungen vom Kontakt anfordern.\n\n</string> + <string name="request_now">Jetzt anfordern</string> + <string name="delete_fingerprint">Fingerabdruck löschen</string> + <string name="sure_delete_fingerprint">Soll dieser Fingerabdruck definitiv gelöscht werden?</string> + <string name="ignore">Ignorieren</string> + <string name="without_mutual_presence_updates"><b>Achtung:</b> Es kann zu unerwarteten Problemen führen, dies ohne gegenseitige Anwesenheitsbenachrichtigungen abzusenden.\n\n<small>Bitte die Online-Status-Abonnements in den Kontaktdetails prüfen.</small></string> + <string name="pref_encryption_settings">Verschlüsselungs-Einstellungen</string> + <string name="pref_force_encryption">Ende-zu-Ende-Verschlüsselung forcieren</string> + <string name="pref_force_encryption_summary">Nachrichten immer verschlüsseln (außer für Konferenzen)</string> + <string name="pref_dont_save_encrypted">Verschlüsselte Nachrichten nicht speichern</string> + <string name="pref_dont_save_encrypted_summary">Achtung: Kann zu Nachrichtenverlust führen</string> + <string name="pref_enable_legacy_ssl">Alte SSL-Version aktivieren</string> + <string name="pref_enable_legacy_ssl_summary">Aktiviert SSLv3-Unterstützung für alte Server. Achtung: SSLv3 ist unsicher.</string> + <string name="pref_expert_options">Einstellungen für Experten</string> + <string name="pref_expert_options_summary">Hier bitte vorsichtig sein</string> + <string name="pref_use_larger_font">Schrift vergrößern</string> + <string name="pref_use_larger_font_summary">Überall in der App eine größere Schrift verwenden</string> + <string name="pref_use_send_button_to_indicate_status">Absende-Knopf zeigt Online-Status an</string> + <string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string> + <string name="pref_expert_options_other">Sonstiges</string> + <string name="pref_conference_name">Konferenz-Name</string> + <string name="pref_conference_name_summary">Konferenz-Thema statt Raum-JID als Name verwenden</string> + <string name="toast_message_otr_fingerprint">OTR Fingerabdruck in die Zwischenablage kopiert!</string> + <string name="conference_banned">Du wurdest aus dem Konferenzraum verbannt</string> + <string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</string> + <string name="conference_kicked">Du wurdest aus dem Konferenzraum geworfen</string> + <string name="using_account">Verwende Konto %s</string> + <string name="checking_image">Prüfe Bild auf HTTP-Host</string> + <string name="image_file_deleted">Bilddatei wurde gelöscht</string> + <string name="not_connected_try_again">Nicht verbunden, bitte später versuchen</string> + <string name="check_image_filesize">Bildgröße prüfen</string> + <string name="message_options">Nachrichtenoptionen</string> + <string name="copy_text">Text kopieren</string> + <string name="share_image">Bild teilen</string> + <string name="copy_original_url">Original-URL kopieren</string> + <string name="send_again">Erneut senden</string> + <string name="image_url">Bild-URL</string> + <string name="message_text">Nachrichtentext</string> + <string name="url_copied_to_clipboard">URL in Zwischenablage kopiert</string> + <string name="message_copied_to_clipboard">Nachricht in Zwischenablage kopiert</string> + +</resources> diff --git a/src/main/res/values-es/arrays.xml b/src/main/res/values-es/arrays.xml new file mode 100644 index 00000000..15231955 --- /dev/null +++ b/src/main/res/values-es/arrays.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Móvil</item> + <item>Teléfono</item> + <item>Tablet</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>nunca</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + <string-array name="mute_options_descriptions"> + <item>30 minutos</item> + <item>1 hora</item> + <item>2 horas</item> + <item>8 horas</item> + <item>Hasta nuevo aviso</item> + </string-array> + + <integer-array name="mute_options_durations"> + <item>1800</item> + <item>3600</item> + <item>7200</item> + <item>28800</item> + <item>-1</item> + </integer-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml new file mode 100644 index 00000000..47424d00 --- /dev/null +++ b/src/main/res/values-es/strings.xml @@ -0,0 +1,285 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Ajustes</string> + <string name="action_add">Nueva conversación</string> + <string name="action_accounts">Gestionar cuentas</string> + <string name="action_end_conversation">Terminar conversación</string> + <string name="action_contact_details">Detalles del contacto</string> + <string name="action_muc_details">Detalles de la conferencia</string> + <string name="action_secure">Conversación segura</string> + <string name="action_add_account">Añadir cuenta</string> + <string name="action_edit_contact">Editar contacto</string> + <string name="action_delete_contact">Eliminar contacto de la lista</string> + <string name="action_add_phone_book">Añadir a contactos del teléfono</string> + <string name="title_activity_manage_accounts">Gestionar Cuentas</string> + <string name="title_activity_settings">Ajustes</string> + <string name="title_activity_conference_details">Detalles de Conferencia</string> + <string name="title_activity_contact_details">Detalles del Contacto</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Compartir con Conversación</string> + <string name="title_activity_start_conversation">Nueva Conversación</string> + <string name="title_activity_choose_contact">Elegir Contacto</string> + <string name="just_now">ahora</string> + <string name="minute_ago">hace 1 min</string> + <string name="minutes_ago">hace %d min</string> + <string name="unread_conversations">conversaciones por leer</string> + <string name="sending">enviando…</string> + <string name="encrypted_message">Desencriptando mensaje. Espera por favor…</string> + <string name="nick_in_use">El apodo ya está en uso</string> + <string name="admin">Administrador</string> + <string name="owner">Propietario</string> + <string name="moderator">Moderador</string> + <string name="participant">Participante</string> + <string name="visitor">Visitante</string> + <string name="remove_contact_text">¿Quieres eliminar a %s de tu lista? La conversación asociada a esta cuenta no se eliminará.</string> + <string name="remove_bookmark_text">¿Quieres eliminar %s de tus marcadores? La conversación de la conferencia asociada con este marcador no se eliminará.</string> + <string name="register_account">Registrar nueva cuenta en servidor</string> + <string name="share_with">Compartir con</string> + <string name="start_conversation">Comenzar conversación</string> + <string name="invite_contact">Invitar contactos</string> + <string name="contacts">Contactos</string> + <string name="cancel">Cancelar</string> + <string name="add">Añadir</string> + <string name="edit">Editar</string> + <string name="delete">Eliminar</string> + <string name="save">Guardar</string> + <string name="ok">OK</string> + <string name="crash_report_title">Conversations se ha detenido.</string> + <string name="crash_report_message">Si envías un informe de fallos ayudas al desarrollo de Conversations\n<b>Aviso:</b> Esto usará tu cuenta XMPP para enviar los registros de error al desarrollador.</string> + <string name="send_now">Enviar ahora</string> + <string name="send_never">No preguntar de nuevo</string> + <string name="problem_connecting_to_account">No se ha podido conectar a la cuenta</string> + <string name="problem_connecting_to_accounts">No se ha podido conectar a múltiples cuentas</string> + <string name="touch_to_fix">Pulsa aquí para gestionar tus cuentas</string> + <string name="attach_file">Adjuntar</string> + <string name="not_in_roster">El contacto no está en tu lista. ¿Te gustaría añadirlo?</string> + <string name="add_contact">Añadir contacto</string> + <string name="send_failed">Error al enviar</string> + <string name="send_rejected">rechazado</string> + <string name="receiving_image">Recibiendo archivo de imagen. Espera por favor…</string> + <string name="preparing_image">Preparando imagen para enviar</string> + <string name="action_clear_history">Limpiar historial</string> + <string name="clear_conversation_history">Limpiar historial de conversación</string> + <string name="clear_histor_msg">¿Quieres borrar todos los mensajes de esta conversación?\n\n<b>Aviso:</b> Esto no afectará a los mensajes guardados en otros dispositivos o servidores.</string> + <string name="delete_messages">Borrar mensajes</string> + <string name="also_end_conversation">Terminar esta conversación más tarde</string> + <string name="choose_presence">Selecciona recurso del contacto</string> + <string name="send_plain_text_message">Enviar mensaje de texto</string> + <string name="send_otr_message">Enviar mensaje encriptado con OTR</string> + <string name="send_pgp_message">Enviar mensaje encriptado con OpenPGP</string> + <string name="your_nick_has_been_changed">Tu apodo se ha modificado</string> + <string name="download_image">Descargar imagen</string> + <string name="image_offered_for_download"><i>Archivo de imagen ofrecido para descarga</i></string> + <string name="send_unencrypted">Enviar sin encriptar</string> + <string name="decryption_failed">Falló la desencriptación. Tal vez no tengas la clave privada apropiada.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations utiliza una aplicación de terceros llamada <b>OpenKeychain</b> para encriptar y desencriptar mensajes y gestionar tus claves públicas.\n\nOpenKeychain está publicado bajo licencia GPLv3 y disponible on F-Droid y Google Play.\n\n<small>(Por favor, reinicie Conversations después.)</small></string> + <string name="restart">Reiniciar</string> + <string name="install">Instalar</string> + <string name="offering">ofreciendo…</string> + <string name="waiting">esperando…</string> + <string name="no_pgp_key">Clave OpenPGP no encontrada</string> + <string name="contact_has_no_pgp_key">Conversations no ha podido encriptar tus mensajes porque el contacto no está anunciando su clave publica.\n\n<small>Por favor, pide a tu contacto que configure OpenPGP.</small></string> + <string name="no_pgp_keys">Claves OpenPGP no encontradas</string> + <string name="contacts_have_no_pgp_keys">Conversations no ha podido encriptar tus mensajes porque tus contactos no están anunciando su clave publica.\n\n<small>Por favor, pide a tus contactos que configuren OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Mensaje encriptado recibido. Pulsa para ver.</i></string> + <string name="encrypted_image_received"><i>Imagen encriptada recibida. Pulsa para ver.</i></string> + <string name="image_file"><i>Imagen recibida. Pulsa para ver</i></string> + <string name="pref_general">General</string> + <string name="pref_xmpp_resource">Recurso</string> + <string name="pref_xmpp_resource_summary">El nombre que identifica el cliente que estás utilizando</string> + <string name="pref_accept_files">Aceptar archivos</string> + <string name="pref_accept_files_summary">De forma automática aceptar archivos menores que…</string> + <string name="pref_notification_settings">Ajustes de notificación</string> + <string name="pref_notifications">Notificaciones</string> + <string name="pref_notifications_summary">Notifica cuando llega un nuevo mensaje</string> + <string name="pref_vibrate">Vibrar</string> + <string name="pref_vibrate_summary">Vibra cuando llega un nuevo mensaje</string> + <string name="pref_sound">Sonido</string> + <string name="pref_sound_summary">Reproduce tono con la notificación</string> + <string name="pref_conference_notifications">Notificaciones de conferencia</string> + <string name="pref_conference_notifications_summary">Siempre notifica cuando llega un mensaje de conferencia y no solo cuando llega un mensaje destacado</string> + <string name="pref_notification_grace_period">Notificaciones Carbons</string> + <string name="pref_notification_grace_period_summary">Deshabilita las notificaciones durante un corto periodo de tiempo después de recibir la copia del mensaje carbon</string> + <string name="pref_advanced_options">Opciones avanzadas</string> + <string name="pref_never_send_crash">Nunca enviar informe de fallos</string> + <string name="pref_never_send_crash_summary">Si envías registros de error ayudas al desarrollo de Conversations</string> + <string name="pref_confirm_messages">Confirmar Mensajes</string> + <string name="pref_confirm_messages_summary">Permitir a tus contactos saber cuando recibes y lees un mensaje</string> + <string name="pref_ui_options">Opciones de interfaz</string> + <string name="openpgp_error">OpenKeychain reportó un error</string> + <string name="error_decrypting_file">Error desencriptando fichero</string> + <string name="accept">Aceptar</string> + <string name="error">Ha ocurrido un error</string> + <string name="pref_grant_presence_updates">Suscripción de presencia</string> + <string name="pref_grant_presence_updates_summary">De forma automática solicitar y conceder suscripciones de presencia de los contactos que has creado</string> + <string name="subscriptions">Suscripciones</string> + <string name="your_account">Tu cuenta</string> + <string name="keys">Claves</string> + <string name="send_presence_updates">Enviar actualizaciones de presencia</string> + <string name="receive_presence_updates">Recibir actualizaciones de presencia</string> + <string name="ask_for_presence_updates">Solicitar actualizaciones de presencia</string> + <string name="attach_choose_picture">Seleccionar imagen</string> + <string name="attach_take_picture">Hacer foto</string> + <string name="preemptively_grant">De forma automática conceder solicitud de suscripción</string> + <string name="error_not_an_image_file">El archivo seleccionado no es una imagen</string> + <string name="error_compressing_image">Error comprimiendo el archivo de imagen</string> + <string name="error_file_not_found">Archivo no encontrado</string> + <string name="error_io_exception">Error general. ¿Puede que no tengas espacio en disco?</string> + <string name="error_security_exception_during_image_copy">La aplicación que usas para seleccionar imágenes no proporciona suficientes permisos para leer el archivo.\n\n<small>Utiliza un explorador de ficheros diferente para seleccionar la imagen</small></string> + <string name="account_status_unknown">Desconocido</string> + <string name="account_status_disabled">Deshabilitado temporalmente</string> + <string name="account_status_online">Conectado</string> + <string name="account_status_connecting">Conectando\u2026</string> + <string name="account_status_offline">Desconectado</string> + <string name="account_status_unauthorized">No autorizado</string> + <string name="account_status_not_found">Servidor no encontrado</string> + <string name="account_status_no_internet">Sin conectividad</string> + <string name="account_status_regis_fail">Error en el registro</string> + <string name="account_status_regis_conflict">El identificador ya está en uso</string> + <string name="account_status_regis_success">Registro completado</string> + <string name="account_status_regis_not_sup">El servidor no soporta registros</string> + <string name="encryption_choice_none">Texto plano</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Editar cuenta</string> + <string name="mgmt_account_delete">Eliminar cuenta</string> + <string name="mgmt_account_disable">Deshabilitar temporalmente</string> + <string name="mgmt_account_publish_avatar">Imagen de perfil</string> + <string name="mgmt_account_publish_pgp">Publicar clave pública OpenPGP</string> + <string name="mgmt_account_enable">Habilitar</string> + <string name="mgmt_account_are_you_sure">¿Estás seguro?</string> + <string name="mgmt_account_delete_confirm_text">Si eliminas tu cuenta tu historial completo de conversaciones se perderá</string> + <string name="attach_record_voice">Grabar audio</string> + <string name="account_settings_jabber_id">Identificador Jabber</string> + <string name="account_settings_password">Contraseña</string> + <string name="account_settings_example_jabber_id">usuario@ejemplo.com</string> + <string name="account_settings_confirm_password">Confirmar contraseña</string> + <string name="password">Contraseña</string> + <string name="confirm_password">Confirmar contraseña</string> + <string name="passwords_do_not_match">Las contraseñas no coinciden</string> + <string name="invalid_jid">El identificador no es un identificador de Jabber válido</string> + <string name="error_out_of_memory">Sin memoria. La imagen es demasiado grande</string> + <string name="add_phone_book_text">¿Te gustaría añadir a %s a tus contactos del teléfono?</string> + <string name="contact_status_online">Disponible</string> + <string name="contact_status_free_to_chat">Hablador</string> + <string name="contact_status_away">Ausente</string> + <string name="contact_status_extended_away">Ausencia extendida</string> + <string name="contact_status_do_not_disturb">No molestar</string> + <string name="contact_status_offline">Desconectado</string> + <string name="muc_details_conference">Conferencia</string> + <string name="muc_details_other_members">Otros Miembros</string> + <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> + <string name="server_info_stream_management">XEP-0198: Stream Management</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="server_info_available">Sí</string> + <string name="server_info_unavailable">No</string> + <string name="missing_public_keys">Se han perdido las claves de anuncio públicas</string> + <string name="last_seen_now">Visto última vez ahora</string> + <string name="last_seen_min">Visto última vez hace 1 minuto</string> + <string name="last_seen_mins">Visto última vez hace %d minutos</string> + <string name="last_seen_hour">Visto última vez hace 1 hora</string> + <string name="last_seen_hours">Visto última vez hace %d horas</string> + <string name="last_seen_day">Visto última vez hace 1 día</string> + <string name="last_seen_days">Visto última vez hace %d días</string> + <string name="never_seen">Nunca visto</string> + <string name="install_openkeychain">Mensaje encriptado. Por favor instala OpenKeychain para desencriptar.</string> + <string name="unknown_otr_fingerprint">Clave OTR desconocida</string> + <string name="openpgp_messages_found">Encontrado mensaje encriptado con OpenPGP</string> + <string name="reception_failed">Error al recibir</string> + <string name="your_fingerprint">Tu clave</string> + <string name="otr_fingerprint">Clave OTR</string> + <string name="verify">Verificar</string> + <string name="decrypt">Desencriptar</string> + <string name="conferences">Conferencias</string> + <string name="search">Buscar</string> + <string name="create_contact">Crear Contacto</string> + <string name="join_conference">Unirse a Conferencia</string> + <string name="delete_contact">Eliminar Contacto</string> + <string name="view_contact_details">Ver detalles del contacto</string> + <string name="create">Crear</string> + <string name="contact_already_exists">El contacto ya existe</string> + <string name="join">Unirse</string> + <string name="conference_address">Dirección de la Conferencia</string> + <string name="conference_address_example">nombre@conferencia.ejemplo.com</string> + <string name="save_as_bookmark">Guardar en marcadores</string> + <string name="delete_bookmark">Eliminar marcador</string> + <string name="bookmark_already_exists">Este marcador ya exsite</string> + <string name="you">Tú</string> + <string name="action_edit_subject">Editar asunto de la conferencia</string> + <string name="conference_not_found">Conferencia no encontrada</string> + <string name="leave">Salir</string> + <string name="contact_added_you">El contacto te ha añadido a su lista de contactos</string> + <string name="add_back">Añadir contacto</string> + <string name="contact_has_read_up_to_this_point">%s ha leído hasta aquí</string> + <string name="publish">Publicar</string> + <string name="touch_to_choose_picture">Pulsa para seleccionar una imagen de la galería</string> + <string name="publish_avatar_explanation">Nota: Todos tus contactos podrán ver esta imagen.</string> + <string name="publishing">Publicando…</string> + <string name="error_publish_avatar_server_reject">El servidor rechazó la publicación</string> + <string name="error_publish_avatar_converting">Se ha producido un error mientras se convertía la imagen</string> + <string name="error_saving_avatar">No se ha podido guardar la imagen de perfil en disco</string> + <string name="or_long_press_for_default">(O pulsación prolongada para volver a tu imagen de la agenda)</string> + <string name="error_publish_avatar_no_server_support">Tu servidor no soporta la publicación de imágenes de perfil</string> + <string name="private_message">en privado</string> + <string name="private_message_to">en privado para %s</string> + <string name="send_private_message_to">Enviar mensaje privado a %s</string> + <string name="connect">Conectar</string> + <string name="account_already_exists">Esta cuenta ya existe</string> + <string name="next">Siguiente</string> + <string name="server_info_session_established">Inicio sesión actual</string> + <string name="additional_information">Información adicional</string> + <string name="skip">Omitir</string> + <string name="disable_notifications">Deshabilitar notificaciones</string> + <string name="disable_notifications_for_this_conversation">Deshabilitar notificaciones para esta conversación</string> + <string name="notifications_disabled">Las notificaciones están deshabilitadas</string> + <string name="enable">Habilitar</string> + <string name="conference_requires_password">La conferencia requiere contraseña</string> + <string name="enter_password">Introduce la contraseña</string> + <string name="missing_presence_updates">Suscripción de actualizaciones de presencia del contacto perdida</string> + <string name="request_presence_updates">Por favor, solicita la suscripción de presencia a tu contacto primero.\n\n<small>Esto será usado para determinar qué cliente(s) está usando tu contacto.</small></string> + <string name="request_now">Solicitar ahora</string> + <string name="delete_fingerprint">Eliminar Clave OTR</string> + <string name="sure_delete_fingerprint">¿Estás seguro que quieres eliminar la clave OTR?</string> + <string name="ignore">Ignorar</string> + <string name="without_mutual_presence_updates"><b>Aviso:</b> Enviando esto sin suscripción de presencia por ambas partes podría causar problemas inesperados.\n\n<small>Verficia la suscripción de presencia en detalles del contacto.</small></string> + <string name="pref_encryption_settings">Ajustes de encriptación</string> + <string name="pref_force_encryption">Forzar encriptación end-to-end</string> + <string name="pref_force_encryption_summary">Siempre enviar mensajes encriptados (excepto para conferencias)</string> + <string name="pref_dont_save_encrypted">No guardar mensajes encriptados</string> + <string name="pref_dont_save_encrypted_summary">Aviso: Esto podría llevar a pérdida de mensajes</string> + <string name="pref_enable_legacy_ssl">Habilitar SSL heredado</string> + <string name="pref_enable_legacy_ssl_summary">Habilita soporte SSLv3 para servidores heredados. Advertencia: SSLv3 se considera no seguro.</string> + <string name="pref_expert_options">Ajustes avanzados</string> + <string name="pref_expert_options_summary">Por favor, cuidado con estas opciones</string> + <string name="pref_use_larger_font">Incrementar tamaño de fuente</string> + <string name="pref_use_larger_font_summary">Usar fuentes grandes en toda la aplicación</string> + <string name="pref_use_send_button_to_indicate_status">Botón enviar indica estado</string> + <string name="pref_use_indicate_received">Solicitar entrega de mensaje</string> + <string name="pref_use_indicate_received_summary">Cuando el contacto reciba el mensaje será indicado con una marca verde. Cuidado, esto podría no funcionar en todos los casos.</string> + <string name="pref_use_send_button_to_indicate_status_summary">El color del botón enviar indica el estado del contacto</string> + <string name="pref_expert_options_other">Otros</string> + <string name="pref_conference_name">Nombre de conferencia</string> + <string name="pref_conference_name_summary">Usar el asunto de la conferencia en lugar del identificador jabber como nombre de conferencia</string> + <string name="toast_message_otr_fingerprint">¡Clave OTR copiada en el portapapeles!</string> + <string name="conference_banned">Tu entrada a esta conferencia ha sido prohibida</string> + <string name="conference_members_only">Esta conferencia es solo para miembros</string> + <string name="conference_kicked">Has sido expulsado de esta conferencia</string> + <string name="using_account">Usando cuenta %s</string> + <string name="checking_image">Comprobando imagen en servidor HTTP</string> + <string name="image_file_deleted">El archivo de imagen ha sido eliminado</string> + <string name="not_connected_try_again">No estás concectado. Inténtalo más tarde</string> + <string name="check_image_filesize">Comprobar el tamaño del archivo de imagen</string> + <string name="message_options">Opciones de mensaje</string> + <string name="copy_text">Copiar texto</string> + <string name="share_image">Compartir imagen</string> + <string name="copy_original_url">Copiar URL original</string> + <string name="send_again">Volver a enviar</string> + <string name="image_url">URL Imagen</string> + <string name="message_text">Mensaje de texto</string> + <string name="url_copied_to_clipboard">URL copiada al portapapeles</string> + <string name="message_copied_to_clipboard">Mensaje copiado al portapapeles</string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-eu/arrays.xml b/src/main/res/values-eu/arrays.xml new file mode 100644 index 00000000..a34d3c6a --- /dev/null +++ b/src/main/res/values-eu/arrays.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Mugikorra</item> + <item>Telefonoa</item> + <item>Tableta</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>inoiz</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + <string-array name="mute_options_descriptions"> + <item>30 minutu</item> + <item>ordu bat</item> + <item>2 ordu</item> + <item>8 ordu</item> + <item>abisatu arte</item> + </string-array> + + <integer-array name="mute_options_durations"> + <item>1800</item> + <item>3600</item> + <item>7200</item> + <item>28800</item> + <item>-1</item> + </integer-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml new file mode 100644 index 00000000..8896e9b5 --- /dev/null +++ b/src/main/res/values-eu/strings.xml @@ -0,0 +1,276 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Ezarpenak</string> + <string name="action_add">Elkarrizketa berria</string> + <string name="action_accounts">Kontuak kudeatu</string> + <string name="action_end_conversation">Elkarrizketa hau amaitu</string> + <string name="action_contact_details">Kontaktuaren xehetasunak</string> + <string name="action_muc_details">Konferentziaren xehetasunak</string> + <string name="action_secure">Elkarrizketa segurua</string> + <string name="action_add_account">Kontua gehitu</string> + <string name="action_edit_contact">Izena editatu</string> + <string name="action_add_phone_book">Telefono kontaktuetara gehitu</string> + <string name="action_delete_contact">Zerrendatik ezabatu</string> + <string name="title_activity_manage_accounts">Kontuak kudeatu</string> + <string name="title_activity_settings">Ezarpenak</string> + <string name="title_activity_conference_details">Konferentziaren xehetasunak</string> + <string name="title_activity_contact_details">Kontaktuaren xehetasunak</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Elkarrizketa batekin partekatu</string> + <string name="title_activity_start_conversation">Elkarrizketa hasi</string> + <string name="title_activity_choose_contact">Kontaktua hautatu</string> + <string name="just_now">orain</string> + <string name="minute_ago">min 1 lehenago</string> + <string name="minutes_ago">%d min lehenago</string> + <string name="unread_conversations">irakurri gabeko elkarrizketak</string> + <string name="sending">bidaltzen…</string> + <string name="encrypted_message">Mezua desenkriptatzen. Mesedez itxaron…</string> + <string name="nick_in_use">Ezizena erabilita dagoeneko</string> + <string name="admin">Administratzailea</string> + <string name="owner">Jabea</string> + <string name="moderator">Moderatzailea</string> + <string name="participant">Parte-hartzailea</string> + <string name="visitor">Bisitaria</string> + <string name="remove_contact_text">%s zure zerrendatik ezabatu nahi duzu? Kontu honekin lotutako elkarrizketa ez da ezabatuko.</string> + <string name="remove_bookmark_text">%s laster-marka bezala ezabatu nahi duzu? Laster-marka honekin lotutako elkarrizketa ez da ezabatuko.</string> + <string name="register_account">Kontu berria zerbitzarian erregistratu</string> + <string name="share_with">Honekin partekatu</string> + <string name="start_conversation">Elkarrizketa hasi</string> + <string name="invite_contact">Kontaktu bat gonbidatu</string> + <string name="contacts">Kontaktuak</string> + <string name="cancel">Utzi</string> + <string name="add">Gehitu</string> + <string name="edit">Editatu</string> + <string name="delete">Ezabatu</string> + <string name="save">Gorde</string> + <string name="ok">Ados</string> + <string name="crash_report_title">Conversations gelditu da</string> + <string name="crash_report_message">Akats harraskak bidaliz Conversationsen garapenean laguntzen duzu\n<b>Abisua:</b> Honek zure XMPP kontua erabiliko du garatzaileari akats harraska bidaltzeko.</string> + <string name="send_now">Bidali orain</string> + <string name="send_never">Ez galdetu berriz</string> + <string name="problem_connecting_to_account">Ezin izan da kontura konektatu</string> + <string name="problem_connecting_to_accounts">Ezin izan da hainbat kontuetara konektatu</string> + <string name="touch_to_fix">Ukitu hemen zure kontuak kudeatzeko</string> + <string name="attach_file">Fitxategia erantsi</string> + <string name="not_in_roster">Kontaktua ez dago zure zerrendan. Gehitu nahiko al zenuke?</string> + <string name="add_contact">Kontaktua gehitu</string> + <string name="send_failed">huts bidaltzerakoan</string> + <string name="send_rejected">ukatua</string> + <string name="receiving_image">Irudi fitxategia jasotzen. Mesedez itxaron…</string> + <string name="preparing_image">Irudia transmisiorako prestatzen. Mesedez itxaron…</string> + <string name="action_clear_history">Historia garbitu</string> + <string name="clear_conversation_history">Elkarrizketa historia garbitu</string> + <string name="clear_histor_msg">Elkarrizketa honetako mezu guztiak ezabatu nahi al dituzu?\n\n<b>Abisua:</b> Honek ez du beste gailu edo zerbitzarietan gordetako mezuetan eraginik izango.</string> + <string name="delete_messages">Mezuak ezabatu</string> + <string name="also_end_conversation">Elkarrizketa hau jarraian amaitu</string> + <string name="choose_presence">Hautatu agerpena kontaktuarentzat</string> + <string name="send_plain_text_message">Testu mezua bidali</string> + <string name="send_otr_message">OTRz enkriptatutako mezua bidali</string> + <string name="send_pgp_message">OpenPGPz enkriptatutako mezua bidali</string> + <string name="your_nick_has_been_changed">Zure ezizena aldatu da</string> + <string name="download_image">Irudia deskargatu</string> + <string name="image_offered_for_download"><i>Irudi fitxategia deskargarako eskeinia</i></string> + <string name="send_unencrypted">Enkriptatu gabe bidali</string> + <string name="decryption_failed">Desenkriptazioak huts egin du. Agian ez duzu gako pribatu egokia.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversationsek <b>OpenKeychain</b> izeneko hirugarren app bat erabiltzen du mezuak enkriptatu eta desenkriptatzeko eta zure gako publikoak kudeatzeko.\n\nOpenKeychain GPLv3 lizentziapean dago eta F-Droid eta Google Playn eskura daiteke.\n\n<small>(Mesedez ondoren Conversations berrabiarazi)</small></string> + <string name="restart">Berrabiarazi</string> + <string name="install">Instalatu</string> + <string name="offering">eskeintzen…</string> + <string name="waiting">itxaroten…</string> + <string name="no_pgp_key">Ez da OpenPGP gakorik aurkitu</string> + <string name="contact_has_no_pgp_key">Conversations ez da zure mezuak enkriptatzeko gai zure kontaktua bere gako publikoa jakinarazten ez dagoelako.\n\n<small>Mesedez eskatu ezaiozu zure kontaktuari openPGP konfigura dezan.</small></string> + <string name="no_pgp_keys">Ez da OpenPGP gakorik aurkitu</string> + <string name="contacts_have_no_pgp_keys">Conversations ez da zure mezuak enkriptatzeko gai zure kontaktuak haien gako publikoa jakinarazten ez daudelako.\n\n<small>Mesedez eskatu ezaiezu zure kontakuei OpenPGP konfigura dezaten.</small></string> + <string name="encrypted_message_received"><i>Enkriptatutako mezua jaso da. Ukitu ikusi eta desenkriptatzeko.</i></string> + <string name="encrypted_image_received"><i>Enkriptatutako irudia jaso da. Ukitu ikusi eta desenkriptatzeko.</i></string> + <string name="image_file"><i>Irudia jaso da. Ukitu ikusteko</i></string> + <string name="pref_general">Orokorrak</string> + <string name="pref_xmpp_resource">XMPP baliabidea</string> + <string name="pref_xmpp_resource_summary">Bezero honek bere burua aurkezteko erabiltzen duen izena</string> + <string name="pref_accept_files">Fitxategiak onartu</string> + <string name="pref_accept_files_summary">Hurrengo tamaina baino fitxategi txikiagoak automatikoki onartu…</string> + <string name="pref_notification_settings">Jakinarazpenen ezarpenak</string> + <string name="pref_notifications">Jakinarazpenak</string> + <string name="pref_notifications_summary">Mezu berri bat heltzerakoan jakinarazi</string> + <string name="pref_vibrate">Dardaratu</string> + <string name="pref_vibrate_summary">Dardaratu ere mezu berri bat heltzerakoan</string> + <string name="pref_sound">Soinua</string> + <string name="pref_sound_summary">Dei-tonua jo jakinarazpenarekin</string> + <string name="pref_conference_notifications">Konferentzien jakinarazpenak</string> + <string name="pref_conference_notifications_summary">Beti jakinarazi konferentzia mezu berri bat heltzerakoan eta ez soilik nabarmentzerakoan</string> + <string name="pref_notification_grace_period">Jakinarazpenen grazia epea</string> + <string name="pref_notification_grace_period_summary">Jakinarazpenak denbora labur baterako ezgaitu ikatz-kopia bat jaso ondoren</string> + <string name="pref_advanced_options">Aukera aurreratuak</string> + <string name="pref_never_send_crash">Gelditze txostenik ez bidali inoiz</string> + <string name="pref_never_send_crash_summary">Akats harraskak bidaliz Conversationsen garapenean laguntzen duzu</string> + <string name="pref_confirm_messages">Mezuak egiaztatu</string> + <string name="pref_confirm_messages_summary">Zure kontaktuak mezu bat noiz jaso eta irakurri duzun jakin dezan baimendu</string> + <string name="pref_ui_options">Erabiltzaile-interfazearen aukerak</string> + <string name="openpgp_error">OpenKeychainek akats baten berri eman du</string> + <string name="error_decrypting_file">Sarrera/Irteera akatsa fitxategia desenkriptatzerakoan</string> + <string name="accept">Onartu</string> + <string name="error">Akats bat gertatu da</string> + <string name="pref_grant_presence_updates">Presentzia eguneraketak eman</string> + <string name="pref_grant_presence_updates_summary">Prebentiboki presentzia eguneraketak eman eta eskatu sortu dituzun kontaktuetarako</string> + <string name="subscriptions">Harpidetzak</string> + <string name="your_account">Zure kontua</string> + <string name="keys">Gakoak</string> + <string name="send_presence_updates">Presentzia eguneraketak bidali</string> + <string name="receive_presence_updates">Presentzia eguneraketak jaso</string> + <string name="ask_for_presence_updates">Presentzia eguneraketak eskatu</string> + <string name="attach_choose_picture">Argazkia aukeratu</string> + <string name="attach_take_picture">Argazkia egin</string> + <string name="preemptively_grant">Prebentiboki harpidetza eskaera eman</string> + <string name="error_not_an_image_file">Aukeratu duzun fitxategia ez da irudi bat</string> + <string name="error_compressing_image">Huts irudi fitxategia bihurtzerakoan</string> + <string name="error_file_not_found">Fitxategia ez da aurkitu</string> + <string name="error_io_exception">Sarrera/Irteera akats orokorra. Agian biltegian lekurik gabe gelditu zara?</string> + <string name="error_security_exception_during_image_copy">Irudi hau aukeratzeko erabili duzun aplikazioak ez digu fitxategia irakurtzeko baimen nahikorik eman.\n\n<small>Beste fitxategi kudeatzaile bat erabili ezazu irudia aukeratzeko</small></string> + <string name="account_status_unknown">Ezezaguna</string> + <string name="account_status_disabled">Aldi baterako ezgaituta</string> + <string name="account_status_online">Konektatuta</string> + <string name="account_status_connecting">Konektatzen\u2026</string> + <string name="account_status_offline">Lineaz kanpo</string> + <string name="account_status_unauthorized">Ez baimenduta</string> + <string name="account_status_not_found">Zerbitzaria ez da aurkitu</string> + <string name="account_status_no_internet">Konektagarritasunik ez</string> + <string name="account_status_regis_fail">Erregistroak huts egin du</string> + <string name="account_status_regis_conflict">Erabiltzaile izena dagoeneko erabilita</string> + <string name="account_status_regis_success">Erregistroa burutu da</string> + <string name="account_status_regis_not_sup">Zerbitzariak ez du erregistratzea onartzen</string> + <string name="encryption_choice_none">Testu laua</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Kontua editatu</string> + <string name="mgmt_account_delete">Kontua ezabatu</string> + <string name="mgmt_account_disable">Aldi baterako ezgaitu</string> + <string name="mgmt_account_publish_avatar">Profileko argazkia argitaratu</string> + <string name="mgmt_account_publish_pgp">OpenPGP gako publikoa argitaratu</string> + <string name="mgmt_account_enable">Kontua gaitu</string> + <string name="mgmt_account_are_you_sure">Ziur al zaude?</string> + <string name="mgmt_account_delete_confirm_text">Zure kontua ezabatzen baduzu zure elkarrizketa historia guztia galduko da</string> + <string name="attach_record_voice">Ahotsa grabatu</string> + <string name="account_settings_jabber_id">Jabber IDa</string> + <string name="account_settings_password">Pasahitza</string> + <string name="account_settings_example_jabber_id">erabiltzailea@adibidea.com</string> + <string name="account_settings_confirm_password">Pasahitza egiaztatu</string> + <string name="password">Pasahitza</string> + <string name="confirm_password">Pasahitza egiaztatu</string> + <string name="passwords_do_not_match">Pasahitzak ez dute bat egiten</string> + <string name="invalid_jid">Hau ez da Jabber ID baliodun bat</string> + <string name="error_out_of_memory">Memoriarik gabe. Irudia handiegia da</string> + <string name="add_phone_book_text">%s zure telefono kontaktu zerrendara gehitu nahi al duzu?</string> + <string name="contact_status_online">konektatuta</string> + <string name="contact_status_free_to_chat">hitzegiteko aske</string> + <string name="contact_status_away">kanpoan</string> + <string name="contact_status_extended_away">luzerako kanpoan</string> + <string name="contact_status_do_not_disturb">ez gogaitu</string> + <string name="contact_status_offline">lineaz kanpo</string> + <string name="muc_details_conference">Konferentzia</string> + <string name="muc_details_other_members">Beste kideak</string> + <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> + <string name="server_info_stream_management">XEP-0198: Stream Management</string> + <string name="server_info_pep">XEP-0163: PEP (Profileko argazkiak)</string> + <string name="server_info_available">eskuragarri</string> + <string name="server_info_unavailable">ez eskuragarri</string> + <string name="missing_public_keys">Gako publikoen iragarpenak faltan</string> + <string name="last_seen_now">azkenengoz ikusia orain</string> + <string name="last_seen_mins">azkenengoz ikusia %d minutu lehenago</string> + <string name="last_seen_hours">azkenengoz ikusia %d ordu lehenago</string> + <string name="last_seen_days">azkenengoz ikusia %d egun lehenago</string> + <string name="never_seen">inoiz ez ikusia</string> + <string name="last_seen_min">azkenengoz ikusia minutu 1 lehenago</string> + <string name="last_seen_hour">azkenengoz ikusia ordu 1 lehenago</string> + <string name="last_seen_day">azkenengoz ikusia egun 1 lehenago</string> + <string name="install_openkeychain">Mezu enkriptatua. Mesedez instalatu OpenKeychain desenkriptatzeko.</string> + <string name="unknown_otr_fingerprint">OTR hatz-marka ezezaguna</string> + <string name="openpgp_messages_found">OpenPGPz enkriptatutako mezuak aurkitu dira</string> + <string name="reception_failed">Jasotzeak huts egin du</string> + <string name="your_fingerprint">Zure hatz-marka</string> + <string name="otr_fingerprint">OTR hatz-marka</string> + <string name="verify">Egiaztatu</string> + <string name="decrypt">Desenkriptatu</string> + <string name="conferences">Konferentziak</string> + <string name="search">Bilatu</string> + <string name="create_contact">Kontaktua sortu</string> + <string name="join_conference">Konferentziara batu</string> + <string name="delete_contact">Kontaktua ezabatu</string> + <string name="view_contact_details">Kontaktuaren xehetasunak ikusi</string> + <string name="create">Sortu</string> + <string name="contact_already_exists">Kontaktua existitzen da dagoeneko</string> + <string name="join">Batu</string> + <string name="conference_address">Konferentziaren helbidea</string> + <string name="conference_address_example">gela@conference.example.com</string> + <string name="save_as_bookmark">Gorde laster-marka bezala</string> + <string name="delete_bookmark">Laster-marka ezabatu</string> + <string name="bookmark_already_exists">Laster-marka hau existitzen da dagoeneko</string> + <string name="you">Zu</string> + <string name="action_edit_subject">Konferentziaren gaia editatu</string> + <string name="conference_not_found">Konferentzia ez da aurkitu</string> + <string name="leave">Alde egin</string> + <string name="contact_added_you">Kontaktuak bere zerrendara gehitu zaitu</string> + <string name="add_back">Bera gehitu</string> + <string name="contact_has_read_up_to_this_point">%s(e)k puntu honetaraino irakurri du</string> + <string name="publish">Argitaratu</string> + <string name="touch_to_choose_picture">Ukitu profileko argazkia irudi bat galeriatik hautatzeko</string> + <string name="publish_avatar_explanation">Adi: Zure presentzia eguneraketetara harpidetutako edonork irudi hau ikusi ahal izango du.</string> + <string name="publishing">Argitaratzen…</string> + <string name="error_publish_avatar_server_reject">Zerbitzariak zure argitarapena ukatu du</string> + <string name="error_publish_avatar_converting">Zerbait oker joan da zure irudia bihurtzerakoan</string> + <string name="error_saving_avatar">Ezin izan da profileko argazkia diskoan gorde</string> + <string name="or_long_press_for_default">(Edo sakatu luze lehenetsira bueltatzeko)</string> + <string name="error_publish_avatar_no_server_support">Zure zerbitzariak ez du profileko argazkien argitarapena onartzen</string> + <string name="private_message">xuxurlatu</string> + <string name="private_message_to">%s(r)i</string> + <string name="send_private_message_to">%s(r)i mezu pribatua bidali</string> + <string name="connect">Konektatu</string> + <string name="account_already_exists">Kontu hau existitzen da dagoeneko</string> + <string name="next">Hurrengoa</string> + <string name="server_info_session_established">Uneko saioa ezarri da</string> + <string name="additional_information">Informazio gehiago</string> + <string name="skip">Orain ez</string> + <string name="disable_notifications">Jakinarazpenak ezgaitu</string> + <string name="disable_notifications_for_this_conversation">Elkarrizketa honetarako jakinarazpenak ezgaitu</string> + <string name="notifications_disabled">Jakinarazpenak ezgaituta daude</string> + <string name="enable">Gaitu</string> + <string name="conference_requires_password">Konferentziak pasahitza behar du</string> + <string name="enter_password">Sartu pasahitza</string> + <string name="missing_presence_updates">Kontaktuaren presentzia eguneraketak falta dira</string> + <string name="request_presence_updates">Mesedez eskatu lehenago zure kontaktuaren presentzia eguneraketak.\n\n<small>Kontaktuak erabiltzen ari den bezeroa(k) zehazteko erabilika da hau.</small></string> + <string name="request_now">Eskatu orain</string> + <string name="delete_fingerprint">Hatz-marka ezabatu</string> + <string name="sure_delete_fingerprint">Ziur al zaude hatz-marka hau ezabatu nahi duzulaz?</string> + <string name="ignore">Kasurik ez egin</string> + <string name="without_mutual_presence_updates"><b>Adi:</b> Bien arteko presentzia eguneraketarik gabe hau bidaltzeak ustekabeko arazoak sor litzake.\n\n<small>Joan zaitez kontaktuaren xehetasunetara zure presentzia eguneraketak egiaztatzeko.</small></string> + <string name="pref_encryption_settings">Enkriptazio ezarpenak</string> + <string name="pref_force_encryption">End-to-end enkriptazioa behartu</string> + <string name="pref_force_encryption_summary">Mezuak beti enkriptatuta bidali (konferentzietan izan ezik)</string> + <string name="pref_dont_save_encrypted">Ez gorde enkriptatutako mezuak</string> + <string name="pref_dont_save_encrypted_summary">Adi: Honek mezuen galera ekar lezake</string> + <string name="pref_enable_legacy_ssl">Oinordetutako SSL gaitu</string> + <string name="pref_enable_legacy_ssl_summary">SSLv3 gaitzen du oinordetutako zerbitzarietarako. Adi: SSLv3 ez segurutzat hartzen da.</string> + <string name="pref_expert_options">Adituentzako aukerak</string> + <string name="pref_expert_options_summary">Mesedez kontuz ibili hauekin</string> + <string name="pref_use_larger_font">Letraren tamaina handitu</string> + <string name="pref_use_larger_font_summary">Letra tamaina handiagoa erabili aplikazio osoan zehar</string> + <string name="pref_use_send_button_to_indicate_status">Bidaltze botoiak egoera adierazten du</string> + <string name="pref_use_indicate_received">Mezuen jasotzea eskatu</string> + <string name="pref_use_indicate_received_summary">Jasotako mezuak marka berde batekin markatuko dira. Baliteke kasu guztietan ez funtzionatzea.</string> + <string name="pref_use_send_button_to_indicate_status_summary">Bidaltze botoia koloreztatu kontaktu baten egoera adierazteko</string> + <string name="pref_expert_options_other">Besteak</string> + <string name="pref_conference_name">Konferentziaren izena</string> + <string name="pref_conference_name_summary">Erabili gelaren gaia konferentziak identifikatzeko eta ez JIDa</string> + <string name="toast_message_otr_fingerprint">OTR hatz-marka arbelara kopiatu da</string> + <string name="conference_banned">Konferentzia honetara sartzea debekatuta duzu</string> + <string name="conference_members_only">Konferentzia hau kideentzat da soilik</string> + <string name="conference_kicked">Konferentzia honetatik kanporatua izan zara</string> + <string name="using_account">%s kontua erabiltzen</string> + <string name="checking_image">Irudia egiaztatzen HTTP ostalarian</string> + <string name="image_file_deleted">Irudia ezabatu egin da</string> + <string name="not_connected_try_again">Ez zaude konektatuta. Saiatu beranduago berriz</string> + <string name="check_image_filesize">Irudiaren tamaina egiaztatu</string> + +</resources> diff --git a/src/main/res/values-fr/arrays.xml b/src/main/res/values-fr/arrays.xml new file mode 100644 index 00000000..ae140796 --- /dev/null +++ b/src/main/res/values-fr/arrays.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Mobile</item> + <item>Téléphone</item> + <item>Tablette</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>jamais</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..e1db316d --- /dev/null +++ b/src/main/res/values-fr/strings.xml @@ -0,0 +1,273 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Paramètres</string> + <string name="action_add">Nouvelle conversation</string> + <string name="action_accounts">Gérer les comptes</string> + <string name="action_end_conversation">Terminer cette conversation</string> + <string name="action_contact_details">Détails du contact</string> + <string name="action_muc_details">Détails de la conférence</string> + <string name="action_secure">Conversation sécurisée</string> + <string name="action_add_account">Ajouter un compte</string> + <string name="action_edit_contact">Modifier le nom</string> + <string name="action_add_phone_book">Ajouter aux contacts</string> + <string name="action_delete_contact">Retirer des contacts</string> + <string name="title_activity_manage_accounts">Gestion des comptes</string> + <string name="title_activity_settings">Paramètres</string> + <string name="title_activity_conference_details">Détails de la conférence</string> + <string name="title_activity_contact_details">Détails du contact</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Partager avec Conversation</string> + <string name="title_activity_start_conversation">Lancement de Conversation</string> + <string name="title_activity_choose_contact">Choix du contact</string> + <string name="just_now">À l\'instant</string> + <string name="minute_ago">Il y a 1 minute</string> + <string name="minutes_ago">Il y a %d minutes</string> + <string name="unread_conversations">Conversations non lues</string> + <string name="sending">envoi…</string> + <string name="encrypted_message">Déchiffrement du message. Patientez…</string> + <string name="nick_in_use">Cet identifiant est déjà utilisé.</string> + <string name="admin">Administrateur</string> + <string name="owner">Propriétaire</string> + <string name="moderator">Modérateur</string> + <string name="participant">Participant</string> + <string name="visitor">Visiteur</string> + <string name="remove_contact_text">Voulez-vous supprimer %s de votre liste? Les conversations associées à ce compte ne seront pas supprimées.</string> + <string name="remove_bookmark_text">Voulez-vous retirer %s des favoris? La conversation associée avec ce favoris ne sera pas supprimé.</string> + <string name="register_account">Créer un nouveau compte sur le serveur</string> + <string name="share_with">Partager avec</string> + <string name="start_conversation">Démarrer une conversation</string> + <string name="invite_contact">Inviter des contacts</string> + <string name="contacts">Contacts</string> + <string name="cancel">Annuler</string> + <string name="add">Ajouter</string> + <string name="edit">Modifier</string> + <string name="delete">Supprimer</string> + <string name="save">Enregistrer</string> + <string name="ok">OK</string> + <string name="crash_report_title">Conversations s\'est arreté</string> + <string name="crash_report_message">En envoyant des logs vous aidez au développement de Conversations.\n\n<b>Attention:</b> Votre compte XMPP sera utilisé pour envoyer les logs aux développeurs.</string> + <string name="send_now">Envoyer</string> + <string name="send_never">Ne plus me demander</string> + <string name="problem_connecting_to_account">Impossible de se connecter au compte.</string> + <string name="problem_connecting_to_accounts">Impossible de se connecter aux comptes.</string> + <string name="touch_to_fix">Appuyez pour gérer vos comptes.</string> + <string name="attach_file">Lier un fichier</string> + <string name="not_in_roster">Le contact n\'est pas dans votre carnet d\'adresses. Voulez-vous l\'y ajouter?</string> + <string name="add_contact">Ajouter un contact</string> + <string name="send_failed">Echec de l\'envoi.</string> + <string name="send_rejected">Rejeté</string> + <string name="receiving_image">Réception d\'une image. Patientez…</string> + <string name="preparing_image">Préparation de la transmission de l\'image. Patientez…</string> + <string name="action_clear_history">Vider l\'historique</string> + <string name="clear_conversation_history">Vider l\'historique de la conversation</string> + <string name="clear_histor_msg">Voulez-vous supprimer tous les messages de cette conversation?\n\n<b>Attention:</b> Les messages seront supprimés uniquement sur ce périphérique.</string> + <string name="delete_messages">Supprimer les messages</string> + <string name="also_end_conversation">Terminer plus tard cette conversation</string> + <string name="choose_presence">Choisir le status de présence</string> + <string name="send_plain_text_message">Envoyer un message</string> + <string name="send_otr_message">Envoyer un message sécurisé par OTR</string> + <string name="send_pgp_message">Envoyer un message sécurisé par OpenPGP</string> + <string name="your_nick_has_been_changed">Votre identifiant a été changé</string> + <string name="download_image">Télécharger l\'image</string> + <string name="image_offered_for_download"><i>Image proposée au téléchargement.</i></string> + <string name="send_unencrypted">Envoyer en clair</string> + <string name="decryption_failed">Echec du déchiffrement. Merci de vérifier la clef privée utilisée.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations requiert une application tierce nommée <b>OpenKeychain</b> pour chiffrer et déchiffrer les messages.\n\nOpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.\n\n<small>(Merci de redémarrer Conversations apres l\'installation du logiciel)</small></string> + <string name="restart">Redémarrer</string> + <string name="install">Installer</string> + <string name="offering">Proposition…</string> + <string name="waiting">Patientez…</string> + <string name="no_pgp_key">Aucune clef OpenPGP trouvée.</string> + <string name="contact_has_no_pgp_key">Conversations ne peut chiffrer vos messages car votre correspondant n\'a pas communiqué sa clef publique.\n\n<small>Merci de demander à votre correspondant de configurer OpenPGP.</small></string> + <string name="no_pgp_keys">Aucune clef OpenPGP n\'est disponible.</string> + <string name="contacts_have_no_pgp_keys">Conversations ne peut pas chiffrer votre message car vous ne connaissez pas la clef publique de vos contacts.\n\n<small>Merci de les faire configurer leur OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Message chiffré reçu. Appuyez pour le déchiffrer.</i></string> + <string name="encrypted_image_received"><i>Image chiffrée reçue. Appuyez pour la déchiffrer.</i></string> + <string name="image_file"><i>Image reçue. Appuyez pour visualiser.</i></string> + <string name="pref_general">Général</string> + <string name="pref_xmpp_resource">Ressource XMPP</string> + <string name="pref_xmpp_resource_summary">Nom permettant d\'identifier ce client XMPP</string> + <string name="pref_accept_files">Accepter les fichiers</string> + <string name="pref_accept_files_summary">Accepter automatiquement les fichiers plus petits que…</string> + <string name="pref_notification_settings">Paramètres de notification</string> + <string name="pref_notifications">Notifications</string> + <string name="pref_notifications_summary">Notifier l\'arrivée d\'un message</string> + <string name="pref_vibrate">Vibration</string> + <string name="pref_vibrate_summary">Vibrer lors de l\'arrivée d\'un message</string> + <string name="pref_sound">Sonore</string> + <string name="pref_sound_summary">Jouer une sonnerie lors de l\'arrivée d\'un message</string> + <string name="pref_conference_notifications">Notifications lors des conférences</string> + <string name="pref_conference_notifications_summary">Toujours notifier l\'arrivée d\'un message provenant d\'une conférence.</string> + <string name="pref_notification_grace_period">Période sans notification</string> + <string name="pref_notification_grace_period_summary">Désactiver momentanément les notifications après l\'arrivée d\'une copie carbone.</string> + <string name="pref_advanced_options">Options avancées</string> + <string name="pref_never_send_crash">Ne jamais envoyer de rapports d\'erreurs</string> + <string name="pref_never_send_crash_summary">En envoyant des logs vous aidez au développement de Conversations.</string> + <string name="pref_confirm_messages">Confirmation de lecture</string> + <string name="pref_confirm_messages_summary">Informer l\'expéditeur d\'un message de sa bonne réception.</string> + <string name="pref_ui_options">Options d\'affichage</string> + <string name="openpgp_error">Une erreur s\'est produite via OpenKeychain</string> + <string name="error_decrypting_file">Erreur d\'E/S lors du déchiffrement du fichier</string> + <string name="accept">Accepter</string> + <string name="error">Une erreur s\'est produite</string> + <string name="pref_grant_presence_updates">Accepter les mises à jour de présence</string> + <string name="pref_grant_presence_updates_summary">Demander et accepter par avance les mises à jour de présence des contacts créés.</string> + <string name="subscriptions">Publications</string> + <string name="your_account">Votre compte</string> + <string name="keys">Clefs</string> + <string name="send_presence_updates">Envoyer les mises à jour de présence</string> + <string name="receive_presence_updates">Recevoir les mises à jour de présence</string> + <string name="ask_for_presence_updates">Demander les mises à jour de présence</string> + <string name="attach_choose_picture">Choisir une image</string> + <string name="attach_take_picture">Prendre une photo</string> + <string name="preemptively_grant">Accepter par avance les demandes de publication.</string> + <string name="error_not_an_image_file">Le fichier choisi n\'est pas une image</string> + <string name="error_compressing_image">Une erreur s\'est produite en convertissant l\'image</string> + <string name="error_file_not_found">Fichier non trouvé</string> + <string name="error_io_exception">Erreur générale d\'E/S. Avez-vous encore de l\'espace libre?</string> + <string name="error_security_exception_during_image_copy">L\'application utilisée empêche la lecture de l\'image.\n\n<small>Choisissez l\'image depuis une autre application.</small></string> + <string name="account_status_unknown">Inconnu</string> + <string name="account_status_disabled">Désactivé temporairement</string> + <string name="account_status_online">En ligne</string> + <string name="account_status_connecting">Connexion\u2026</string> + <string name="account_status_offline">Hors-ligne</string> + <string name="account_status_unauthorized">Non autorisé</string> + <string name="account_status_not_found">Serveur non trouvé</string> + <string name="account_status_no_internet">Aucune connectivité</string> + <string name="account_status_regis_fail">Enregistrement échoué</string> + <string name="account_status_regis_conflict">Identifiant déjà utilisé</string> + <string name="account_status_regis_success">Enregistrement réussi</string> + <string name="account_status_regis_not_sup">Le serveur ne permet pas l\'enregistrement</string> + <string name="encryption_choice_none">Texte clair</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Modifier le compte</string> + <string name="mgmt_account_delete">Supprimer</string> + <string name="mgmt_account_disable">Désactiver temporairement</string> + <string name="mgmt_account_publish_avatar">Publier un avatar</string> + <string name="mgmt_account_publish_pgp">Publier la clef publique OpenPGP</string> + <string name="mgmt_account_enable">Activer</string> + <string name="mgmt_account_are_you_sure">Êtes-vous sûr?</string> + <string name="mgmt_account_delete_confirm_text">En supprimant votre compte, votre historique de conversations sera perdu!</string> + <string name="attach_record_voice">Enregistrer un son</string> + <string name="account_settings_jabber_id">Identifiant</string> + <string name="account_settings_password">Mot de passe</string> + <string name="account_settings_example_jabber_id">utilisateur@exemple.com</string> + <string name="account_settings_confirm_password">Confirmer le mot de passe</string> + <string name="password">Mot de passe</string> + <string name="confirm_password">Confirmer le mot de passe</string> + <string name="passwords_do_not_match">Les deux mots de passes ne correspondent pas.</string> + <string name="invalid_jid">Ce n\'est pas un identifiant valide.</string> + <string name="error_out_of_memory">Plus de mémoire disponible. L\'image est trop volumineuse.</string> + <string name="add_phone_book_text">Voulez-vous ajouter %s aux contacts du téléphone?</string> + <string name="contact_status_online">En ligne</string> + <string name="contact_status_free_to_chat">Disponible</string> + <string name="contact_status_away">Absent</string> + <string name="contact_status_extended_away">Absent depuis longtemps</string> + <string name="contact_status_do_not_disturb">Ne pas déranger</string> + <string name="contact_status_offline">Hors-ligne</string> + <string name="muc_details_conference">Conférence</string> + <string name="muc_details_other_members">Autres membres</string> + <string name="server_info_carbon_messages">Copies carbone</string> + <string name="server_info_stream_management">Gestion des flux</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="server_info_available">disponible</string> + <string name="server_info_unavailable">indisponible</string> + <string name="missing_public_keys">Aucune annonce de clef publique</string> + <string name="last_seen_now">en ligne à l\'instant</string> + <string name="last_seen_min">en ligne il y a 1 minute</string> + <string name="last_seen_mins">en ligne il y a %d minutes</string> + <string name="last_seen_hour">en ligne il y a 1 heure</string> + <string name="last_seen_hours">en ligne il y a %d heures</string> + <string name="last_seen_day">en ligne hier</string> + <string name="last_seen_days">en ligne il y a %d jours</string> + <string name="never_seen">jamais vu en ligne</string> + <string name="install_openkeychain">Message chiffré. Merci d\'installer OpenKeychain pour lire le contenu du message.</string> + <string name="unknown_otr_fingerprint">Empreinte OTR inconnue.</string> + <string name="openpgp_messages_found">Messages chiffrés par OpenPGP détectés.</string> + <string name="reception_failed">Echec lors de la réception</string> + <string name="your_fingerprint">Votre empreinte</string> + <string name="otr_fingerprint">Empreinte OTR</string> + <string name="verify">Vérifier</string> + <string name="decrypt">Déchiffrer</string> + <string name="conferences">Conférences</string> + <string name="search">Rechercher</string> + <string name="create_contact">Ajouter un contact</string> + <string name="join_conference">Rejoindre la conférence</string> + <string name="delete_contact">Supprimer le contact</string> + <string name="view_contact_details">Afficher les détails du contact</string> + <string name="create">Ajouter</string> + <string name="contact_already_exists">Le contact existe déjà.</string> + <string name="join">Rejoindre</string> + <string name="conference_address">Adresse de la conférence</string> + <string name="conference_address_example">salle@conference.exemple.com</string> + <string name="save_as_bookmark">Enregistrer en favoris</string> + <string name="delete_bookmark">Supprimer le favoris</string> + <string name="bookmark_already_exists">Ce favoris existe déjà.</string> + <string name="you">Vous</string> + <string name="action_edit_subject">Modifier le sujet de la conférence</string> + <string name="conference_not_found">Conférence non trouvée</string> + <string name="leave">Partir</string> + <string name="contact_added_you">Votre correspondant vous a ajouté dans sa liste de contacts</string> + <string name="add_back">Ajouter également</string> + <string name="contact_has_read_up_to_this_point">%s a lu les messages précédents.</string> + <string name="publish">Publier</string> + <string name="touch_to_choose_picture">Toucher l\'avatar pour choisir une image depuis la galerie.</string> + <string name="publish_avatar_explanation">Nota Bene: Les personnes ayant activé les mises jour de présence verront cette image.</string> + <string name="publishing">Mise à jour…</string> + <string name="error_publish_avatar_server_reject">Le serveur a rejeté votre envoi d\'image</string> + <string name="error_publish_avatar_converting">Une erreur s\'est produite pendant la conversion de votre image.</string> + <string name="error_saving_avatar">Impossible de stocker l\'image sur le disque</string> + <string name="or_long_press_for_default">(Un appui long réinitialise le paramètre par defaut)</string> + <string name="error_publish_avatar_no_server_support">Votre serveur n\'autorise pas l\'envoi d\'avatars</string> + <string name="private_message">chuchoté</string> + <string name="private_message_to">pour %s</string> + <string name="send_private_message_to">Envoyer un message privé à %s</string> + <string name="connect">Se connecter</string> + <string name="account_already_exists">Ce compte existe déjà</string> + <string name="next">suivant</string> + <string name="server_info_session_established">Session établie</string> + <string name="additional_information">Informations supplémentaires</string> + <string name="skip">Passer</string> + <string name="disable_notifications">Désactiver les notifications</string> + <string name="disable_notifications_for_this_conversation">Désactiver les notifications pour cette conversation</string> + <string name="notifications_disabled">Notifications are Désactivées</string> + <string name="enable">Activer</string> + <string name="conference_requires_password">La conférence necessite un mot de passe</string> + <string name="enter_password">Entrer le mot de passe</string> + <string name="missing_presence_updates">Mise à jour de présence non connue</string> + <string name="request_presence_updates">Merci de demander à votre contact de fournir les mises à jour de présence.\n\n<small>Cela permettra de savoir quel matériel utilise votre contact.</small></string> + <string name="request_now">Demander maintenant</string> + <string name="delete_fingerprint">Supprimer l\'empreinte</string> + <string name="sure_delete_fingerprint">Etes-vous sûr de vouloir supprimer l\'empreinte?</string> + <string name="ignore">Ignorer</string> + <string name="without_mutual_presence_updates"><b>Attention:</b> Ceci peut poser problème si l\'un des deux correspondants n\'a pas activé les mises à jour de présence.\n\n<small>Go to contact details to verify your presence subscriptions.</small></string> + <string name="pref_encryption_settings">Paramètres de chiffrement</string> + <string name="pref_force_encryption">Forcer le chiffrement de bout en bout</string> + <string name="pref_force_encryption_summary">Toujours envoyer des messages chiffrés (sauf pour les conférences)</string> + <string name="pref_dont_save_encrypted">Ne pas sauvegarder les messages chiffrés</string> + <string name="pref_dont_save_encrypted_summary">Attention: Celà peut mener à une perte de messages</string> + <string name="pref_expert_options">Options avancées</string> + <string name="pref_expert_options_summary">A utiliser avec précautions</string> + <string name="pref_use_larger_font">Augmenter la taille du texte</string> + <string name="pref_use_larger_font_summary">Augmenter la taille du texte partout dans l\'application</string> + <string name="pref_use_send_button_to_indicate_status">Le bouton Envoyer permet d\'indiquer le statut</string> + <string name="pref_use_indicate_received">Accusé de reception</string> + <string name="pref_use_indicate_received_summary">Les messages recus seront marqués d\'une coche verte si disponible</string> + <string name="pref_use_send_button_to_indicate_status_summary">Adapter la couleur du bouton Envoyer pour indiquer le statut</string> + <string name="pref_expert_options_other">Autres</string> + <string name="pref_conference_name">Nom de la conférence </string> + <string name="pref_conference_name_summary">Identifier la conférence par son nom plutot que par son JID</string> + <string name="toast_message_otr_fingerprint">Empreinte OTR copiée dans le presse-papier!</string> + <string name="conference_banned">Vous êtes interdit de cette conférence</string> + <string name="conference_members_only">Cette conférence est réservée aux membres</string> + <string name="conference_kicked">Vous avez été éjecté de cette conférence</string> + <string name="using_account">utiliser le compte %s</string> + <string name="checking_image">Vérification de l\'image</string> + <string name="image_file_deleted">L\'image a été suprimée</string> + <string name="not_connected_try_again">Vous n\'êtes pas connecté. Merci de retenter plus tard.</string> + +</resources> diff --git a/src/main/res/values-gl/arrays.xml b/src/main/res/values-gl/arrays.xml new file mode 100644 index 00000000..19424a78 --- /dev/null +++ b/src/main/res/values-gl/arrays.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Móvil</item> + <item>Teléfono</item> + <item>Tablet</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>nunca</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml new file mode 100644 index 00000000..58116463 --- /dev/null +++ b/src/main/res/values-gl/strings.xml @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Axustes</string> + <string name="action_add">Nova conversa</string> + <string name="action_accounts">Xestionar contas</string> + <string name="action_end_conversation">Terminar conversa</string> + <string name="action_contact_details">Detalles do contacto</string> + <string name="action_muc_details">Detalles da conferencia</string> + <string name="action_secure">Conversa segura</string> + <string name="action_add_account">Engadir conta</string> + <string name="action_edit_contact">Editar contacto</string> + <string name="action_delete_contact">Eliminar contacto da lista</string> + <string name="just_now">agora</string> + <string name="minutes_ago">min</string> + <string name="unread_conversations">conversas sen ler</string> + <string name="sending">enviando…</string> + <string name="encrypted_message">Descifrando mensaxe. Agarda uns intres…</string> + <string name="nick_in_use">O apodo xa está en uso</string> + <string name="moderator">Moderador</string> + <string name="participant">Participante</string> + <string name="visitor">Visitante</string> + <string name="remove_contact_text">¿Queres eliminar a %s da túa lista?. A conversa asociada a esta conta non se eliminará.</string> + <string name="register_account">Rexistrar nova conta no servidor</string> + <string name="share_with">Compartir con</string> + <string name="start_conversation">Comeza conversa</string> + <string name="cancel">Cancelar</string> + <string name="crash_report_title">Conversations deteuse.</string> + <string name="crash_report_message">Enviando volcados de pilas axudas ao desenrolo de Conversations\n<b>Aviso:</b> Isto empregará a túa conta XMPP para enviar o volcado de pila ao desenrolador.</string> + <string name="send_now">Enviar agora</string> + <string name="send_never">Non preguntar de novo</string> + <string name="problem_connecting_to_account">Erro na conexión á conta</string> + <string name="problem_connecting_to_accounts">Erro na conexión a múltiples contas</string> + <string name="touch_to_fix">Pulsa aquí para xestionar as túas contass</string> + <string name="attach_file">Adxuntar</string> + <string name="not_in_roster">O contacto non está na túa lista. ¿Queres engadilo?</string> + <string name="add_contact">Engadir contacto</string> + <string name="send_failed">Erro ao enviar</string> + <string name="send_rejected">rechazado</string> + <string name="receiving_image">Recibindo arquivo de imaxe. Agarda por favor…</string> + <string name="preparing_image">Preparando imaxe para enviar</string> + <string name="action_clear_history">Limpar historial</string> + <string name="clear_conversation_history">Limpar historial de conversa</string> + <string name="clear_histor_msg">¿Queres borrar todas as mensaxes desta conversa?\n\n<b>Ollo:</b> Isto non afectará ás mensaxes gardadas noutros dispositivos ou servidores.</string> + <string name="delete_messages">Borrar mensaxes</string> + <string name="also_end_conversation">Terminar esta conversa máis tarde</string> + <string name="choose_presence">Selecciona recurso del contacto</string> + <string name="send_plain_text_message">Enviar mensaxe de texto</string> + <string name="send_otr_message">Enviar mensaxe cifrado con OTR</string> + <string name="send_pgp_message">Enviar mensaxe cifrado con OpenPGP</string> + <string name="your_nick_has_been_changed">Modificouse o teu apodo</string> + <string name="download_image">Descargar imaxe</string> + <string name="image_offered_for_download"><i>Arquivo de imaxe ofrecido para descarga</i></string> + <string name="send_unencrypted">Enviar sen cifrar</string> + <string name="decryption_failed">Fallou o descifrado. Quizábeis non teñas a clave privada apropiada.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations emprega unha aplicación de terceiros chamada <b>OpenKeychain</b> para cifrar e descifrar mensaxes e xestionar as túas claves públicas.\n\nOpenKeychain está publicado baixo licencia GPLv3 e disponible en F-Droid e Google Play.\n\n<small>(Por favor, reinicie Conversations despois.)</small></string> + <string name="restart">Reiniciar</string> + <string name="install">Instalar</string> + <string name="offering">ofrecendo…</string> + <string name="no_pgp_key">Clave OpenPGP non atopada</string> + <string name="contact_has_no_pgp_key">Conversations non foi quen de cifrar as túas mensaxes porque o teu contactos non está anunciando a súa clave pública.\n\n<small>Por favor, pídelle ao teu contacto que configure OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Mensaxe cifrado recibido. Pulsa para ver.</i></string> + <string name="encrypted_image_received"><i>Imaxe cifrada recibida. Pulsa para ver.</i></string> + <string name="image_file"><i>Imaxe recibida. Pulsa para ver</i></string> + <string name="pref_xmpp_resource">Recurso</string> + <string name="pref_xmpp_resource_summary">O nome que identifica o cliente que estás a empregar</string> + <string name="pref_accept_files">Aceptar arquivos</string> + <string name="pref_accept_files_summary">De forma automática aceptar arquivos menores de…</string> + <string name="pref_notification_settings">Axustes de notificación</string> + <string name="pref_notifications">Notificacións</string> + <string name="pref_notifications_summary">Notifica cuando chega unha nova mensaxe</string> + <string name="pref_vibrate">Tremer</string> + <string name="pref_vibrate_summary">Treme cando chega unha novo mensaxe</string> + <string name="pref_sound">Son</string> + <string name="pref_sound_summary">Reproduce un ton ca notificación</string> + <string name="pref_conference_notifications">Notificacións de conferencia</string> + <string name="pref_conference_notifications_summary">Siempre notifica cuando chega unha mensaxe de conferencia e non solo cuando chega unha mensaxe destacada</string> + <string name="pref_notification_grace_period">Notificacións Carbons</string> + <string name="pref_notification_grace_period_summary">Deshabilita as notificacións durante un corto periodo de tiempo despois de recibir a copia da mensaxe carbón</string> + <string name="pref_advanced_options">Opcións avanzadas</string> + <string name="pref_never_send_crash">Nunca enviar informe de erros</string> + <string name="pref_never_send_crash_summary">Enviando volcados de pilas axudas al desenrolo de Conversations</string> + <string name="openpgp_error">OpenKeychain reportou un erro</string> + <string name="error_decrypting_file">I/O Erro descifrando arquivo</string> + <string name="accept">Aceptar</string> + <string name="error">Produciuse un erro</string> + <string name="pref_grant_presence_updates">Suscripción de presencia</string> + <string name="pref_grant_presence_updates_summary">Por defecto otorgar e pedir suscripcións de presencia dos contactos que creaches</string> + <string name="subscriptions">Suscripcións</string> + <string name="your_account">A túa conta</string> + <string name="keys">Chaves</string> + <string name="send_presence_updates">Enviar actualizacións de presencia</string> + <string name="receive_presence_updates">Recibir actualizacións de presencia</string> + <string name="ask_for_presence_updates">Solicitar actualizacións de presencia</string> + <string name="attach_choose_picture">Seleccionar imaxe</string> + <string name="attach_take_picture">Facer foto</string> + <string name="preemptively_grant">Por defecto otorgar peticiones de suscripción</string> + <string name="error_not_an_image_file">O arquivo seleccionado non é unha imaxe</string> + <string name="error_compressing_image">Erro convertindo o arquivo de imaxe</string> + <string name="error_file_not_found">Arquivo non atopado</string> + <string name="error_io_exception">Erro xeral de I/O. ¿Quedaches sen espazo no disco?</string> + <string name="error_security_exception_during_image_copy">A aplicación que usas para seleccionar imaxes non proporciona suficientes permisos para leer o arquivo.\n\n<small>Utiliza un explorador de arquivos diferente para seleccionar a imaxe</small></string> + <string name="account_status_unknown">Descoñecido</string> + <string name="account_status_disabled">Deshabilitado temporalmente</string> + <string name="account_status_online">Conectado</string> + <string name="account_status_connecting">Conectando\u2026</string> + <string name="account_status_offline">Desconectado</string> + <string name="account_status_unauthorized">Non autorizado</string> + <string name="account_status_not_found">Servidor non atopado</string> + <string name="account_status_no_internet">Sen conectividade</string> + <string name="account_status_regis_fail">Erro no rexistro</string> + <string name="account_status_regis_conflict">O identificador xa está en uso</string> + <string name="account_status_regis_success">Rexistro completado</string> + <string name="account_status_regis_not_sup">O servidor non soporta rexistros</string> + <string name="encryption_choice_none">Texto plano</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Editar conta</string> + <string name="mgmt_account_delete">Eliminar conta</string> + <string name="mgmt_account_disable">Deshabilitar temporalmente</string> + <string name="mgmt_account_enable">Habilitar</string> + <string name="attach_record_voice">Grabar audio</string> + <string name="save">Gardar</string> + <string name="passwords_do_not_match">As contrasinais non coinciden</string> + <string name="invalid_jid">O identificador non é un identificador de Jabber válido</string> + <string name="pref_ui_options">Opcións de interfaz</string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-it/arrays.xml b/src/main/res/values-it/arrays.xml new file mode 100644 index 00000000..491c4438 --- /dev/null +++ b/src/main/res/values-it/arrays.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Cellulare</item> + <item>Telefono</item> + <item>Tablet</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>mai</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + <string-array name="mute_options_descriptions"> + <item>30 minuti</item> + <item>un\'ora</item> + <item>2 ore</item> + <item>8 ore</item> + <item>fino avviso ulteriore</item> + </string-array> + + <integer-array name="mute_options_durations"> + <item>1800</item> + <item>3600</item> + <item>7200</item> + <item>28800</item> + <item>-1</item> + </integer-array> + +</resources> diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml new file mode 100644 index 00000000..0bb0e05e --- /dev/null +++ b/src/main/res/values-it/strings.xml @@ -0,0 +1,260 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Impostazioni</string> + <string name="action_add">Nuova conversazione</string> + <string name="action_accounts">Gestisci utenti</string> + <string name="action_end_conversation">Termina questa conversazione</string> + <string name="action_contact_details">Dettagli del contatto</string> + <string name="action_muc_details">Dettagli conferenza</string> + <string name="action_secure">Conversazione sicura</string> + <string name="action_add_account">Aggiungi utente</string> + <string name="action_edit_contact">Modifica il nome</string> + <string name="action_add_phone_book">Aggiungi alla rubrica</string> + <string name="action_delete_contact">Cancella dalla lista</string> + <string name="title_activity_manage_accounts">Gestisci Utenti</string> + <string name="title_activity_settings">Impostazioni</string> + <string name="title_activity_conference_details">Dettagli conferenza</string> + <string name="title_activity_contact_details">Dettagli del contatto</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Condividi con Conversation</string> + <string name="title_activity_start_conversation">Inizia una Conversazione</string> + <string name="title_activity_choose_contact">Scegli contatto</string> + <string name="just_now">adesso</string> + <string name="minute_ago">1 min fa</string> + <string name="minutes_ago">%d min fa</string> + <string name="unread_conversations">Conversazioni non lette</string> + <string name="sending">invio…</string> + <string name="encrypted_message">Decifrazione del messaggio. Attendere prego…</string> + <string name="nick_in_use">Nome utente già in uso</string> + <string name="admin">Amministratore</string> + <string name="owner">Proprietario</string> + <string name="moderator">Moderatore</string> + <string name="participant">Partecipante</string> + <string name="visitor">Visitatore</string> + <string name="remove_contact_text">Vuoi rimuovere %s dalla tua lista contatti? La conversazione associata con questo contatto non sarà rimossa.</string> + <string name="remove_bookmark_text">Vuoi rimuovere il segnalibro %s? La conversazione associata con questo contatto non sarà rimossa.</string> + <string name="register_account">Registra un nuovo account sul server</string> + <string name="share_with">Condividi con</string> + <string name="start_conversation">Inizia Conversazione</string> + <string name="invite_contact">Invita Contatto</string> + <string name="contacts">Contatti</string> + <string name="cancel">Cancella</string> + <string name="add">Aggiungi</string> + <string name="edit">Modifica</string> + <string name="delete">Elimina</string> + <string name="save">Salva</string> + <string name="ok">OK</string> + <string name="crash_report_title">Conversations è crashato</string> + <string name="crash_report_message">Se scegli di inviare una segnalazione dell\'errore aiuterai lo sviluppo di Conversations\n<b>Attenzione:</b> Questo utilizzerà il tuo account XMPP per inviare la segnalazione agli sviluppatori.</string> + <string name="send_now">Invia adesso</string> + <string name="send_never">Non chiedere mai più</string> + <string name="problem_connecting_to_account">Impossibile collegarsi all\'utente</string> + <string name="problem_connecting_to_accounts">Impossibile collegarsi a più utenti</string> + <string name="touch_to_fix">Tocca qui per gestire i tuoi utenti</string> + <string name="attach_file">Allega file</string> + <string name="not_in_roster">Il contatto non è nella tua lista. Vuoi aggiungerlo?</string> + <string name="add_contact">Aggiungi contatto</string> + <string name="send_failed">Invio fallito</string> + <string name="send_rejected">rifiutato</string> + <string name="receiving_image">Ricezione di un\'immagine. Attendere prego…</string> + <string name="preparing_image">Preparazioone immagine per la trasmissione</string> + <string name="action_clear_history">Pulisci la cronologia</string> + <string name="clear_conversation_history">Pulisci la cronologia della Conversazione</string> + <string name="clear_histor_msg">Vuoi cancellare tutti i messaggi di questa Conversazione?\n\n<b>Attenzione:</b> Questo non influenzerà i messaggi presenti su altri dispositivi o server.</string> + <string name="delete_messages">Elimina messaggi</string> + <string name="also_end_conversation">Termina questa conversazione in seguito</string> + <string name="choose_presence">Choose presence to contact</string> + <string name="send_plain_text_message">Invia messaggio di testo semplice</string> + <string name="send_otr_message">Invia messaggio cifrato con OTR</string> + <string name="send_pgp_message">Invia messaggio cifrato con OpenPGP</string> + <string name="your_nick_has_been_changed">Il tuo nome utente èstato cambiato</string> + <string name="download_image">Scarica Immagine</string> + <string name="image_offered_for_download"><i>Immagine disponibile per il download</i></string> + <string name="send_unencrypted">Invia non cifrato</string> + <string name="decryption_failed">Decifrazione fallita. Forse non disponi della chiave privata corretta.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations usa una app di terze parti chiamata <b>OpenKeychain</b> per cifrare e decifrare i messaggi per gestire le tue chiavi pubbliche.\n\nOpenKeychain è rilasciato secondo i termini della GPLv3 ed è disponibile sia su F-Droid, che su Google Play.\n\n<small>(Riavvia Conversations in seguito.)</small></string> + <string name="restart">Riavvia</string> + <string name="install">Installa</string> + <string name="offering">offrendo…</string> + <string name="waiting">in attesa…</string> + <string name="no_pgp_key">Nessuna chiave OpenPGP trovata</string> + <string name="contact_has_no_pgp_key">Conversations non è in grado di cifrare i tuoi messaggi perchè il contatto non sta annunciando la sua chiave pubblica.\n\n<small>Per favore chiedi al tuo contatto di configurare OpenPGP.</small></string> + <string name="no_pgp_keys">Nessuna chiave OpenPGP trovata</string> + <string name="contacts_have_no_pgp_keys">Conversations non è in grado di cifrare i tuoi messaggi perchè i contatti non stanno annunciando la propria chiave pubblica.\n\n<small>Per favore chiedi ai tuoi contatti di configurare OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Messaggio cifrato ricevuto. Tocca per decifrare.</i></string> + <string name="encrypted_image_received"><i>Immagine cifrata ricevuta. Tocca per decifrare e mostrare.</i></string> + <string name="image_file"><i>Immagine ricevuta. Tocca per mostrare</i></string> + <string name="pref_general">Generale</string> + <string name="pref_xmpp_resource">Risorsa XMPP</string> + <string name="pref_xmpp_resource_summary">Il nome con il quale questo client si identifica</string> + <string name="pref_accept_files">Accetta i file</string> + <string name="pref_accept_files_summary">Accetta automaticamente i file più piccoli di…</string> + <string name="pref_notification_settings">Impostazioni di Notifica</string> + <string name="pref_notifications">Notifiche</string> + <string name="pref_notifications_summary">Notifica quando arriva un nuovo messaggio</string> + <string name="pref_vibrate">Vibra</string> + <string name="pref_vibrate_summary">Vibra anche quando arriva un nuovo messaggio</string> + <string name="pref_sound">Suono</string> + <string name="pref_sound_summary">Riproduci una suoneria con la notifica</string> + <string name="pref_conference_notifications">Notifiche Conferenze</string> + <string name="pref_conference_notifications_summary">Notifica sempre quando arriva un nuovo messaggio da una conferenza, invece che solo quando in primo piano</string> + <string name="pref_notification_grace_period">Periodo tra notifiche</string> + <string name="pref_notification_grace_period_summary">Disabilita le notifiche per un breve lasso di tempo dopo che un messaggio è stato ricevuto</string> + <string name="pref_advanced_options">Opzioni Avanzate</string> + <string name="pref_never_send_crash">Non inviare mai segnalazioni di errore</string> + <string name="pref_never_send_crash_summary">Se scegli di inviare una segnalazione dell\'errore aiuterai lo sviluppo di Conversations</string> + <string name="pref_confirm_messages">Conferma Messaggi</string> + <string name="pref_confirm_messages_summary">Fai sapere ai tuoi contatti quando hai ricevuto il messaggio e l\'hai letto</string> + <string name="pref_ui_options">Opzioni Interfaccia</string> + <string name="openpgp_error">OpenKeychain ha riportato un errore</string> + <string name="error_decrypting_file">Errore di I/O nel decifrare il file</string> + <string name="accept">Accetta</string> + <string name="error">Si è verificato un errore</string> + <string name="pref_grant_presence_updates">Concedi aggiornamenti della presenza</string> + <string name="pref_grant_presence_updates_summary">Concedi e chiedi preventivamente la sottoscrizione della presenza ai contatti che hai creato</string> + <string name="subscriptions">Sottoscrizioni</string> + <string name="your_account">Il tuo utente</string> + <string name="keys">Chiavi</string> + <string name="send_presence_updates">Invia aggiornamenti della presenza</string> + <string name="receive_presence_updates">Ricevi aggiornamenti della presenza</string> + <string name="ask_for_presence_updates">Chiedi aggiornamenti della presenza</string> + <string name="attach_choose_picture">Scegli un\'immagine</string> + <string name="attach_take_picture">Foto</string> + <string name="preemptively_grant">Concedi aggiornamenti della presenza preventivamente</string> + <string name="error_not_an_image_file">Il file selezionato non è un\'immagine</string> + <string name="error_compressing_image">Errore durante la conversione dell\'immagine</string> + <string name="error_file_not_found">File non trovato</string> + <string name="error_io_exception">Errore di I/O generico. Forse hai esaurito lo spazio?</string> + <string name="error_security_exception_during_image_copy">L\'app che hai usato per selezionare questa immagine non ci ha fornito permessi sufficienti per leggere il file.\n\n<small>Usa un file manager differente per scegliere un\'immagine</small></string> + <string name="account_status_unknown">Sconosciuto</string> + <string name="account_status_disabled">Disabilitato temporaneamente</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">In connessione\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Non autorizzato</string> + <string name="account_status_not_found">Server non trovato</string> + <string name="account_status_no_internet">Connettività assente</string> + <string name="account_status_regis_fail">Registrazione fallita</string> + <string name="account_status_regis_conflict">Nome utente già in uso</string> + <string name="account_status_regis_success">Registrazione completata</string> + <string name="account_status_regis_not_sup">Il Server non supporta la registrazione</string> + <string name="encryption_choice_none">Testo semplice</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Modifica utente</string> + <string name="mgmt_account_delete">Elimina utente</string> + <string name="mgmt_account_disable">Disabilita temporaneamente</string> + <string name="mgmt_account_publish_avatar">Pubblica avatar</string> + <string name="mgmt_account_publish_pgp">Pubblica chiave pubblica OpenPGP</string> + <string name="mgmt_account_enable">Abilita utente</string> + <string name="mgmt_account_are_you_sure">Sei sicuro?</string> + <string name="mgmt_account_delete_confirm_text">Se cancelli il tuo utente la cronologia delle tue conversazioni verrà persa</string> + <string name="attach_record_voice">Registra la voce</string> + <string name="account_settings_jabber_id">ID Jabber</string> + <string name="account_settings_password">Password</string> + <string name="account_settings_example_jabber_id">utente@esempio.com</string> + <string name="account_settings_confirm_password">Conferma password</string> + <string name="password">Password</string> + <string name="confirm_password">Conferma password</string> + <string name="passwords_do_not_match">Le Password non corrispondono</string> + <string name="invalid_jid">Questo non è un ID Jabber valido</string> + <string name="error_out_of_memory">Memoria esaurita. L\'immagine è tropppo grande</string> + <string name="add_phone_book_text">Vuoi aggiungere %s alla rubrica del telefono?</string> + <string name="contact_status_online">online</string> + <string name="contact_status_free_to_chat">vuole chattare</string> + <string name="contact_status_away">assente</string> + <string name="contact_status_extended_away">assenza prolungata</string> + <string name="contact_status_do_not_disturb">non disturbare</string> + <string name="contact_status_offline">offline</string> + <string name="muc_details_conference">Conferenza</string> + <string name="muc_details_other_members">Altri Membri</string> + <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> + <string name="server_info_stream_management">XEP-0198: Stream Management</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="server_info_available">disponibile</string> + <string name="server_info_unavailable">non disponibile</string> + <string name="missing_public_keys">Annuncio chiave pubblica non effettuato</string> + <string name="last_seen_now">visto adesso</string> + <string name="last_seen_min">visto 1 minuto fa</string> + <string name="last_seen_mins">visto %d minuti fa</string> + <string name="last_seen_hour">visto 1 ora fa</string> + <string name="last_seen_hours">visto %d ore fa</string> + <string name="last_seen_day">visto 1 giorno fa</string> + <string name="last_seen_days">visto %d giorni fa</string> + <string name="never_seen">mai visto</string> + <string name="install_openkeychain">Messaggio cifrato. Installa OpenKeychain per decifrare.</string> + <string name="unknown_otr_fingerprint">Impronta OTR sconosciuta</string> + <string name="openpgp_messages_found">Messaggi cifrati con OpenPGP trovati</string> + <string name="reception_failed">Ricezione fallita</string> + <string name="your_fingerprint">La tua impronta</string> + <string name="otr_fingerprint">Impronta OTR</string> + <string name="verify">Verifica</string> + <string name="decrypt">Decripta</string> + <string name="conferences">Conferenze</string> + <string name="search">Cerca</string> + <string name="create_contact">Crea Contatto</string> + <string name="join_conference">Entra in Conferenza</string> + <string name="delete_contact">Elimina Contatto</string> + <string name="view_contact_details">Mostra dettagli contatto</string> + <string name="create">Crea</string> + <string name="contact_already_exists">Il contatto esiste già</string> + <string name="join">Entra</string> + <string name="conference_address">Indirizzo conferenza</string> + <string name="conference_address_example">room@conference.example.com</string> + <string name="save_as_bookmark">Salva come segnalibro</string> + <string name="delete_bookmark">Elimina segnalibro</string> + <string name="bookmark_already_exists">Questo segnalibro esiste già</string> + <string name="you">Tu</string> + <string name="action_edit_subject">Modifica soggetto conferenza</string> + <string name="conference_not_found">Conferenza non trovata</string> + <string name="leave">Abbandona</string> + <string name="contact_added_you">Il contatto ti ha aggiunto alla sua lista contatti</string> + <string name="add_back">Add back</string> + <string name="contact_has_read_up_to_this_point">%s ha letto fino a questo punto</string> + <string name="publish">Pubblica</string> + <string name="touch_to_choose_picture">Tocca l\'avatar per selezionare l\'immagine dalla gallaria</string> + <string name="publish_avatar_explanation">Nota bene: tutti i contatti sottoscritti agli aggiornamenti della tua presenza avranno il permesso di vedere questa immagine.</string> + <string name="publishing">Pubblicazione…</string> + <string name="error_publish_avatar_server_reject">Il server ha rifiutato la tua pubblicazione</string> + <string name="error_publish_avatar_converting">Qualcosa è andato storto durante la conversione della tua immagine</string> + <string name="error_saving_avatar">Impossibile salvare l\'avatar sulla memoria interna</string> + <string name="or_long_press_for_default">(O premi a lungo per ripristinare le impostazioni di default)</string> + <string name="error_publish_avatar_no_server_support">Il tuo server non supporta la pubblicazione degli avatar</string> + <string name="private_message">sussurrato</string> + <string name="private_message_to">a %s</string> + <string name="send_private_message_to">Invia messaggio privato a %s</string> + <string name="connect">Connetti</string> + <string name="account_already_exists">Questo utente esiste già</string> + <string name="next">Successivo</string> + <string name="server_info_session_established">Sessione corrente stabilita</string> + <string name="additional_information">Informazioni Aggiuntive</string> + <string name="skip">Salta</string> + <string name="disable_notifications">Disabilita le notifiche</string> + <string name="disable_notifications_for_this_conversation">Disabilita le notifiche per questa conversazione</string> + <string name="notifications_disabled">Le notifiche sono disabilitate</string> + <string name="enable">Abilita</string> + <string name="conference_requires_password">La conferenza richiede una password</string> + <string name="enter_password">Inserisci la password</string> + <string name="missing_presence_updates">Aggiornamenti della presenza del contatto mancanti</string> + <string name="request_presence_updates">Richiedi gli aggiornamenti della presenza dal tuo contatto.\n\n<small>Questo verrà usato per determinare quali client sta usando il tuo contatto.</small></string> + <string name="request_now">Rechiedi adesso</string> + <string name="delete_fingerprint">Elimina Impronta</string> + <string name="sure_delete_fingerprint">Sei sicuro di voler eliminare questa impronta?</string> + <string name="ignore">Ignora</string> + <string name="without_mutual_presence_updates"><b>Attenzione:</b> Inviando questo messaggio senza aggiornamenti della presenza reciproci potrebbe causare problemi inaspettati.\n\n<small>Vai nei dettagli del contatto per verificare le tue sottoscrizioni alla presenza.</small></string> + <string name="pref_encryption_settings">Impostazioni di cifratura</string> + <string name="pref_force_encryption">Forza cifratura end-to-end</string> + <string name="pref_force_encryption_summary">Manda sempre messaggi cifrati (ad eccezione delle conferenze)</string> + <string name="pref_dont_save_encrypted">Non salvare i messaggi cifrati</string> + <string name="pref_dont_save_encrypted_summary">Attenzione: Questo potrebbe comportare la perdita di messaggi</string> + <string name="pref_expert_options">Opzioni da Esperto</string> + <string name="pref_expert_options_summary">Fai attenzione con queste impostazioni</string> + <string name="pref_use_larger_font">Aumenta la dimensione dei font</string> + <string name="pref_use_larger_font_summary">Usa font più grandi in tutta l\'app</string> + <string name="pref_use_send_button_to_indicate_status">Il pulsante di invio indica lo stato</string> + <string name="pref_use_send_button_to_indicate_status_summary">Colora il pulsante di invio per indicare lo stato di un contatto</string> + +</resources> diff --git a/src/main/res/values-iw/arrays.xml b/src/main/res/values-iw/arrays.xml new file mode 100644 index 00000000..28768d6c --- /dev/null +++ b/src/main/res/values-iw/arrays.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>נייד</item> + <item>טלפון</item> + <item>טאבלט</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>אף פעם</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml new file mode 100644 index 00000000..fd8eaa0b --- /dev/null +++ b/src/main/res/values-iw/strings.xml @@ -0,0 +1,224 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">הגדרות</string> + <string name="action_add">דיון חדש</string> + <string name="action_accounts">נהל חשבונות</string> + <string name="action_end_conversation">סיים את דיון זה</string> + <string name="action_contact_details">פרטי איש קשר</string> + <string name="action_muc_details">פרטי ועידה</string> + <string name="action_secure">דיון מאובטח</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="title_activity_manage_accounts">נהל חשבונות</string> + <string name="title_activity_settings">הגדרות</string> + <string name="title_activity_conference_details">פרטי ועידה</string> + <string name="title_activity_contact_details">פרטי איש קשר</string> + <string name="title_activity_conversations">דיונים</string> + <string name="title_activity_sharewith">שתף בעזרת Conversations</string> + <string name="title_activity_start_conversation">התחל דיון</string> + <string name="title_activity_choose_contact">בחר איש קשר</string> + <string name="just_now">רק כעת</string> + <string name="minute_ago">לפני דקה 1</string> + <string name="minutes_ago">לפני %d דקות</string> + <string name="unread_conversations">דיונים שלא נקראו</string> + <string name="sending">כעת שולח…</string> + <string name="encrypted_message">כעת מפענח הודעה. אנא המתן…</string> + <string name="nick_in_use">שם כינוי כבר מצוי בשימוש</string> + <string name="admin">מנהל</string> + <string name="owner">בעלים</string> + <string name="moderator">אחראי</string> + <string name="participant">משתתף</string> + <string name="visitor">מבקר</string> + <string name="remove_contact_text">האם ברצונך להסיר את %s מתןך הרשימה שלך? הדיונים אשר משוייכים עם חשבון זה לא יוסרו.</string> + <string name="remove_bookmark_text">האם ברצונך להסיר את %s בתוור סימנייה? הדיונים אשר משוייכים עם סימנייה זו לא יוסרו.</string> + <string name="register_account">רשום חשבון חדש על שרת</string> + <string name="share_with">שתף בעזרת</string> + <string name="start_conversation">התחל דיון</string> + <string name="invite_contact">הזמן איש קשר</string> + <string name="contacts">אנשי קשר</string> + <string name="cancel">ביטול</string> + <string name="add">הוסף</string> + <string name="edit">ערוך</string> + <string name="delete">מחק</string> + <string name="save">שמור</string> + <string name="ok">אישור</string> + <string name="crash_report_title">Conversations קרסה</string> + <string name="crash_report_message">על ידי שליחת עקבות מחסנית אתה עוזר להתקדמות הפיתוח של Conversations\n<b>אזהרה:</b> זו תעשה שימוש בחשבון XMPP שלך כדי לשלוח עקבות מחסנית אל המפתח.</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="not_in_roster">איש קשר אינו מצוי בתוך הרשימה שלך. האם ברצונך להוסיפו?</string> + <string name="add_contact">הוסף איש קשר</string> + <string name="send_failed">מסירה נכשלה</string> + <string name="send_rejected">סורב</string> + <string name="receiving_image">כעת מקבל קובץ תצלום. אנא המתן…</string> + <string name="preparing_image">כעת מכין תצלום לשם תמסורת</string> + <string name="action_clear_history">טהר היסטוריה</string> + <string name="clear_conversation_history">טהר היסטוריית דיונים</string> + <string name="clear_histor_msg">האם ברצונך למחוק את כל ההודעות בתוך דיון זה?\n\n<b>אזהרה:</b> זו לא תשפיע על הודעות מאוחסנות על מכשירים או שרתים אחרים.</string> + <string name="delete_messages">מחק הודעות</string> + <string name="also_end_conversation">סיים את דיון זה לאחר מכן</string> + <string name="choose_presence">בחר נוכחות לאיש קשר</string> + <string name="send_plain_text_message">שלח הודעת טקסט גלוי</string> + <string name="send_otr_message">שלח הודעה מוצפנת OTR</string> + <string name="send_pgp_message">שלח הודעה מוצפנת OpenPGP</string> + <string name="your_nick_has_been_changed">שם כינוי שלך השתנה</string> + <string name="download_image">הורד תצלום</string> + <string name="image_offered_for_download"><i>קובץ תצלום מוצע להורדה</i></string> + <string name="send_unencrypted">שלח לא מוצפנת</string> + <string name="decryption_failed">פענוח נכשל. אולי אין לך את המפתח הפרטי המתאים.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations מפיקה תועלת מן אפליקציית צד-שלישי הקרויה <b>OpenKeychain</b> כדי להצפין ולפענח הודעות וגם כדי לנהל את המפתחות הפומביים שלך.\n\nOpenKeychain הינה רשויה תחת GPLv3 וזמינה אצל F-Droid וגם Google Play.\n\n<small>(אנא התחל מחדש את Conversations לאחר מכן.)</small></string> + <string name="restart">התחל מחדש</string> + <string name="install">התקן</string> + <string name="offering">כעת מציע…</string> + <string name="waiting">כעת ממתין…</string> + <string name="no_pgp_key">לא נמצא מפתח OpenPGP</string> + <string name="contact_has_no_pgp_key">Conversations אינה מסוגלת להצפין את הודעותיך משום שאיש הקשר שלך אינו מכריז על המפתח הפומבי שלו או שלה.\n\n<small>אנא בקש מאיש הקשר שלך לארגן OpenPGP.</small></string> + <string name="no_pgp_keys">לא נמצאו מפתחות OpenPGP</string> + <string name="contacts_have_no_pgp_keys">Conversations אינה מסוגלת להצפין את הודעותיך משום שאנשי הקשר שלך אינם מכריזים על המפתח הפומבי שלהם.\n\n<small>אנא בקש מאנשי הקשר שלך לארגן OpenPGP.</small></string> + <string name="encrypted_message_received"><i>הודעה מוצפנת התקבלה. לחץ כדי לצפות ולפענח.</i></string> + <string name="encrypted_image_received"><i>תצלום מוצפן התקבל. לחץ כדי לצפות ולפענח.</i></string> + <string name="image_file"><i>תצלום התקבל. לחץ כדי לצפות</i></string> + <string name="pref_xmpp_resource">משאב XMPP</string> + <string name="pref_xmpp_resource_summary">השם שלקוח זה מזהה את עצמו עם</string> + <string name="pref_accept_files">קבל קבצים</string> + <string name="pref_accept_files_summary">קבל אוטומטית קבצים קטנים יותר מאשר…</string> + <string name="pref_notification_settings">הגדרות התראה</string> + <string name="pref_notifications">התראות</string> + <string name="pref_notifications_summary">תודיע כאשר הודעה חדשה מגיעה</string> + <string name="pref_vibrate">הרטט</string> + <string name="pref_vibrate_summary">הרטט גם כאשר הודעה חדשה מגיעה</string> + <string name="pref_sound">צליל</string> + <string name="pref_sound_summary">נגן צלצול עם התראה</string> + <string name="pref_conference_notifications">התראות ועידה</string> + <string name="pref_conference_notifications_summary">תמיד תודיע כאשר הודעת ועידה חדשה מגיעה במקום רק כאשר מודגשת</string> + <string name="pref_notification_grace_period">משך ארכת התראה</string> + <string name="pref_notification_grace_period_summary">נטרל התראות לזמן קצר לאחר שהודעת פחם התקבלה</string> + <string name="pref_advanced_options">אפשרויות מתקדמות</string> + <string name="pref_never_send_crash">לעולם אל תשלח דיווחי קריסה</string> + <string name="pref_never_send_crash_summary">על ידי שליחת עקבות מחסנית אתה עוזר להתקדמות הפיתוח של Conversations</string> + <string name="pref_confirm_messages">אשר הודעות</string> + <string name="pref_confirm_messages_summary">אפשר לאיש קשר שלך לדעת מתי קיבלת וקראת הודעה</string> + <string name="openpgp_error">OpenKeychain דיווח שגיאה</string> + <string name="error_decrypting_file">שגיאת I/O פענוח קובץ</string> + <string name="accept">קבל</string> + <string name="error">אירעה שגיאה</string> + <string name="pref_grant_presence_updates">הענק עדכוני נוכחות</string> + <string name="pref_grant_presence_updates_summary">הענק ובקש הרשמות נוכחות מראש עבור אנשי קשר שיצרת</string> + <string name="subscriptions">הרשמות</string> + <string name="your_account">החשבון שלך</string> + <string name="keys">מפתחות</string> + <string name="send_presence_updates">שלח עדכוני נוכחות</string> + <string name="receive_presence_updates">קבל עדכוני נוכחות</string> + <string name="ask_for_presence_updates">בקש עדכוני נוכחות</string> + <string name="attach_choose_picture">בחר תמונה</string> + <string name="attach_take_picture">קח תמונה</string> + <string name="preemptively_grant">הענק בקשת הרשמה מראש</string> + <string name="error_not_an_image_file">הקובץ שבחרת אינו תצלום</string> + <string name="error_compressing_image">שגיאה במהלך המרת קובץ תצלום</string> + <string name="error_file_not_found">קובץ לא נמצא</string> + <string name="error_io_exception">שגיאת I/O כללית. אולי אזל לך נפח אחסון?</string> + <string name="error_security_exception_during_image_copy">האפליקציה בה השתמשת כדי לבחור את תצלום זה לא סיפקה לנו מספיק הרשאות כדי לקרוא את הקובץ.\n\n<small>השתמש במנהל קבצים אחר כדי לבחור תצלום</small></string> + <string name="account_status_unknown">לא ידוע</string> + <string name="account_status_disabled">מנוטרל זמנית</string> + <string name="account_status_online">מקוון</string> + <string name="account_status_connecting">כעת מתחבר\u2026</string> + <string name="account_status_offline">לא מקוון</string> + <string name="account_status_unauthorized">לא מורשה</string> + <string name="account_status_not_found">שרת לא נמצא</string> + <string name="account_status_no_internet">אין חיבוריות</string> + <string name="account_status_regis_fail">הרשמה נכשלה</string> + <string name="account_status_regis_conflict">שם משתמש כבר מצוי בשימוש</string> + <string name="account_status_regis_success">הרשמה הושלמה</string> + <string name="account_status_regis_not_sup">שרת לא תומך הרשמה</string> + <string name="encryption_choice_none">טקסט גלוי</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">ערוך חשבון</string> + <string name="mgmt_account_delete">מחק</string> + <string name="mgmt_account_disable">נטרל זמנית</string> + <string name="mgmt_account_publish_avatar">פרסם אווטאר</string> + <string name="mgmt_account_enable">אפשר</string> + <string name="mgmt_account_are_you_sure">האם אתה בטוח?</string> + <string name="mgmt_account_delete_confirm_text">אם אתה מוחק את חשבונך כל היסטוריית הדיון שלך תאבד</string> + <string name="attach_record_voice">הקלט קול</string> + <string name="account_settings_jabber_id">מזהה Jabber</string> + <string name="account_settings_password">סיסמה</string> + <string name="account_settings_example_jabber_id">username@example.com</string> + <string name="account_settings_confirm_password">אמת סיסמה</string> + <string name="password">סיסמה</string> + <string name="confirm_password">אמת סיסמה</string> + <string name="passwords_do_not_match">סיסמאות לא תואמות</string> + <string name="invalid_jid">זה אינו מזהה Jabber תקף</string> + <string name="error_out_of_memory">חסר זיכרון. תצלום גדול מדי</string> + <string name="add_phone_book_text">האם ברצונך להוסיף את %s אל רשימת קשר טלפונית?</string> + <string name="contact_status_online">מקוון</string> + <string name="contact_status_free_to_chat">חופשי לשיחה</string> + <string name="contact_status_away">נעדר</string> + <string name="contact_status_extended_away">נעדר לזמן מה</string> + <string name="contact_status_do_not_disturb">אל תפריעו</string> + <string name="contact_status_offline">לא מקוון</string> + <string name="muc_details_conference">ועידה</string> + <string name="muc_details_other_members">חברים אחרים</string> + <string name="server_info_carbon_messages">הודעות פחם</string> + <string name="server_info_stream_management">ניהול זרם</string> + <string name="missing_public_keys">הכרזות מפתח פומבי חסרות</string> + <string name="last_seen_now">נראה לאחרונה ממש עכשיו</string> + <string name="last_seen_min">נראה לאחרונה לפני דקה 1</string> + <string name="last_seen_mins">נראה לאחרונה לפני %d דקות</string> + <string name="last_seen_hour">נראה לאחרונה לפני שעה 1</string> + <string name="last_seen_hours">נראה לאחרונה לפני %d שעות ago</string> + <string name="last_seen_day">נראה לאחרונה לפני יום 1</string> + <string name="last_seen_days">נראה לאחרונה לפני %d ימים</string> + <string name="never_seen">לא נראה מעולם</string> + <string name="install_openkeychain">הודעה מוצפנת. אנא התקן OpenKeychain כדי לפענח.</string> + <string name="unknown_otr_fingerprint">טביעת אצבע OTR לא מוכרת</string> + <string name="openpgp_messages_found">הודעות מוצפנות OpenPGP נמצאו</string> + <string name="reception_failed">קבלה נכשלה</string> + <string name="your_fingerprint">טביעת אצבע שלך</string> + <string name="otr_fingerprint">טביעת אצבע OTR</string> + <string name="verify">אמת</string> + <string name="decrypt">פענח</string> + <string name="conferences">ועידות</string> + <string name="search">חפש</string> + <string name="create_contact">צור איש קשר</string> + <string name="join_conference">הצטרף לועידה</string> + <string name="delete_contact">מחק איש קשר</string> + <string name="view_contact_details">צפה בפרטי איש קשר</string> + <string name="create">צור</string> + <string name="contact_already_exists">איש קשר כבר קיים</string> + <string name="join">הצטרף</string> + <string name="conference_address">כתובת ועידה</string> + <string name="conference_address_example">room@conference.example.com</string> + <string name="save_as_bookmark">שמור בתור סימנייה</string> + <string name="delete_bookmark">מחק סימנייה</string> + <string name="bookmark_already_exists">סימנייה זו כבר קיימת</string> + <string name="you">אני</string> + <string name="action_edit_subject">ערוך נושא ועידה</string> + <string name="conference_not_found">ועידה לא נמצאה</string> + <string name="leave">עזוב</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="touch_to_choose_picture">לחץ על אווטאר כדי לבחור תמונה מתוך גלריה</string> + <string name="publish_avatar_explanation">לתשומת לבך: כל מי אשר רשום לעדכוני נוכחות שלך יורשה לראות את תמונה זו.</string> + <string name="publishing">כעת מפרסם…</string> + <string name="error_publish_avatar_server_reject">השרת פסל פרסום</string> + <string name="error_publish_avatar_converting">משהו השתבש במהלך המרת תמונה</string> + <string name="error_saving_avatar">לא היה מסוגל לשמור אווטאר אל כונן</string> + <string name="or_long_press_for_default">(או לחיצה ארוכה כדי להחזיר לשגרה)</string> + <string name="error_publish_avatar_no_server_support">שרתך לא תומך בפרסום של אווטארים</string> + <string name="private_message">בפרטי</string> + <string name="private_message_to">בפרטי אל %s</string> + <string name="send_private_message_to">שלח הודעה פרטית אל %s</string> + <string name="pref_ui_options">אפשרויות ממשק משתמש</string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-nl/arrays.xml b/src/main/res/values-nl/arrays.xml new file mode 100644 index 00000000..9ced79f4 --- /dev/null +++ b/src/main/res/values-nl/arrays.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Mobiel</item> + <item>Telefoon</item> + <item>Tablet</item> + <item>Conversaties</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>nooit</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml new file mode 100644 index 00000000..7b3faca9 --- /dev/null +++ b/src/main/res/values-nl/strings.xml @@ -0,0 +1,233 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversaties</string> + <string name="action_settings">Instellingen</string> + <string name="action_add">Nieuwe conversatie</string> + <string name="action_accounts">Beheer account</string> + <string name="action_end_conversation">Beëindig conversatie</string> + <string name="action_contact_details">Contact details</string> + <string name="action_muc_details">Gesprek details</string> + <string name="action_secure">Beveiligde conversatie</string> + <string name="action_add_account">Voeg account toe</string> + <string name="action_edit_contact">Verander naam</string> + <string name="action_add_phone_book">Voeg aan telefoonboek toe</string> + <string name="action_delete_contact">Verwijder uit lijst</string> + <string name="title_activity_manage_accounts">Beheer Accounts</string> + <string name="title_activity_settings">Instellingen</string> + <string name="title_activity_conference_details">Groepsconversatie Details</string> + <string name="title_activity_contact_details">Contact Details</string> + <string name="title_activity_conversations">Conversaties</string> + <string name="title_activity_sharewith">Delen met Conversatie</string> + <string name="just_now">net</string> + <string name="minute_ago">1 min geleden</string> + <string name="minutes_ago">%d min geleden</string> + <string name="unread_conversations">ongelezen Conversaties</string> + <string name="sending">versturen…</string> + <string name="encrypted_message">Bericht aan het ontsleutelen. Een moment geduld a.u.b.…</string> + <string name="nick_in_use">Naam is al in gebruik</string> + <string name="admin">Beheerder</string> + <string name="owner">Eigenaar</string> + <string name="moderator">Moderator</string> + <string name="participant">Deelnemer</string> + <string name="visitor">Bezoeker</string> + <string name="remove_contact_text">Wilt u %s uit uw lijst verwijderen? De conversatie met deze account zal niet worden verwijderd.</string> + <string name="register_account">Registreer nieuwe account op server</string> + <string name="share_with">Deel met</string> + <string name="start_conversation">Start Conversatie</string> + <string name="contacts">Contacten</string> + <string name="cancel">Annuleer</string> + <string name="add">Voeg toe</string> + <string name="edit">Bewerk</string> + <string name="delete">Verwijder</string> + <string name="save">Sla op</string> + <string name="ok">OK</string> + <string name="crash_report_title">Conversaties is gecrashed</string> + <string name="crash_report_message">Door het versturen van crash rapportages helpt u mee met de ontwikkeling van Conversaties.\n<b>Waarschuwing:</b> Deze app zal uw XMPP account gebruiken om de crash rapportages te versturen naar de ontwikkelaars.</string> + <string name="send_now">Nu versturen</string> + <string name="send_never">Niet opnieuw vragen</string> + <string name="problem_connecting_to_account">Account verbinden mislukt</string> + <string name="problem_connecting_to_accounts">Verbinden met meerdere accounts mislukt</string> + <string name="touch_to_fix">Raak hier aan om accounts te beheren</string> + <string name="attach_file">Voeg bestand bij</string> + <string name="not_in_roster">Het contact is geen onderdeel van uw lijst. Wilt u het toevoegen?</string> + <string name="add_contact">Voeg contact toe</string> + <string name="send_failed">afleveren mislukt</string> + <string name="send_rejected">geweigerd</string> + <string name="receiving_image">Bezig met ontvangen van afbeelding. Een moment geduld a.u.b.…</string> + <string name="preparing_image">Bezig met voorbereiden van het versturen van afbeelding</string> + <string name="action_clear_history">Wis geschiedenis</string> + <string name="clear_conversation_history">Wis conversatie geschiedenis</string> + <string name="clear_histor_msg">Wilt U alle berichten in deze Conversatie verwijderen?\n\n<b>Waarschuwing:</b> Dit zal geen invloed hebben op de berichten opgeslagen op andere apparaten of servers.</string> + <string name="delete_messages">Verwijder berichten</string> + <string name="also_end_conversation">Beëindig deze conversatie na afloop</string> + <string name="choose_presence">Kies aanwezigheid om te tonen aan contact</string> + <string name="send_plain_text_message">Verstuur eenvoudig tekst bericht</string> + <string name="send_otr_message">Verstuur OTR versleuteld bericht</string> + <string name="send_pgp_message">Verstuur OpenPGP versleuteld bericht</string> + <string name="your_nick_has_been_changed">Uw naam is veranderd</string> + <string name="download_image">Download Afbeelding</string> + <string name="image_offered_for_download"><i>Afbeelding aangeboden voor downloaden</i></string> + <string name="send_unencrypted">Verstuur onversleuteld</string> + <string name="decryption_failed">Ontsleutelen mislukt. Misschien hebt U niet de juiste private sleutel.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversaties gebruikt een derde partij app genaamd <b>OpenKeychain</b> om berichten te versleutelen en ontsleutelen, en om publieke sleutels te beheren.\n\nOpenKeychain is beschikbaar onder de GPLv3 en beschikbaar op F-Droid en Google Play.\n\n<small>(Herstart Conversaties na installatie.)</small></string> + <string name="restart">Herstart</string> + <string name="install">Installeer</string> + <string name="offering">offering…</string> + <string name="waiting">wachten…</string> + <string name="no_pgp_key">Geen OpenPGP sleutel gevonden</string> + <string name="contact_has_no_pgp_key">Conversaties kan Uw berichten niet versleutelen omdat uw contact geen publieke sleutel heeft ingesteld.\n\n<small>Vraag uw contact om OpenPGP te configureren.</small></string> + <string name="no_pgp_keys">Geen OpenPGP sleutels gevonden</string> + <string name="contacts_have_no_pgp_keys">Conversaties kan uw berichten niet versleutelen omdat uw contacten geen publieke sleutel hebben ingesteld.\n\n<small>Vraag uw contacten om OpenPGP te configureren.</small></string> + <string name="encrypted_message_received"><i>Versleuteld bericht ontvangen. Raak aan om te bekijken en te ontsleutelen.</i></string> + <string name="encrypted_image_received"><i>Versleutelde afbeelding ontvangen. Raak aan om te bekijken en te ontsleutelen.</i></string> + <string name="image_file"><i>Afbeelding ontvangen. Raak aan om te bekijken.</i></string> + <string name="pref_xmpp_resource">XMPP resource</string> + <string name="pref_xmpp_resource_summary">De naam waarmee deze client zich identificeert</string> + <string name="pref_accept_files">Accepteer bestanden</string> + <string name="pref_accept_files_summary">Accepteer automatisch bestanden kleiner dan…</string> + <string name="pref_notification_settings">Notificatie Instellingen</string> + <string name="pref_notifications">Notificaties</string> + <string name="pref_notifications_summary">Notificatie als een nieuw bericht arriveert</string> + <string name="pref_vibrate">Trillen</string> + <string name="pref_vibrate_summary">Tril ook wanneer een nieuw bericht arriveert</string> + <string name="pref_sound">Geluid</string> + <string name="pref_sound_summary">Speel ringtone af bij notificatie</string> + <string name="pref_conference_notifications">Groepsconversatie notificaties</string> + <string name="pref_conference_notifications_summary">Toon altijd notificaties als er nieuwe berichten arriveren in groepsconversaties in plaats van alleen bij highlighting</string> + <string name="pref_notification_grace_period">Notificatie uitstel periode</string> + <string name="pref_notification_grace_period_summary">Zet notificaties voor korte tijd uit als er een carbon copy wordt ontvangen</string> + <string name="pref_advanced_options">Geadvanceerde Opties</string> + <string name="pref_never_send_crash">Verstuur nooit crash rapportages</string> + <string name="pref_never_send_crash_summary">Door crash rapportages te versturen helpt U mee aan de ontwikkeling van Conversaties</string> + <string name="pref_confirm_messages">Bevestig Berichten</string> + <string name="pref_confirm_messages_summary">Laat uw contacten weten waneer U berichten hebt ontvangen en gelezen</string> + <string name="openpgp_error">OpenKeychain rapporteerde een fout</string> + <string name="error_decrypting_file">I/O Fout tijdens ontsleutelen bestand</string> + <string name="accept">Accepteer</string> + <string name="error">Er is een fout opgetreden</string> + <string name="pref_grant_presence_updates">Verleen toestemming voor aanwezigheid updates</string> + <string name="pref_grant_presence_updates_summary">Vantevoren toestemming verlenen en vragen aan contacten die U hebt aangemaakt</string> + <string name="subscriptions">Abonnementen</string> + <string name="your_account">Uw account</string> + <string name="keys">Sleutels</string> + <string name="send_presence_updates">Verstuur aanwezigheid updates</string> + <string name="receive_presence_updates">Ontvang aanwezigheid updates</string> + <string name="ask_for_presence_updates">Vraag naar aanwezigheid updates</string> + <string name="attach_choose_picture">Kies afbeelding</string> + <string name="attach_take_picture">Neem foto</string> + <string name="preemptively_grant">Vantevoren toestemming verlenen voor abonneren</string> + <string name="error_not_an_image_file">Het bestand dat U gekozen hebt is geen afbeelding</string> + <string name="error_compressing_image">Fout tijdens converteren van afbeelding</string> + <string name="error_file_not_found">Bestand niet gevonden</string> + <string name="error_io_exception">Generieke I/O fout. Misschien is er geen opslagruimte meer beschikbaar?</string> + <string name="error_security_exception_during_image_copy">De app die U gebruikte om de afbeelding te selecteren heeft niet voldoende toegang geleverd om het bestand te lezen.\n\n<small>Gebruik een andere app om een afbeelding te kiezen</small></string> + <string name="account_status_unknown">Onbekend</string> + <string name="account_status_disabled">Tijdelijk uitgezet</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">Verbinden\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Niet gemachtigd</string> + <string name="account_status_not_found">Server niet gevonden</string> + <string name="account_status_no_internet">Geen verbinding</string> + <string name="account_status_regis_fail">Registratie mislukt</string> + <string name="account_status_regis_conflict">Gebruikersnaam bezet</string> + <string name="account_status_regis_success">Registratie compleet</string> + <string name="account_status_regis_not_sup">Server ondersteunt geen registratie</string> + <string name="encryption_choice_none">Onversleuteld</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Bewerk account</string> + <string name="mgmt_account_delete">Verwijder</string> + <string name="mgmt_account_disable">Tijdelijk uitzetten</string> + <string name="mgmt_account_enable">Aanzetten</string> + <string name="mgmt_account_are_you_sure">Weet U het zeker?</string> + <string name="mgmt_account_delete_confirm_text">Als U uw account verwijderd wordt Uw volledige conversatie geschiedenis gewist</string> + <string name="attach_record_voice">Neem stem op</string> + <string name="account_settings_jabber_id">Jabber ID:</string> + <string name="account_settings_password">Wachtwoord:</string> + <string name="account_settings_example_jabber_id">gebruikersnaam@voorbeeld.nl</string> + <string name="account_settings_confirm_password">Bevestig wachtwoord:</string> + <string name="password">Wachtwoord</string> + <string name="confirm_password">Bevestig wachtwoord</string> + <string name="passwords_do_not_match">Wachtwoorden komen niet overeen</string> + <string name="invalid_jid">Dit is geen geldig Jabber ID</string> + <string name="error_out_of_memory">Geen geheugen beschikbaar. Afbeelding is te groot</string> + <string name="add_phone_book_text">Wilt U %s toevoegen aan de contactenlijst op uw telefoon?</string> + <string name="contact_status_online">online</string> + <string name="contact_status_free_to_chat">beschikbaar</string> + <string name="contact_status_away">weg</string> + <string name="contact_status_extended_away">langdurig weg</string> + <string name="contact_status_do_not_disturb">niet storen</string> + <string name="contact_status_offline">offline</string> + <string name="muc_details_conference">groepsconversatie</string> + <string name="muc_details_other_members">Andere Leden</string> + <string name="server_info_carbon_messages">Carbon Berichten</string> + <string name="server_info_stream_management">Stream Management</string> + <string name="missing_public_keys">Ontbrekende publieke sleutel aankondigingen</string> + <string name="last_seen_now">zonet voor het laatst gezien</string> + <string name="last_seen_min">1 minuut geleden voor het laatst gezien</string> + <string name="last_seen_mins">%d minuten geleden voor het laatst gezien</string> + <string name="last_seen_hour">1 uur geleden voor het laatst gezien</string> + <string name="last_seen_hours">%d uur geleden voor het laatst gezien</string> + <string name="last_seen_day">1 dag geleden voor het laatst gezien</string> + <string name="last_seen_days">%d dagen geleden voor het laatst gezien</string> + <string name="never_seen">nog nooit gezien</string> + <string name="install_openkeychain">Versleuteld bericht. Installeer OpenKeychain om te ontsleutelen.</string> + <string name="unknown_otr_fingerprint">Onbekende OTR vingerafdruk</string> + <string name="openpgp_messages_found">OpenPGP encrypted messages found</string> + <string name="reception_failed">Ontvangen mislukt</string> + <string name="join_conference">Aan groepsconversatie deelnemen</string> + <string name="invite_contact">Contact uitnodigen</string> + <string name="your_fingerprint">Uw vingerafdruk</string> + <string name="delete_bookmark">Bladwijzer verwijderen</string> + <string name="join">Deelnemen</string> + <string name="otr_fingerprint">OTR vingerafdruk</string> + <string name="you">U</string> + <string name="conference_not_found">Groepsconversatie niet gevonden</string> + <string name="search">Zoeken</string> + <string name="contact_already_exists">Het contact bestaat al</string> + <string name="title_activity_start_conversation">Start Groepsconversatie</string> + <string name="title_activity_choose_contact">Kies contact</string> + <string name="contact_added_you">Contact added you to contact list</string> + <string name="view_contact_details">Contactdetails bekijken</string> + <string name="conferences">Groepsconversaties</string> + <string name="verify">Controleren</string> + <string name="create_contact">Contact Aanmaken</string> + <string name="remove_bookmark_text">Wilt u %s als bladwijzer verwijderen? De groepsconversatie die verbonden is met deze bladwijzer zal niet verwijderd worden.</string> + <string name="action_edit_subject">Onderwerp van groepsconversatie veranderen</string> + <string name="delete_contact">Contact Verwijderen</string> + <string name="create">Aanmaken</string> + <string name="leave">Verlaten</string> + <string name="conference_address">Groepsconversatie adres</string> + <string name="save_as_bookmark">Bladwijzer toevoegen</string> + <string name="conference_address_example">kamer@groepsconversatie.voorbeeld.nl</string> + <string name="add_back">Terug toevoegen</string> + <string name="bookmark_already_exists">Deze bladwijzer bestaat al</string> + <string name="decrypt">Ontsleutelen</string> + <string name="contact_has_read_up_to_this_point">%s heeft tot hier gelezen</string> + <string name="next">Volgende</string> + <string name="publish_avatar_explanation">N.B.: Iedereen die uw aanwezigheid kan zien kan deze afbeelding zien.</string> + <string name="server_info_unavailable">niet beschikbaar</string> + <string name="mgmt_account_publish_pgp">Publiceer publieke OpenPGP sleutel</string> + <string name="additional_information">Extra informatie</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="skip">Overslaan</string> + <string name="connect">Verbinden</string> + <string name="account_already_exists">Dit account bestaat al</string> + <string name="private_message_to">naar %s</string> + <string name="send_private_message_to">Verstuur privé bericht aan %s</string> + <string name="touch_to_choose_picture">Klik op avatar om een afbeelding te selecteren uit de gallerij</string> + <string name="mgmt_account_publish_avatar">Publiceer avatar</string> + <string name="error_publish_avatar_server_reject">De server weigerde uw publicatie</string> + <string name="error_publish_avatar_converting">Er ging iets mis bij het converteren van uw afbeelding</string> + <string name="error_publish_avatar_no_server_support">Uw server ondersteunt de publicatie van avatars niet</string> + <string name="publishing">Publiceren…</string> + <string name="error_saving_avatar">Kon de avatar niet opslaan</string> + <string name="server_info_session_established">Huidige sessie opgezet</string> + <string name="or_long_press_for_default">(Of houdt lang ingedrukt om de oorspronkelijke terug te zetten)</string> + <string name="server_info_available">beschikbaar</string> + <string name="pref_ui_options">UI Opties</string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-ru/arrays.xml b/src/main/res/values-ru/arrays.xml new file mode 100644 index 00000000..d01d4eb9 --- /dev/null +++ b/src/main/res/values-ru/arrays.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Мобильный</item> + <item>Телефон</item> + <item>Планшет</item> + <item>Conversations</item> + <item>Андроид</item> + </string-array> + <string-array name="filesizes"> + <item>никогда</item> + <item>256 Кб</item> + <item>512 Кб</item> + <item>1 Мб</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml new file mode 100644 index 00000000..2aa26b0b --- /dev/null +++ b/src/main/res/values-ru/strings.xml @@ -0,0 +1,260 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Настройки</string> + <string name="action_add">Новая беседа</string> + <string name="action_accounts">Управление аккаунтами</string> + <string name="action_end_conversation">Закончить текущую беседу</string> + <string name="action_contact_details">Сведения о контакте</string> + <string name="action_muc_details">Сведения о конференции</string> + <string name="action_secure">Защищенная беседа</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="title_activity_manage_accounts">Управление Аккаунтами</string> + <string name="title_activity_settings">Настройки</string> + <string name="title_activity_conference_details">Сведения о Конференции</string> + <string name="title_activity_contact_details">Сведения о Контакте</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Поделиться</string> + <string name="title_activity_start_conversation">Начать беседу</string> + <string name="title_activity_choose_contact">Выберите собеседника</string> + <string name="just_now">только что</string> + <string name="minute_ago">1 минуту назад</string> + <string name="minutes_ago">%d мин. назад</string> + <string name="unread_conversations">непрочитанных сообщений</string> + <string name="sending">отправка…</string> + <string name="encrypted_message">Расшифровка сообщения. Пожалуйста, подождите…</string> + <string name="nick_in_use">Имя уже используется</string> + <string name="admin">Администратор</string> + <string name="owner">Владелец</string> + <string name="moderator">Модератор</string> + <string name="participant">Участник</string> + <string name="visitor">Посетитель</string> + <string name="remove_contact_text">Вы хотите удалить %s из своего списка? Беседы, связанные с этим аккаунтом будут сохранены.</string> + <string name="remove_bookmark_text">Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой будут сохранены</string> + <string name="register_account">Создать новый аккаунт на сервере</string> + <string name="share_with">Поделиться с</string> + <string name="start_conversation">Начать беседу</string> + <string name="invite_contact">Пригласить собеседника</string> + <string name="contacts">Контакты</string> + <string name="cancel">Отмена</string> + <string name="add">Добавить</string> + <string name="edit">Редактировать</string> + <string name="delete">Удалить</string> + <string name="save">Сохранить</string> + <string name="ok">ОК</string> + <string name="crash_report_title">Conversations был неожиданно остановлен</string> + <string name="crash_report_message">Отправляя отчеты об ошибках, вы помогаете исправить и улучшить программу, поддерживая дальнейшее развитие программы\n<b>Предупреждение:</b>Отчет об ошибке будет отправлен разработчику, используя ваш аккаунт XMPP.</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="not_in_roster">Контакт не находится в вашем списке. Хотите добавить его?</string> + <string name="add_contact">Добавить контакт</string> + <string name="send_failed">доставка не удалась</string> + <string name="send_rejected">отклонено</string> + <string name="receiving_image">Получение изображения. Пожалуйста подождите…</string> + <string name="preparing_image">Подготовка изображения к передаче</string> + <string name="action_clear_history">Очистить историю</string> + <string name="clear_conversation_history">Очистить историю</string> + <string name="clear_histor_msg">Вы хотите удалить все сообщения в этой беседе?\n\n<b>Предупреждение:</b> Данная операция не повлияет на сообщения, хранящиеся на других устройствах.</string> + <string name="delete_messages">Удалить сообщения</string> + <string name="also_end_conversation">Завершить беседу</string> + <string name="choose_presence">Укажите статус для контакта</string> + <string name="send_plain_text_message">Отправить незашифрованное текстовое сообщение</string> + <string name="send_otr_message">Отправить OTR защифрованное сообщение</string> + <string name="send_pgp_message">Отправить OpenPGP защифрованное сообщение</string> + <string name="your_nick_has_been_changed">Ваш псевдоним был изменен</string> + <string name="download_image">Загрузить изображение</string> + <string name="image_offered_for_download"><i>Изображение предложено для загрузки</i></string> + <string name="send_unencrypted">Отправить в незашифрованном виде</string> + <string name="decryption_failed">Расшифровка не удалась. Вероятно, что у вас нет надлежащего ключа.</string> + <string name="openkeychain_required">Установите OpenKeychain</string> + <string name="openkeychain_required_long">Conversations использует стороннее приложение под названием <b>OpenKeychain</b> для шифрования и расшифрования сообщений и управления открытыми ключами.\nПрограмма OpenKeychain распространяется под лицензией GPLv3 и доступна для загрузки через F-Droid или Google Play.\n\n<small>(Потребуется перезапуск Conversations после установки.)</small></string> + <string name="restart">Перезапуск</string> + <string name="install">Установка</string> + <string name="offering">предложение…</string> + <string name="waiting">ожидание…</string> + <string name="no_pgp_key">Нет OpenPGP ключа</string> + <string name="contact_has_no_pgp_key">Conversations не может зашифровать сообщение, потому что удаленный пользователь не анонсирует свой открытый ключ.\n\n<small>Пожалуйста, попросите удаленного пользователя тоже установить OpenPGP.</small></string> + <string name="no_pgp_keys">Нет OpenPGP ключей</string> + <string name="contacts_have_no_pgp_keys">Conversations не может зашифровать сообщения, потому что удаленные пользователи не анонсируют свои открытые ключи.\n\n<small>Пожалуйста, попросите удаленных пользователей тоже установить OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Зашифрованное сообщение получено. Нажмите здесь, чтобы расшифровать и посмотреть сообщение.</i></string> + <string name="encrypted_image_received"><i>Зашифрованное изображение получено. Нажмите здесь, чтобы расшифровать и посмотреть изображение.</i></string> + <string name="image_file"><i>Изображение получено. Нажмите здесь, чтобы посмотреть.</i></string> + <string name="pref_general">Общие</string> + <string name="pref_xmpp_resource">Название ресурса</string> + <string name="pref_xmpp_resource_summary">Имя которым Conversations идентифицирует себя</string> + <string name="pref_accept_files">Принимать файлы</string> + <string name="pref_accept_files_summary">Автоматический прием файлов…</string> + <string name="pref_notification_settings">Настройки Уведомлений</string> + <string name="pref_notifications">Уведомление</string> + <string name="pref_notifications_summary">Использовать звуковое уведомление когда приходят новые сообщения</string> + <string name="pref_vibrate">Вибрация</string> + <string name="pref_vibrate_summary">Использовать вибрацию когда приходят новые сообщения</string> + <string name="pref_sound">Звуковой сигнал</string> + <string name="pref_sound_summary">Выберите звуковой сигнал для сообщений</string> + <string name="pref_conference_notifications">Уведомления конференции</string> + <string name="pref_conference_notifications_summary">Всегда сообщать при получении нового сообщения в конференции</string> + <string name="pref_notification_grace_period">Отсрочка уведомлений</string> + <string name="pref_notification_grace_period_summary">Не использовать уведомления, если вы прочитали сообщение на другом устройстве</string> + <string name="pref_advanced_options">Дополнительные параметры</string> + <string name="pref_never_send_crash">Отчеты об ошибках</string> + <string name="pref_never_send_crash_summary">Отправляя отчеты об ошибках, вы помогаете исправить и улучшить Conversations, поддерживая дальнейшее развитие программы</string> + <string name="pref_confirm_messages">Отчеты о получении</string> + <string name="pref_confirm_messages_summary">Разрешить уведомлять отправителя, когда вы получили и прочитали сообщение</string> + <string name="pref_ui_options">Параметры интерфейса</string> + <string name="openpgp_error">Возникла ошибка в OpenKeychain</string> + <string name="error_decrypting_file">Ошибка расшифровки файла</string> + <string name="accept">Принять</string> + <string name="error">Произошла ошибка</string> + <string name="pref_grant_presence_updates">Предоставлять обновления</string> + <string name="pref_grant_presence_updates_summary">Разрешить и запрашивать статус присутствия для созданных вами контактов</string> + <string name="subscriptions">Подписки</string> + <string name="your_account">Ваш аккаунт</string> + <string name="keys">Ключи</string> + <string name="send_presence_updates">Анонсировать статус присутствия</string> + <string name="receive_presence_updates">Получать обновления статусов присутствия</string> + <string name="ask_for_presence_updates">Запрашивать обновления статусов присутствия</string> + <string name="attach_choose_picture">Выберите изображение</string> + <string name="attach_take_picture">Снимите изображение</string> + <string name="preemptively_grant">Удовлетворять запросы на подписки</string> + <string name="error_not_an_image_file">Выбранный файл не является изображением</string> + <string name="error_compressing_image">Ошибка при преобразовании изображения</string> + <string name="error_file_not_found">Файл не найден</string> + <string name="error_io_exception">Общая ошибка ввода/вывода. Возможно, на устройстве недостаточно свободного места?</string> + <string name="error_security_exception_during_image_copy">Приложение, которое было использовано для выбора изображения не имеет достаточных прав для чтения файла.\n\n<small>Используйте другой файловый менеджер, чтобы выбрать изображение</small></string> + <string name="account_status_unknown">Неизвестен</string> + <string name="account_status_disabled">Временно отключен</string> + <string name="account_status_online">В сети</string> + <string name="account_status_offline">Не в сети</string> + <string name="account_status_connecting">Соединение\u2026</string> + <string name="account_status_unauthorized">Неавторизован</string> + <string name="account_status_not_found">Сервер не найден</string> + <string name="account_status_no_internet">Нет подключения к сети</string> + <string name="account_status_regis_fail">Регистрация не удалась</string> + <string name="account_status_regis_conflict">Имя пользователя уже используется</string> + <string name="account_status_regis_success">Регистрация завершена</string> + <string name="account_status_regis_not_sup">Сервер не поддерживает регистрацию</string> + <string name="encryption_choice_none">Без шифрования</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Редактировать аккаунт</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> + <string name="mgmt_account_enable">Включить</string> + <string name="mgmt_account_are_you_sure">Вы уверены?</string> + <string name="mgmt_account_delete_confirm_text">Если вы удалите свой аккаунт, вся ваша история будет потеряна</string> + <string name="attach_record_voice">Запись голоса</string> + <string name="account_settings_jabber_id">JID (Джаббер ID)</string> + <string name="account_settings_password">Пароль</string> + <string name="account_settings_example_jabber_id">username@example.com</string> + <string name="account_settings_confirm_password">Подтвердите пароль</string> + <string name="password">Пароль</string> + <string name="confirm_password">Подтвердите пароль</string> + <string name="passwords_do_not_match">Пароли не совпадают</string> + <string name="invalid_jid">Недопустимый JID (Джаббер ID)</string> + <string name="error_out_of_memory">Недостаточно памяти. Изображение слишком большое</string> + <string name="add_phone_book_text">Вы хотите добавить %s в свою телефонную книгу?</string> + <string name="contact_status_online">в сети</string> + <string name="contact_status_free_to_chat">свободен для общения</string> + <string name="contact_status_away">скоро буду</string> + <string name="contact_status_extended_away">буду не скоро</string> + <string name="contact_status_do_not_disturb">не беспокоить</string> + <string name="contact_status_offline">не в сети</string> + <string name="muc_details_conference">Конференция</string> + <string name="muc_details_other_members">Другие участники</string> + <string name="server_info_carbon_messages">Дублирование сообщений</string> + <string name="server_info_stream_management">Управление потоками</string> + <string name="server_info_pep">XEP-0163: PEP (Аватары)</string> + <string name="server_info_available">доступен</string> + <string name="server_info_unavailable">недоступен</string> + <string name="missing_public_keys">Отсутствие анонсирования открытых ключей</string> + <string name="last_seen_now">Присутствие: только что</string> + <string name="last_seen_min">Присутствие: 1 минуту назад</string> + <string name="last_seen_mins">Присутствие: %d мин. назад</string> + <string name="last_seen_hour">Присутствие: 1 час назад</string> + <string name="last_seen_hours">Присутствие: %d час. назад</string> + <string name="last_seen_day">Присутствие: 1 день назад</string> + <string name="last_seen_days">Присутствие: %d дн. назад</string> + <string name="never_seen">Никогда</string> + <string name="install_openkeychain">Зашифрованное сообщение. Пожалуйста, установите OpenKeychain для дешифрования.</string> + <string name="unknown_otr_fingerprint">Неизвестная контрольная сумма криптографического протокола OTR</string> + <string name="openpgp_messages_found">Найдены OpenPGP зашифрованые сообщения</string> + <string name="reception_failed">Прием не удался</string> + <string name="your_fingerprint">Контрольная сумма</string> + <string name="otr_fingerprint">OTR контрольная сумма</string> + <string name="verify">Подтвердить</string> + <string name="decrypt">Дешифровать</string> + <string name="conferences">Конференции</string> + <string name="search">Поиск</string> + <string name="create_contact">Создать контакт</string> + <string name="join_conference">Присоединиться к конференции</string> + <string name="delete_contact">Удалить Контакт</string> + <string name="view_contact_details">Посмотреть данные контакта</string> + <string name="create">Создать</string> + <string name="contact_already_exists">Контакт уже существует</string> + <string name="join">Присоединиться</string> + <string name="conference_address">Адрес конференции</string> + <string name="conference_address_example">room@conference.example.com</string> + <string name="save_as_bookmark">Сохранить закладку</string> + <string name="delete_bookmark">Удалить закладку</string> + <string name="bookmark_already_exists">Такая закладка уже существует</string> + <string name="you">Вы</string> + <string name="action_edit_subject">Редактировать тему конференции</string> + <string name="conference_not_found">Конференция не найдена</string> + <string name="leave">Покинуть</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="touch_to_choose_picture">Нажмите на аватар, чтобы выбрать новую фотографию из галереи</string> + <string name="publish_avatar_explanation">Пожалуйста, обратите внимание, что этот аватар смогут увидеть все ваши подписчики</string> + <string name="publishing">Установка…</string> + <string name="error_publish_avatar_server_reject">Сервер отклонил размещение аватара</string> + <string name="error_publish_avatar_converting">В процессе преобразования фотографии возникла ошибка</string> + <string name="error_saving_avatar">Не удалось сохранить аватар</string> + <string name="or_long_press_for_default">(Или долгое прикосновение, чтобы вернуть значения по умолчанию)</string> + <string name="error_publish_avatar_no_server_support">Ваш сервер не поддерживает публикацию аватаров</string> + <string name="private_message">Отправить личное сообщение для %s</string> + <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="next">Далее</string> + <string name="server_info_session_established">Текущий сеанс установлен</string> + <string name="additional_information">Дополнительная информация</string> + <string name="skip">Пропустить</string> + <string name="disable_notifications">Отключить уведомления</string> + <string name="disable_notifications_for_this_conversation">Отключить уведомления для текущей беседы</string> + <string name="notifications_disabled">Уведомления отключены</string> + <string name="enable">Включить</string> + <string name="conference_requires_password">Конференция требует авторизации</string> + <string name="enter_password">Введите пароль</string> + <string name="missing_presence_updates">Обновления присутствия недоступны</string> + <string name="request_presence_updates">Пожалуйста, прежде запросите обновления присутствия у вашего собеседника.\n\n<small>Эта информация будет использоваться для определения того, каким клиентом(ами) пользуетя ваш собеседник.</small></string> + <string name="request_now">Запросить сейчас</string> + <string name="delete_fingerprint">Удалить Контрольную Сумму</string> + <string name="sure_delete_fingerprint">Вы уверены, что хотите удалить данную контрольную сумму?</string> + <string name="ignore">Отменить</string> + <string name="without_mutual_presence_updates"><b>Внимание:</b> Если обновления присутствия не включены на обеих сторонах, это может привести к возникновению неожиданных проблемам.\n\n<small>Уточните сведения о контакте, проверив настройки обновлений присутствия.</small></string> + <string name="pref_encryption_settings">Настройки шифрования</string> + <string name="pref_force_encryption">Обязательное сквозное шифрование</string> + <string name="pref_force_encryption_summary">Всегда отправлять сообщения зашифрованными (за исключением конференций)</string> + <string name="pref_dont_save_encrypted">Не сохранять зашифрованные сообщения</string> + <string name="pref_dont_save_encrypted_summary">Внимание: Это может привести к потере сообщений</string> + <string name="pref_expert_options">Расширенные настройки</string> + <string name="pref_expert_options_summary">Пожалуйста, будьте осторожны с данными настройками</string> + <string name="pref_use_larger_font">Увеличить размер шрифта</string> + <string name="pref_use_larger_font_summary">Установите больший размер шрифта по всей программе</string> + <string name="pref_use_send_button_to_indicate_status">Использовать кнопку-индикатор</string> + <string name="pref_use_send_button_to_indicate_status_summary">Раскрасить кнопку отправить, указывая текущий статус собеседника</string> + +</resources> diff --git a/src/main/res/values-sv/arrays.xml b/src/main/res/values-sv/arrays.xml new file mode 100644 index 00000000..890e2915 --- /dev/null +++ b/src/main/res/values-sv/arrays.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Mobile</item> + <item>Phone</item> + <item>Tablet</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>aldrig</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml new file mode 100644 index 00000000..a3ed9112 --- /dev/null +++ b/src/main/res/values-sv/strings.xml @@ -0,0 +1,260 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Inställningar</string> + <string name="action_add">Ny konversation</string> + <string name="action_accounts">Kontoinställningar</string> + <string name="action_end_conversation">Avsluta denna konversation</string> + <string name="action_contact_details">Kontaktdetaljer</string> + <string name="action_muc_details">Konferensdetaljer</string> + <string name="action_secure">Skyddad konversation</string> + <string name="action_add_account">Lägg till konto</string> + <string name="action_edit_contact">Ändra namn</string> + <string name="action_add_phone_book">Lägg till i telefonbok</string> + <string name="action_delete_contact">Ta bort kontakt</string> + <string name="title_activity_manage_accounts">Hantera konton</string> + <string name="title_activity_settings">Inställningar</string> + <string name="title_activity_conference_details">Konferensdetaljer</string> + <string name="title_activity_contact_details">Kontaktdetaljer</string> + <string name="title_activity_sharewith">Dela med konversation</string> + <string name="title_activity_start_conversation">Starta konversation</string> + <string name="title_activity_choose_contact">Välj kontakt</string> + <string name="just_now">just nu</string> + <string name="minute_ago">1 min sedan</string> + <string name="minutes_ago">%d min sedan</string> + <string name="unread_conversations">olästa konversationer</string> + <string name="sending">skickar…</string> + <string name="encrypted_message">Avkrypterar meddelande. Vänta…</string> + <string name="nick_in_use">Nick används redan</string> + <string name="admin">Admin</string> + <string name="owner">Ägare</string> + <string name="moderator">Moderator</string> + <string name="participant">Deltagare</string> + <string name="visitor">Besökare</string> + <string name="remove_contact_text">Vill du ta bort %s från din kontaktlista? Konversationer associerade med denna kontakt kommer inte tas bort.</string> + <string name="remove_bookmark_text">Vill du ta bort %s som bokmärke? Konversationer associerade med detta bokmärke kommer inte tas bort.</string> + <string name="register_account">Registrera nytt konto på servern</string> + <string name="share_with">Dela med</string> + <string name="start_conversation">Starta konversation</string> + <string name="invite_contact">Bjud in kontakt</string> + <string name="contacts">Kontakter</string> + <string name="cancel">Avbryt</string> + <string name="add">Lägg till</string> + <string name="edit">Ändra</string> + <string name="delete">Ta bort</string> + <string name="save">Spara</string> + <string name="ok">Ok</string> + <string name="crash_report_title">Conversations har kraschat</string> + <string name="crash_report_message">Genom att skicka in stack traces hjälper du utvecklarna av Conversations\n<b>Varning:</b> Detta använder ditt XMPP konto för att skicka informationen till utvecklarna.</string> + <string name="send_now">Skicka nu</string> + <string name="send_never">Fråga aldrig igen</string> + <string name="problem_connecting_to_account">Kan inte ansluta till konto</string> + <string name="problem_connecting_to_accounts">Kan inte ansluta till flera konton</string> + <string name="touch_to_fix">Tryck här för att hantera dina konton</string> + <string name="attach_file">Bifoga fil</string> + <string name="not_in_roster">Kontakten är inte i din kontaktlista. Vill du lägga till den?</string> + <string name="add_contact">Lägg till kontakt</string> + <string name="send_failed">sändning misslyckades</string> + <string name="send_rejected">avvisad</string> + <string name="receiving_image">Tar emot bildfil. Vänta…</string> + <string name="preparing_image">Förbereder bild för sändning</string> + <string name="action_clear_history">Rensa historik</string> + <string name="clear_conversation_history">Rensa konversationshistorik</string> + <string name="clear_histor_msg">Vill du ta bort alla meddelanden i denna konversation?\n\n<b>Varning:</b> Detta kommer inte påverka meddelanden lagrade på andra enheter eller servrar.</string> + <string name="delete_messages">Ta bort meddelanden</string> + <string name="also_end_conversation">Avsluta denna konversation efter</string> + <string name="send_plain_text_message">Skicka meddelande i klartext</string> + <string name="send_otr_message">Skicka OTR-krypterat meddelande</string> + <string name="send_pgp_message">Skicka OpenPGP-krypterat meddelande</string> + <string name="your_nick_has_been_changed">Ditt nick har ändrats</string> + <string name="download_image">Ladda ner bild</string> + <string name="image_offered_for_download"><i>Bildfil erbjuds för nedladdning</i></string> + <string name="send_unencrypted">Skicka okrypterat</string> + <string name="decryption_failed">Avkryptering gick fel. Du kanske inte har rätt privat nyckel.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations använder en tredjeparts applikation som heter <b>OpenKeychain</b> för att kryptera och avkryptera meddelanden och hantera dina publika nycklar.\n\nOpenKeychain är licensierat under GPLv3 och tillgängligt på F-Droid och Google Play.\n\n<small>(Starta om Conversations efter.)</small></string> + <string name="restart">Starta om</string> + <string name="install">Installera</string> + <string name="offering">erbjuder…</string> + <string name="waiting">väntar…</string> + <string name="no_pgp_key">Ingen OpenPGP-nyckel funnen</string> + <string name="contact_has_no_pgp_key">Conversations kan inte avkryptera ditt meddelande eftersom din kontakt inte annonserar sin publika nyckel.\n\n<small>Be din kontakt att sätta upp OpenPGP.</small></string> + <string name="no_pgp_keys">Inga OpenPGP-nycklar funna</string> + <string name="contacts_have_no_pgp_keys">Conversations kan inte avkryptera ditt meddelande eftersom din kontakt inte annonserar sin publika nyckel.\n\n<small>Be din kontakt att sätta upp OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Krypterat meddelande mottaget. Tryck för att se och avkryptera.</i></string> + <string name="encrypted_image_received"><i>Krypterad bild mottagen. Tryck för att se och avkryptera.</i></string> + <string name="image_file"><i>Bild mottagen. Tryck för att se</i></string> + <string name="pref_xmpp_resource">XMPP resurs</string> + <string name="pref_xmpp_resource_summary">Namnet som klienten identifierar sig med</string> + <string name="pref_accept_files">Acceptera filer</string> + <string name="pref_accept_files_summary">Acceptera automatistk filer som är mindre än…</string> + <string name="pref_notification_settings">Notifieringsinställningar</string> + <string name="pref_notifications">Notifieringar</string> + <string name="pref_notifications_summary">Notifiera när meddelande tagits emot</string> + <string name="pref_vibrate">Vibrera</string> + <string name="pref_vibrate_summary">Vibrera när meddelande tagits emot</string> + <string name="pref_sound">Ljud</string> + <string name="pref_sound_summary">Spela ljud med notifiering</string> + <string name="pref_conference_notifications">Konferensnotifieringar</string> + <string name="pref_conference_notifications_summary">Notifiera alltid när nytt konferensmeddelande tagits emot istället för endast vid highlight</string> + <string name="pref_notification_grace_period">Notifieringsfrist</string> + <string name="pref_advanced_options">Avancerade inställningar</string> + <string name="pref_never_send_crash">Skicka aldrig krasch-rapporter</string> + <string name="pref_never_send_crash_summary">Genom att skicka in stack traces hjälper du utvecklarna av Conversations</string> + <string name="pref_confirm_messages">Bekräfta meddelanden</string> + <string name="pref_confirm_messages_summary">Låter dina kontakter veta när du har tagit emot och läst ett meddelande</string> + <string name="openpgp_error">OpenKeychain rapporterade ett fel</string> + <string name="error_decrypting_file">I/O-fel vid avkryptering av fil</string> + <string name="accept">Acceptera</string> + <string name="error">Ett fel har inträffat</string> + <string name="pref_grant_presence_updates">Tillåt tillänglighetsuppdateringar</string> + <string name="pref_grant_presence_updates_summary">Tillåt i förväg och be om tillgänglighetsuppdateringar för kontakter du skapat</string> + <string name="subscriptions">Abonnemang</string> + <string name="your_account">Ditt konto</string> + <string name="keys">Nycklar</string> + <string name="send_presence_updates">Skicka tillgänglighetsuppdatering</string> + <string name="receive_presence_updates">Ta emot tillgänglighetsuppdateringar</string> + <string name="ask_for_presence_updates">Be om tillgänglighetsuppdateringar</string> + <string name="attach_choose_picture">Välj bild</string> + <string name="attach_take_picture">Ta ny bild</string> + <string name="preemptively_grant">Tillåt abonnemangsbegäran i förväg</string> + <string name="error_not_an_image_file">Filen du valt är inte en bild</string> + <string name="error_compressing_image">Fel vid konvertering av bildfilen</string> + <string name="error_file_not_found">Filen hittas ej</string> + <string name="error_io_exception">Generellt I/O-fel. Du kanske fick slut på plats?</string> + <string name="error_security_exception_during_image_copy">Applikationen du använde för att välja bilden gav inte tillräckliga rättigheter för att läsa filen.\n\n<small>Använd en annan filhanterare för att välja bild</small></string> + <string name="account_status_unknown">Okänd</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">Ansluter\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Otillåten</string> + <string name="account_status_not_found">Server ej funnen</string> + <string name="account_status_no_internet">Ingen anslutning</string> + <string name="account_status_regis_fail">Registreringsfel</string> + <string name="account_status_regis_conflict">Användarnamn används redan</string> + <string name="account_status_regis_success">Registrering klar</string> + <string name="account_status_regis_not_sup">Servern stödjer inte registrering</string> + <string name="encryption_choice_none">Klartext</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Ändra konto</string> + <string name="mgmt_account_delete">Ta bort</string> + <string name="mgmt_account_enable">Aktivera</string> + <string name="mgmt_account_are_you_sure">Är du säker?</string> + <string name="mgmt_account_delete_confirm_text">Om du tar bort kontot kommer all konversationshistorik att försvinna</string> + <string name="attach_record_voice">Spela in röst</string> + <string name="account_settings_jabber_id">Jabber ID</string> + <string name="account_settings_password">Lösenord</string> + <string name="account_settings_example_jabber_id">användarnamn@exempel.se</string> + <string name="account_settings_confirm_password">Bekräfta lösenord</string> + <string name="password">Lösenord</string> + <string name="confirm_password">Bekräfta lösenord</string> + <string name="passwords_do_not_match">Lösenorden är inte lika</string> + <string name="invalid_jid">Detta är inte ett korrekt Jabber ID</string> + <string name="error_out_of_memory">Slut på minne. Bilden är för stor</string> + <string name="add_phone_book_text">Vill du lägga till %s i din telefons kontaktlista?</string> + <string name="contact_status_online">online</string> + <string name="contact_status_free_to_chat">tillgänglig</string> + <string name="contact_status_away">borta</string> + <string name="contact_status_extended_away">borta (förlängt)</string> + <string name="contact_status_do_not_disturb">stör ej</string> + <string name="contact_status_offline">offline</string> + <string name="muc_details_conference">Konferens</string> + <string name="muc_details_other_members">Andra medlemmar</string> + <string name="server_info_carbon_messages">Carbon Messages</string> + <string name="server_info_stream_management">Stream Management</string> + <string name="missing_public_keys">Annonsering om publik nyckel saknas</string> + <string name="last_seen_now">senast sedd just nu</string> + <string name="last_seen_min">senast sedd 1 minut sedan</string> + <string name="last_seen_mins">senast sedd %d minuter sedan</string> + <string name="last_seen_hour">senast sedd 1 timme sedan</string> + <string name="last_seen_hours">senast sedd %d timmar sedan</string> + <string name="last_seen_day">senast sedd 1 dag sedan</string> + <string name="last_seen_days">senast sedd %d dagar sedan</string> + <string name="never_seen">aldrig sedd</string> + <string name="install_openkeychain">Krypterat meddelande. Installera OpenKeychain för att avkryptera.</string> + <string name="unknown_otr_fingerprint">Okänt OTR-fingeravtryck</string> + <string name="openpgp_messages_found">OpenPGP-krypterat meddelande funnet</string> + <string name="reception_failed">Mottagning misslyckades</string> + <string name="your_fingerprint">Ditt fingeravtryck</string> + <string name="otr_fingerprint">OTR-fingeravtryck</string> + <string name="verify">Verifiera</string> + <string name="decrypt">Avkryptera</string> + <string name="conferences">Konferenser</string> + <string name="search">Sök</string> + <string name="create_contact">Skapa kontakt</string> + <string name="join_conference">Gå med i konferens</string> + <string name="delete_contact">Ta bort kontakt</string> + <string name="view_contact_details">Se kontaktdetaljer</string> + <string name="create">Skapa</string> + <string name="contact_already_exists">Kontakten finns redan</string> + <string name="join">Gå med</string> + <string name="conference_address">Konferensadress</string> + <string name="conference_address_example">rum@conference.exempel.se</string> + <string name="save_as_bookmark">Spara som bokmärke</string> + <string name="delete_bookmark">Ta bort bokmärke</string> + <string name="bookmark_already_exists">Detta bokmärke finns redan</string> + <string name="you">Du</string> + <string name="action_edit_subject">Ändra konferensämne</string> + <string name="conference_not_found">Konferens hittades inte</string> + <string name="leave">Lämna</string> + <string name="contact_added_you">Kontakten lade till dig i sin kontaktlista</string> + <string name="add_back">Addera tillbaks</string> + <string name="contact_has_read_up_to_this_point">%s har läst fram hit</string> + <string name="next">Nästa</string> + <string name="server_info_unavailable">otillgänglig</string> + <string name="mgmt_account_publish_pgp">Publisera OpenPGP publik nyckel</string> + <string name="additional_information">Ytterligare information</string> + <string name="server_info_pep">XEP-0163: PEP (Avatarbilder)</string> + <string name="skip">skippa</string> + <string name="connect">Anslut</string> + <string name="account_already_exists">Detta konto finns redan</string> + <string name="private_message_to">till %s</string> + <string name="send_private_message_to">Skicka privat meddelande till %s</string> + <string name="touch_to_choose_picture">Tryck på avatarbild för att välja en bild från bildgalleriet</string> + <string name="mgmt_account_publish_avatar">Publisera avatarbild</string> + <string name="error_publish_avatar_server_reject">Servern kunde inte publisera</string> + <string name="error_publish_avatar_converting">Något gick fel vid konvertering av din bild</string> + <string name="error_publish_avatar_no_server_support">Din server stödjer inte publisering av avatarbilder</string> + <string name="publishing">Publiserar…</string> + <string name="error_saving_avatar">Kunde inte spara avatarbild till disk</string> + <string name="server_info_session_established">Nuvarande session upprättad</string> + <string name="or_long_press_for_default">(Eller tryck länge för att få tillbaks förvald)</string> + <string name="server_info_available">tillgänglig</string> + <string name="pref_general">Generellt</string> + <string name="publish">Publicera</string> + <string name="private_message">privat meddelande</string> + <string name="pref_ui_options">UI inställningar</string> + <string name="enable">Aktivera</string> + <string name="without_mutual_presence_updates"><b>Varning:</b> Skicka detta utan gemensamma tillgänglighetsuppdateringar kan ge oväntade problem.\n\n<small>Gå till kontaktdetaljer för att verifiera dina tillgänglighetsuppdateringar.</small></string> + <string name="disable_notifications">Inaktivera notifieringar</string> + <string name="request_presence_updates">Begär tillgänglighetsuppdateringar från din kontakt först.\n\n<small>Detta används för att se vilken klient/klienter din kontakt använder.</small></string> + <string name="conference_requires_password">Konferensen kräver lösenord</string> + <string name="pref_dont_save_encrypted">Spara in krypterade meddelanden</string> + <string name="pref_encryption_settings">Krypteringsinställningar</string> + <string name="pref_use_send_button_to_indicate_status_summary">Färglägg skickaknappen för att indikera kontaktens status</string> + <string name="missing_presence_updates">Saknar tillgänglighetsuppdateringar från kontakt</string> + <string name="pref_expert_options">Expertinställningar</string> + <string name="pref_force_encryption_summary">Sänd alltid krypterade meddelanden (utom för konferenser)</string> + <string name="pref_expert_options_summary">Var försiktig med dem</string> + <string name="disable_notifications_for_this_conversation">Inaktivera notifieringar för denna konversation</string> + <string name="pref_use_send_button_to_indicate_status">Skickaknappen indikerar status</string> + <string name="enter_password">Fyll i lösenord</string> + <string name="notifications_disabled">Notifieringar är inaktiverade</string> + <string name="pref_force_encryption">Tvinga kryptering</string> + <string name="sure_delete_fingerprint">Är du säker på att du vill ta bort detta fingeravtryck?</string> + <string name="ignore">Ignorera</string> + <string name="pref_use_larger_font_summary">Använd större teckenstorlek för hela applikationen</string> + <string name="pref_use_larger_font">Öka teckenstorlek</string> + <string name="pref_dont_save_encrypted_summary">Varning: Detta kan leda till att meddelanden förloras</string> + <string name="delete_fingerprint">Ta bort fingeravtryck</string> + <string name="request_now">Begär nu</string> + <string name="title_activity_conversations">Conversations</string> + <string name="publish_avatar_explanation">Notera: Alla som kan se dina tillgänglighetsuppdateringar kommer se denna bild.</string> + <string name="choose_presence">Välj tillgänglighet till kontakt</string> + <string name="pref_notification_grace_period_summary">Inaktivera notifieringar en kort stund efter att en carbon copy tagits emot</string> + <string name="account_status_disabled">Tillfälligt inaktiverad</string> + <string name="mgmt_account_disable">Inaktivera tillfälligt</string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-zh-rCN/arrays.xml b/src/main/res/values-zh-rCN/arrays.xml new file mode 100644 index 00000000..1a243079 --- /dev/null +++ b/src/main/res/values-zh-rCN/arrays.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>手机</item> + <item>电话</item> + <item>平板电脑</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>永不</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + <string-array name="mute_options_descriptions"> + <item>30 分钟</item> + <item>1 小时</item> + <item>2 小时</item> + <item>8 小时</item> + <item>直至另行取消</item> + </string-array> + + <integer-array name="mute_options_durations"> + <item>1800</item> + <item>3600</item> + <item>7200</item> + <item>28800</item> + <item>-1</item> + </integer-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 00000000..a7898425 --- /dev/null +++ b/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,260 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">设置</string> + <string name="action_add">新会话</string> + <string name="action_accounts">管理账户</string> + <string name="action_end_conversation">结束会话</string> + <string name="action_contact_details">联系人详情</string> + <string name="action_muc_details">讨论组详情</string> + <string name="action_secure">安全对话</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="title_activity_manage_accounts">管理账户</string> + <string name="title_activity_settings">设置</string> + <string name="title_activity_conference_details">讨论组详情</string> + <string name="title_activity_contact_details">联系人详情</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">分享会话</string> + <string name="title_activity_start_conversation">开始会话</string> + <string name="title_activity_choose_contact">选择联系人</string> + <string name="just_now">刚刚</string> + <string name="minute_ago">1 分钟前</string> + <string name="minutes_ago">%d分钟前</string> + <string name="unread_conversations">未读会话</string> + <string name="sending">正在发送…</string> + <string name="encrypted_message">解密信息中. 请稍候…</string> + <string name="nick_in_use">该名称已存在</string> + <string name="admin">管理员</string> + <string name="owner">所有者</string> + <string name="moderator">版主</string> + <string name="participant">参与者</string> + <string name="visitor">访客</string> + <string name="remove_contact_text">将 %s从列表中移除? 与该联系人的会话消息不会清除.</string> + <string name="remove_bookmark_text">从书签中移除 %s?相关会话消息不会被清除 .</string> + <string name="register_account">在服务器上注册新账户</string> + <string name="share_with">分享</string> + <string name="start_conversation">开始会话</string> + <string name="invite_contact">邀请联系人</string> + <string name="contacts">联系人</string> + <string name="cancel">取消</string> + <string name="add">添加</string> + <string name="edit">编辑</string> + <string name="delete">删除</string> + <string name="save">保存</string> + <string name="ok">完成</string> + <string name="crash_report_title">Conversations停止运行</string> + <string name="crash_report_message">发送堆栈跟踪到正在开发Conversations的人员\n<b>警告:</b> 该操作将用您的 XMPP账户发送堆栈跟踪到开发人员.</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="not_in_roster">该联系人不在您的列表.需要加为联系人吗 ?</string> + <string name="add_contact">添加联系人</string> + <string name="send_failed">传递失败</string> + <string name="send_rejected">拒绝</string> + <string name="receiving_image">接收图片文件中. 请稍候…</string> + <string name="preparing_image">准备传输图像</string> + <string name="action_clear_history">清除历史记录</string> + <string name="clear_conversation_history">清除会话记录</string> + <string name="clear_histor_msg">删除该会话中所有信息?\n\n<b>注:</b> 该操作不会影响其他设备或服务器保存的信息.</string> + <string name="delete_messages">删除消息</string> + <string name="also_end_conversation">之后结束该会话</string> + <string name="choose_presence">添加在线用户至联系人</string> + <string name="send_plain_text_message">发送纯文本信息</string> + <string name="send_otr_message">发送 OTR 加密信息</string> + <string name="send_pgp_message">发送 OpenPGP 加密信息</string> + <string name="your_nick_has_been_changed">用户名修改成功</string> + <string name="download_image">下载图片</string> + <string name="image_offered_for_download"><i>供下载的图像文件</i></string> + <string name="send_unencrypted">不加密发送</string> + <string name="decryption_failed">解密失败,可能是私钥不正确.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">会话运用了第三方app,名为 <b>OpenKeychain</b> 用来加密、解码信息以及管理您的公钥.\n\nOpenKeychain 遵循 GPLv3 并且在 F-Droid和Google Play上可操作.\n\n<small>(之后请重启conversations.)</small></string> + <string name="restart">重启</string> + <string name="install">安装</string> + <string name="offering">输入…</string> + <string name="waiting">等待…</string> + <string name="no_pgp_key">未发现OpenPGP 密码</string> + <string name="contact_has_no_pgp_key">会话加密信息失败,因为联系人未告知他/她的公钥.\n\n<small>请通知联系人设置 OpenPGP.</small></string> + <string name="no_pgp_keys">未找到 OpenPGP 密码</string> + <string name="contacts_have_no_pgp_keys">因您的联系人未公布公钥,Conversations未能成功加密您的信息.\n\n<small>请通知联系人设置OpenPGP.</small></string> + <string name="encrypted_message_received"><i>加密信息已接收.点击进行解密和查看.</i></string> + <string name="encrypted_image_received"><i>加密图像已接收.点击进行解密和查看.</i></string> + <string name="image_file"><i>图片已成功接收,点击查看</i></string> + <string name="pref_general">常规</string> + <string name="pref_xmpp_resource">XMPP 资源</string> + <string name="pref_xmpp_resource_summary">客户端标识名称</string> + <string name="pref_accept_files">接收文件</string> + <string name="pref_accept_files_summary">自动接收小于 … 的文件</string> + <string name="pref_notification_settings">通知设置</string> + <string name="pref_notifications">通知</string> + <string name="pref_notifications_summary">收到新消息时通知</string> + <string name="pref_vibrate">震动</string> + <string name="pref_vibrate_summary">收到新消息时震动</string> + <string name="pref_sound">声音</string> + <string name="pref_sound_summary">收到新消息时播放铃声</string> + <string name="pref_conference_notifications">讨论组通知</string> + <string name="pref_conference_notifications_summary">当有新的消息时总是通知而不是亮屏时才通知</string> + <string name="pref_notification_grace_period">通知限期</string> + <string name="pref_notification_grace_period_summary">接收副本短时间内关闭通知</string> + <string name="pref_advanced_options">高级选项</string> + <string name="pref_never_send_crash">总不发送故障报告</string> + <string name="pref_never_send_crash_summary">发送堆栈跟踪帮助Conversations开发人员</string> + <string name="pref_confirm_messages">确认消息</string> + <string name="pref_confirm_messages_summary">当你已收到消息并且已阅时通知好友</string> + <string name="pref_ui_options">UI选项</string> + <string name="openpgp_error">OpenKeychain 报告了一个错误</string> + <string name="error_decrypting_file">解码文件时出现I/O错误</string> + <string name="accept">接受</string> + <string name="error">产生了一个错误</string> + <string name="pref_grant_presence_updates">同意更新在线联系人</string> + <string name="pref_grant_presence_updates_summary">预先同意并请求您的联系人进行更新</string> + <string name="subscriptions">关注</string> + <string name="your_account">你的账号</string> + <string name="keys">Keys</string> + <string name="send_presence_updates">发送在线联系人更新列表</string> + <string name="receive_presence_updates">接收在线联系人更新列表</string> + <string name="ask_for_presence_updates">请求在线联系人更新列表</string> + <string name="attach_choose_picture">选择图片</string> + <string name="attach_take_picture">照相</string> + <string name="preemptively_grant">预先同意订阅请求</string> + <string name="error_not_an_image_file">您选择的文件不是图像文件</string> + <string name="error_compressing_image">转换图像出错</string> + <string name="error_file_not_found">未找到文件</string> + <string name="error_io_exception">常规的I/O错误.可能是存储空间不足的原因?</string> + <string name="error_security_exception_during_image_copy">您用来选择图片的app没有给予足够权限支持我们读取文件.\n\n<small>请使用另一文件管理器选择图片</small></string> + <string name="account_status_unknown">未知</string> + <string name="account_status_disabled">暂时不可用</string> + <string name="account_status_online">在线</string> + <string name="account_status_connecting">Connecting\u2026</string> + <string name="account_status_offline">离线</string> + <string name="account_status_unauthorized">未授权</string> + <string name="account_status_not_found">未找到服务器</string> + <string name="account_status_no_internet">未连接网络</string> + <string name="account_status_regis_fail">注册失败</string> + <string name="account_status_regis_conflict"> 用户名已存在</string> + <string name="account_status_regis_success">注册完成</string> + <string name="account_status_regis_not_sup">服务器不支持注册</string> + <string name="encryption_choice_none">纯文本内容</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">编辑账号</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> + <string name="mgmt_account_enable">启用账户</string> + <string name="mgmt_account_are_you_sure">确定?</string> + <string name="mgmt_account_delete_confirm_text">如果删除用户,所有会话信息将会丢失</string> + <string name="attach_record_voice">Record voice 录音</string> + <string name="account_settings_jabber_id">Jabber ID</string> + <string name="account_settings_password">密码</string> + <string name="account_settings_example_jabber_id">username@example.com</string> + <string name="account_settings_confirm_password">确认密码</string> + <string name="password">密码</string> + <string name="confirm_password">确认密码</string> + <string name="passwords_do_not_match">密码不一致</string> + <string name="invalid_jid">该Jabber ID 无效</string> + <string name="error_out_of_memory">空间不足,图片过大</string> + <string name="add_phone_book_text">您将添加 %s 至手机联系人列表?</string> + <string name="contact_status_online">在线</string> + <string name="contact_status_free_to_chat">免费对话</string> + <string name="contact_status_away">离开</string> + <string name="contact_status_extended_away">长时间离开</string> + <string name="contact_status_do_not_disturb">请勿打扰</string> + <string name="contact_status_offline">离线</string> + <string name="muc_details_conference">讨论组</string> + <string name="muc_details_other_members">其他成员</string> + <string name="server_info_carbon_messages">XEP-0280: 消息碳</string> + <string name="server_info_stream_management">XEP-0198: 流管理</string> + <string name="server_info_pep">XEP-0163: PEP (头像)</string> + <string name="server_info_available">有效</string> + <string name="server_info_unavailable">无效</string> + <string name="missing_public_keys">缺少公共秘钥公告</string> + <string name="last_seen_now">最近一次查看为刚刚</string> + <string name="last_seen_min"> 最近一次查看为一分钟前</string> + <string name="last_seen_mins">最近一次查看为 %d 分钟前</string> + <string name="last_seen_hour">最近一次查看为一小时前</string> + <string name="last_seen_hours">最近一次查看为 %d 小时前</string> + <string name="last_seen_day">最近一次查看为一天前</string> + <string name="last_seen_days">最近一次查看为 %d天前</string> + <string name="never_seen">未曾查看</string> + <string name="install_openkeychain">加密信息. 请安装OpenKeychain进行解码.</string> + <string name="unknown_otr_fingerprint">未知 OTR指纹</string> + <string name="openpgp_messages_found">OpenPGP 发现加密信息</string> + <string name="reception_failed">接收失败</string> + <string name="your_fingerprint">你的指纹</string> + <string name="otr_fingerprint">OTR 指纹</string> + <string name="verify">验证</string> + <string name="decrypt">解密</string> + <string name="conferences">讨论组</string> + <string name="search">查找</string> + <string name="create_contact">创建联系人</string> + <string name="join_conference">加入讨论组</string> + <string name="delete_contact">删除联系人</string> + <string name="view_contact_details">查看联系人详细信息</string> + <string name="create">创建</string> + <string name="contact_already_exists">联系人已存在</string> + <string name="join">加入</string> + <string name="conference_address">讨论组地址</string> + <string name="conference_address_example">room@conference.example.com</string> + <string name="save_as_bookmark">保存为书签</string> + <string name="delete_bookmark">删除书签</string> + <string name="bookmark_already_exists">该书签已存在</string> + <string name="you">你的</string> + <string name="action_edit_subject">编辑讨论组主题</string> + <string name="conference_not_found">讨论组未找到</string> + <string name="leave">离开</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="touch_to_choose_picture">点击头像可选择头像 </string> + <string name="publish_avatar_explanation">请注意: 所有关注您最新动态的人将看到该图像.</string> + <string name="publishing">发布…</string> + <string name="error_publish_avatar_server_reject">服务器拒绝了您的发布请求</string> + <string name="error_publish_avatar_converting">转换头像出错</string> + <string name="error_saving_avatar">不能将头像保存至disk</string> + <string name="or_long_press_for_default">(或长按按钮将返回默认头像)</string> + <string name="error_publish_avatar_no_server_support">您的服务器不支持发布头像</string> + <string name="private_message">密谈</string> + <string name="private_message_to">至 %s</string> + <string name="send_private_message_to">发送私密消息到%s</string> + <string name="connect">Connect</string> + <string name="account_already_exists">该账号已存在</string> + <string name="next">下一步</string> + <string name="server_info_session_established">当前会话已建立</string> + <string name="additional_information">其他信息</string> + <string name="skip">Skip略过</string> + <string name="disable_notifications">关闭通知</string> + <string name="disable_notifications_for_this_conversation">关闭该会话消息</string> + <string name="notifications_disabled">通知已关闭</string> + <string name="enable">打开通知</string> + <string name="conference_requires_password">讨论组设有密码</string> + <string name="enter_password">输入密码</string> + <string name="missing_presence_updates">缺少在线联系人更新</string> + <string name="request_presence_updates">请先发送更新在线联系人请求.\n\n<small>这将用来判断您的联系人所用的客户端类型人.</small></string> + <string name="request_now">现在发送请求</string> + <string name="delete_fingerprint">删除指纹</string> + <string name="sure_delete_fingerprint">是否确定删除该指纹?</string> + <string name="ignore">忽略</string> + <string name="without_mutual_presence_updates"><b>警告:</b>在没有相互更新在线联系人的情况下发送将会出现未知问题.\n\n<small>到联系人详情确认您订阅的在线联系人.</small></string> + <string name="pref_encryption_settings">加密设置</string> + <string name="pref_force_encryption">强制要求 end-to-end 加密</string> + <string name="pref_force_encryption_summary"> 总是发送加密信息(讨论组信息除外)</string> + <string name="pref_dont_save_encrypted">不保存加密信息</string> + <string name="pref_dont_save_encrypted_summary">警告:此操作将会导致信息丢失</string> + <string name="pref_expert_options">Expert 选项</string> + <string name="pref_expert_options_summary">请谨慎使用</string> + <string name="pref_use_larger_font"> 放大字体</string> + <string name="pref_use_larger_font_summary">整个app界面使用更大号的字体</string> + <string name="pref_use_send_button_to_indicate_status">发送按钮显示状态</string> + <string name="pref_use_send_button_to_indicate_status_summary">发送按钮采用其他颜色以示发送状态的区别</string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-zh-rTW/arrays.xml b/src/main/res/values-zh-rTW/arrays.xml new file mode 100644 index 00000000..b9c261ad --- /dev/null +++ b/src/main/res/values-zh-rTW/arrays.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>手機</item> + <item>電話</item> + <item>平板電腦</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>永不</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + <string-array name="mute_options_descriptions"> + <item>30 分鐘</item> + <item>1 小時</item> + <item>2 小時</item> + <item>8 小時</item> + <item>直至另行取消</item> + </string-array> + + <integer-array name="mute_options_durations"> + <item>1800</item> + <item>3600</item> + <item>7200</item> + <item>28800</item> + <item>-1</item> + </integer-array> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 00000000..2c3ea225 --- /dev/null +++ b/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,263 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">設定</string> + <string name="action_add">新對話</string> + <string name="action_accounts">管理帳戶</string> + <string name="action_end_conversation">結束對話</string> + <string name="action_contact_details">聯絡人詳情</string> + <string name="action_secure">安全對話</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="title_activity_manage_accounts">管理帳戶</string> + <string name="title_activity_conference_details">群組詳情</string> + <string name="title_activity_contact_details">聯絡人詳情</string> + <string name="title_activity_conversations">對話</string> + <string name="title_activity_sharewith">分享對話</string> + <string name="title_activity_start_conversation">開始對話</string> + <string name="title_activity_choose_contact">選擇聯絡人</string> + <string name="just_now">剛剛</string> + <string name="minute_ago">1 分鐘前</string> + <string name="minutes_ago">%d 分鐘前</string> + <string name="unread_conversations">未讀對話</string> + <string name="sending">正在發送…</string> + <string name="encrypted_message">正在解密訊息中,請稍候…</string> + <string name="nick_in_use">該用戶名稱已被使用</string> + <string name="admin">管理員</string> + <string name="owner">擁有人</string> + <string name="moderator">版主</string> + <string name="participant">成員</string> + <string name="visitor">訪客</string> + <string name="remove_contact_text">你確定要將 %s 從聯絡人清單中移除嗎?與該聯絡人的對話將不會被清除。</string> + <string name="remove_bookmark_text">你確定要將 %s 從書籤清單中移除嗎?與該聯絡人的對話將不會被清除。</string> + <string name="register_account">在伺服器上註冊新帳戶</string> + <string name="share_with">分享</string> + <string name="start_conversation">開始對話</string> + <string name="invite_contact">邀請聯絡人</string> + <string name="contacts">聯絡人</string> + <string name="cancel">取消</string> + <string name="add">新增</string> + <string name="edit">編輯</string> + <string name="delete">刪除</string> + <string name="save">儲存</string> + <string name="ok">好的</string> + <string name="crash_report_title">Conversations 停止運行</string> + <string name="crash_report_message">發送「堆疊追蹤」給 Conversations 的開發人員能幫助改進本程式。\n<b>警告:</b> 你的 XMPP 帳戶將被用作發送有關訊息之用。</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="not_in_roster">該聯絡人不在你的聯絡人清單上,需要加為聯絡人嗎?</string> + <string name="add_contact">新增聯絡人</string> + <string name="send_failed">傳遞失敗</string> + <string name="send_rejected">拒絕</string> + <string name="receiving_image">接收圖片文件中,請稍候…</string> + <string name="preparing_image">準備傳輸圖片</string> + <string name="action_clear_history">清除歷史記錄</string> + <string name="clear_conversation_history">清除對話記錄</string> + <string name="clear_histor_msg">你確定要刪除該對話中所有訊息嗎?\n\n<b>警告:</b> 這將不會影響其他設備或伺服器儲存的訊息。</string> + <string name="delete_messages">刪除訊息</string> + <string name="also_end_conversation">之後結束這對話</string> + <string name="choose_presence">選擇狀態訊息</string> + <string name="send_plain_text_message">發送純文字訊息</string> + <string name="send_otr_message">發送 OTR 加密訊息</string> + <string name="send_pgp_message">發送 OpenPGP 加密訊息</string> + <string name="your_nick_has_been_changed">用戶名稱修改成功</string> + <string name="download_image">下載圖片</string> + <string name="image_offered_for_download"><i>可供下載的圖像文件</i></string> + <string name="send_unencrypted">不加密發送</string> + <string name="decryption_failed">解密失敗,可能是私鑰不正確。</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations 使用一個名為 <b>OpenKeychain</b> 的第三方程式來加密、解碼訊息以及管理您的公鑰。\n\nOpenKeychain 以 GPLv3 釋出,並可在 F-Droid 和 Google Play 上下載。\n\n<small>(之後請重新啟動 Conversations。)</small></string> + <string name="restart">重新啟動</string> + <string name="install">安裝</string> + <string name="offering">提供中…</string> + <string name="waiting">等待中…</string> + <string name="no_pgp_key">找不到 OpenPGP 鑰匙</string> + <string name="contact_has_no_pgp_key">Conversations 不能將你的訊息加密,因為聯絡人沒有公佈他/她的公鑰。\n\n<small>請通知聯絡人設定 OpenPGP。</small></string> + <string name="no_pgp_keys">找不到多條 OpenPGP 鑰匙</string> + <string name="contacts_have_no_pgp_keys">Conversations 不能將你的訊息加密,因為多位聯絡人沒有公佈他/她的公鑰。\n\n<small>請通知聯絡人設定 OpenPGP。</small></string> + <string name="encrypted_message_received"><i>已收到加密訊息,點擊進行解密和查看。</i></string> + <string name="encrypted_image_received"><i>已收到加密圖片,點擊進行解密和查看。</i></string> + <string name="image_file"><i>已收到圖片,點擊查看</i></string> + <string name="pref_general">一般</string> + <string name="pref_xmpp_resource">XMPP 資源</string> + <string name="pref_xmpp_resource_summary">客戶端標示名稱</string> + <string name="pref_accept_files">接收文件</string> + <string name="pref_accept_files_summary">自動接收小於 … 的文件</string> + <string name="pref_notification_settings">通知設定</string> + <string name="pref_notifications">通知</string> + <string name="pref_notifications_summary">收到新訊息時通知</string> + <string name="pref_vibrate">震動</string> + <string name="pref_vibrate_summary">收到新訊息時震動</string> + <string name="pref_sound">聲音</string> + <string name="pref_sound_summary">收到新訊息時播放鈴聲</string> + <string name="pref_conference_notifications">群組通知</string> + <string name="pref_conference_notifications_summary">當有新訊息時總是通知,而不是被標記時才通知</string> + <string name="pref_notification_grace_period">通知限期</string> + <string name="pref_notification_grace_period_summary">收到副本後,關閉通知一小段時間</string> + <string name="pref_advanced_options">進階選項</string> + <string name="pref_never_send_crash">總是不發送故障報告</string> + <string name="pref_never_send_crash_summary">發送「堆疊追蹤」給 Conversations 的開發人員能幫助改進本程式</string> + <string name="pref_confirm_messages">確認訊息</string> + <string name="pref_confirm_messages_summary">讓你的聯絡人知道你已收到及閱讀訊息</string> + <string name="pref_ui_options">介面選項</string> + <string name="openpgp_error">OpenKeychain 回報了一個錯誤</string> + <string name="error_decrypting_file">解密文件時出現 I/O 錯誤</string> + <string name="accept">接受</string> + <string name="error">發生了一個錯誤</string> + <string name="pref_grant_presence_updates">同意更新狀態訊息</string> + <string name="pref_grant_presence_updates_summary">預先更新狀態訊息並關注聯絡人的狀態訊息</string> + <string name="subscriptions">關注</string> + <string name="your_account">你的帳戶</string> + <string name="keys">鑰匙</string> + <string name="send_presence_updates">發送狀態訊息</string> + <string name="receive_presence_updates">接收狀態訊息</string> + <string name="ask_for_presence_updates">關注狀態訊息</string> + <string name="attach_choose_picture">選擇圖片</string> + <string name="attach_take_picture">拍照</string> + <string name="preemptively_grant">預先同意關注請求</string> + <string name="error_not_an_image_file">您選擇的文件不是圖片</string> + <string name="error_compressing_image">轉換圖片時發生錯誤</string> + <string name="error_file_not_found">找不到文件</string> + <string name="error_io_exception">一般的 I/O 錯誤。是存儲空間不足嗎?</string> + <string name="error_security_exception_during_image_copy">你用來選擇圖片的 app 沒有給予足夠權限我們去讀取文件。\n\n<small>請使用另一文件管理器來選擇圖片</small></string> + <string name="account_status_unknown">未知</string> + <string name="account_status_disabled">暫時停用</string> + <string name="account_status_online">在線</string> + <string name="account_status_connecting">連接中\u2026</string> + <string name="account_status_offline">離線</string> + <string name="account_status_unauthorized">未授權</string> + <string name="account_status_not_found">未找到伺服器</string> + <string name="account_status_no_internet">未連接網絡</string> + <string name="account_status_regis_fail">註冊失敗</string> + <string name="account_status_regis_conflict">該用戶名稱已被使用</string> + <string name="account_status_regis_success">註冊完成</string> + <string name="account_status_regis_not_sup">伺服器不支持註冊</string> + <string name="encryption_choice_none">純文字內容</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">編輯帳戶</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> + <string name="mgmt_account_enable">啟用帳戶</string> + <string name="mgmt_account_are_you_sure">你確定嗎?</string> + <string name="mgmt_account_delete_confirm_text">如果刪除帳戶,則所有對話訊息將會被刪除</string> + <string name="attach_record_voice">錄音</string> + <string name="account_settings_jabber_id">Jabber ID</string> + <string name="account_settings_password">密碼</string> + <string name="account_settings_example_jabber_id">username@example.com</string> + <string name="account_settings_confirm_password">確認密碼</string> + <string name="password">密碼</string> + <string name="confirm_password">確認密碼</string> + <string name="passwords_do_not_match">密碼不一致</string> + <string name="invalid_jid">該 Jabber ID 無效</string> + <string name="error_out_of_memory">空間不足,圖片過大</string> + <string name="add_phone_book_text">你確定要新增 %s 為聯絡人嗎?</string> + <string name="contact_status_online">線上</string> + <string name="contact_status_free_to_chat">目前有空</string> + <string name="contact_status_away">離開</string> + <string name="contact_status_extended_away">長時間離開</string> + <string name="contact_status_do_not_disturb">請勿打擾</string> + <string name="contact_status_offline">離線</string> + <string name="muc_details_conference">群組</string> + <string name="muc_details_other_members">其他成員</string> + <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> + <string name="server_info_stream_management">XEP-0198: Stream Management</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="server_info_available">支援</string> + <string name="server_info_unavailable">不支援</string> + <string name="missing_public_keys">沒有公佈公鑰訊息。</string> + <string name="last_seen_now">剛剛曾在線上</string> + <string name="last_seen_min">一分鐘前曾在線上</string> + <string name="last_seen_mins">%d 分鐘前曾在線上</string> + <string name="last_seen_hour">一小時前曾在線上</string> + <string name="last_seen_hours">%d 小時前曾在線上</string> + <string name="last_seen_day">一天前曾在線上</string> + <string name="last_seen_days">%d 天前曾在線上</string> + <string name="never_seen">未曾上線</string> + <string name="install_openkeychain">加密的訊息。請安裝 OpenKeychain 以解密。</string> + <string name="unknown_otr_fingerprint">未知的 OTR 指紋</string> + <string name="openpgp_messages_found">發現以 OpenPGP 加密的訊息</string> + <string name="reception_failed">接收失敗</string> + <string name="your_fingerprint">你的指紋</string> + <string name="otr_fingerprint">OTR 指紋</string> + <string name="verify">驗證</string> + <string name="decrypt">解密</string> + <string name="conferences">群組</string> + <string name="search">查找</string> + <string name="create_contact">新增聯絡人</string> + <string name="join_conference">加入群組</string> + <string name="delete_contact">刪除聯絡人</string> + <string name="view_contact_details">查看聯絡人詳細訊息</string> + <string name="create">新增</string> + <string name="contact_already_exists">聯絡人已存在</string> + <string name="join">加入</string> + <string name="conference_address">群組地址</string> + <string name="conference_address_example">room@conference.example.com</string> + <string name="save_as_bookmark">儲存為書籤</string> + <string name="delete_bookmark">刪除書籤</string> + <string name="bookmark_already_exists">該書籤已存在</string> + <string name="you">你</string> + <string name="action_edit_subject">編輯群組主題</string> + <string name="conference_not_found">群組未找到</string> + <string name="leave">離開</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="touch_to_choose_picture">點擊頭像可選擇頭像</string> + <string name="publish_avatar_explanation">請注意: 所有關注你狀態訊息的人將看到該圖像。</string> + <string name="publishing">發佈中…</string> + <string name="error_publish_avatar_server_reject">伺服器拒絕了你的發佈請求</string> + <string name="error_publish_avatar_converting">發佈頭像時發生錯誤</string> + <string name="error_saving_avatar">將頭像儲存至硬碟時發生錯誤</string> + <string name="or_long_press_for_default">(或長按以回復預設頭像)</string> + <string name="error_publish_avatar_no_server_support">你的伺服器不支持發佈頭像</string> + <string name="private_message">私密聊天</string> + <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="next">下一步</string> + <string name="server_info_session_established">已建立連接</string> + <string name="additional_information">其他訊息</string> + <string name="skip">略過</string> + <string name="disable_notifications">關閉通知</string> + <string name="disable_notifications_for_this_conversation">關閉該對話消息</string> + <string name="notifications_disabled">通知已關閉</string> + <string name="enable">打開通知</string> + <string name="conference_requires_password">群組設有密碼</string> + <string name="enter_password">輸入密碼</string> + <string name="missing_presence_updates">缺少聯絡人狀態訊息</string> + <string name="request_presence_updates">請先發送關注狀態訊息請求。\n\n<small>這將用來判斷您的聯絡人所用的客戶端類型。</small></string> + <string name="request_now">現在發送請求</string> + <string name="delete_fingerprint">刪除指紋</string> + <string name="sure_delete_fingerprint">你確定刪除該指紋嗎?</string> + <string name="ignore">忽略</string> + <string name="without_mutual_presence_updates"><b>警告:</b> 在沒有互相關注狀態訊息的情況下發送或會引起不能預計的問題。\n\n<small>請檢視聯絡人詳情頁面以確認你們的關注狀態。</small></string> + <string name="pref_encryption_settings">加密設定</string> + <string name="pref_force_encryption">強制要求端到端加密</string> + <string name="pref_force_encryption_summary">總是發送加密訊息 (群組訊息除外)</string> + <string name="pref_dont_save_encrypted">不儲存加密訊息</string> + <string name="pref_dont_save_encrypted_summary">警告: 此操作或會導致訊息丟失</string> + <string name="pref_expert_options">專家選項</string> + <string name="pref_expert_options_summary">請小心設定</string> + <string name="pref_use_larger_font">增加字體大小</string> + <string name="pref_use_larger_font_summary">讓整個 app 界面使用更大號的字體</string> + <string name="pref_use_send_button_to_indicate_status">用「發送」按鈕顯示狀態訊息</string> + <string name="pref_use_indicate_received">要求讀取收據</string> + <string name="pref_use_indicate_received_summary">已被讀取的訊息會以綠色勾號表示。請注意,這個功能未必每次有效。</string> + <string name="pref_use_send_button_to_indicate_status_summary">將「發送」按鈕設成不同顏色,以表示不同的狀態訊息。</string> + <string name="pref_expert_options_other">其他</string> + <string name="pref_conference_name">群組名稱</string> + <string name="pref_conference_name_summary">使用群組的名稱而不是 JID 來識別之。 </string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml new file mode 100644 index 00000000..4acc9e62 --- /dev/null +++ b/src/main/res/values/arrays.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="resources"> + <item>Mobile</item> + <item>Phone</item> + <item>Tablet</item> + <item>Conversations</item> + <item>Android</item> + </string-array> + <string-array name="filesizes"> + <item>never</item> + <item>256 KB</item> + <item>512 KB</item> + <item>1 MB</item> + </string-array> + <string-array name="filesizes_values"> + <item>0</item> + <item>262144</item> + <item>524288</item> + <item>1048576</item> + </string-array> + <string-array name="mute_options_descriptions"> + <item>30 minutes</item> + <item>1 hour</item> + <item>2 hours</item> + <item>8 hours</item> + <item>until further notice</item> + </string-array> + + <integer-array name="mute_options_durations"> + <item>1800</item> + <item>3600</item> + <item>7200</item> + <item>28800</item> + <item>-1</item> + </integer-array> + +</resources> diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml new file mode 100644 index 00000000..2354a5e8 --- /dev/null +++ b/src/main/res/values/attrs.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <attr name="TextSizeInfo" format="dimension" /> + <attr name="TextSizeBody" format="dimension" /> + <attr name="TextSizeHeadline" format="dimension" /> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml new file mode 100644 index 00000000..908b8b89 --- /dev/null +++ b/src/main/res/values/colors.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <color name="primary" type="color">#ff259b24</color> + <color name="primarydark" type="color">#ff0a7e07</color> + <color name="primarytext" type="color">#de000000</color> + <color name="secondarytext" type="color">#8a000000</color> + <color name="ondarktext" type="color">#fffafafa</color> + <color name="primarybackground" type="color">#fffafafa</color> + <color name="secondarybackground" type="color">#ffeeeeee</color> + <color name="darkbackground" type="color">#ff323232</color> + <color name="divider">#1f000000</color> + <color name="red">#ffe51c23</color> + <color name="orange">#ffff9800</color> + <color name="green">#ff259b24</color> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml new file mode 100644 index 00000000..e941ed6d --- /dev/null +++ b/src/main/res/values/strings.xml @@ -0,0 +1,286 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string name="app_name">Conversations</string> + <string name="action_settings">Settings</string> + <string name="action_add">New conversation</string> + <string name="action_accounts">Manage accounts</string> + <string name="action_end_conversation">End this conversation</string> + <string name="action_contact_details">Contact details</string> + <string name="action_muc_details">Conference details</string> + <string name="action_secure">Secure conversation</string> + <string name="action_add_account">Add account</string> + <string name="action_edit_contact">Edit name</string> + <string name="action_add_phone_book">Add to phone book</string> + <string name="action_delete_contact">Delete from roster</string> + <string name="title_activity_manage_accounts">Manage Accounts</string> + <string name="title_activity_settings">Settings</string> + <string name="title_activity_conference_details">Conference Details</string> + <string name="title_activity_contact_details">Contact Details</string> + <string name="title_activity_conversations">Conversations</string> + <string name="title_activity_sharewith">Share with Conversation</string> + <string name="title_activity_start_conversation">Start Conversation</string> + <string name="title_activity_choose_contact">Choose contact</string> + <string name="just_now">just now</string> + <string name="minute_ago">1 min ago</string> + <string name="minutes_ago">%d mins ago</string> + <string name="unread_conversations">unread Conversations</string> + <string name="sending">sending…</string> + <string name="encrypted_message">Decrypting message. Please wait…</string> + <string name="nick_in_use">Nickname is already in use</string> + <string name="admin">Admin</string> + <string name="owner">Owner</string> + <string name="moderator">Moderator</string> + <string name="participant">Participant</string> + <string name="visitor">Visitor</string> + <string name="remove_contact_text">Would you like to remove %s from your roster? The conversation associated with this contact will not be removed.</string> + <string name="remove_bookmark_text">Would you like to remove %s as a bookmark? The conversation associated with this bookmark will not be removed.</string> + <string name="register_account">Register new account on server</string> + <string name="share_with">Share with</string> + <string name="start_conversation">Start Conversation</string> + <string name="invite_contact">Invite Contact</string> + <string name="contacts">Contacts</string> + <string name="cancel">Cancel</string> + <string name="add">Add</string> + <string name="edit">Edit</string> + <string name="delete">Delete</string> + <string name="save">Save</string> + <string name="ok">OK</string> + <string name="crash_report_title">Conversations has crashed</string> + <string name="crash_report_message">By sending in stack traces you are helping the ongoing development of Conversations\n<b>Warning:</b> This will use your XMPP account to send the stack trace to the developer.</string> + <string name="send_now">Send now</string> + <string name="send_never">Never ask again</string> + <string name="problem_connecting_to_account">Unable to connect to account</string> + <string name="problem_connecting_to_accounts">Unable to connect to multiple accounts</string> + <string name="touch_to_fix">Touch here to manage your accounts</string> + <string name="attach_file">Attach file</string> + <string name="not_in_roster">The contact is not in your roster. Would you like to add it?</string> + <string name="add_contact">Add contact</string> + <string name="send_failed">delivery failed</string> + <string name="send_rejected">rejected</string> + <string name="receiving_image">Receiving image file. Please wait…</string> + <string name="preparing_image">Preparing image for transmission</string> + <string name="action_clear_history">Clear history</string> + <string name="clear_conversation_history">Clear Conversation History</string> + <string name="clear_histor_msg">Do you want to delete all messages within this Conversation?\n\n<b>Warning:</b> This will not influence messages stored on other devices or servers.</string> + <string name="delete_messages">Delete messages</string> + <string name="also_end_conversation">End this conversations afterwards</string> + <string name="choose_presence">Choose presence to contact</string> + <string name="send_plain_text_message">Send plain text message</string> + <string name="send_otr_message">Send OTR encrypted message</string> + <string name="send_pgp_message">Send OpenPGP encrypted message</string> + <string name="your_nick_has_been_changed">Your nickname has been changed</string> + <string name="download_image">Download Image</string> + <string name="image_offered_for_download"><i>Image file offered for download</i></string> + <string name="send_unencrypted">Send unencrypted</string> + <string name="decryption_failed">Decryption failed. Maybe you don’t have the proper private key.</string> + <string name="openkeychain_required">OpenKeychain</string> + <string name="openkeychain_required_long">Conversations utilizes a third party app called <b>OpenKeychain</b> to encrypt and decrypt messages and to manage your public keys.\n\nOpenKeychain is licensed under GPLv3 and available on F-Droid and Google Play.\n\n<small>(Please restart Conversations afterwards.)</small></string> + <string name="restart">Restart</string> + <string name="install">Install</string> + <string name="offering">offering…</string> + <string name="waiting">waiting…</string> + <string name="no_pgp_key">No OpenPGP Key found</string> + <string name="contact_has_no_pgp_key">Conversations is unable to encrypt your messages because your contact is not announcing his or hers public key.\n\n<small>Please ask your contact to setup OpenPGP.</small></string> + <string name="no_pgp_keys">No OpenPGP Keys found</string> + <string name="contacts_have_no_pgp_keys">Conversations is unable to encrypt your messages because your contacts are not announcing their public key.\n\n<small>Please ask your contacts to setup OpenPGP.</small></string> + <string name="encrypted_message_received"><i>Encrypted message received. Touch to view and decrypt.</i></string> + <string name="encrypted_image_received"><i>Encrypted image received. Touch to view and decrypt.</i></string> + <string name="image_file"><i>Image received. Touch to view</i></string> + <string name="pref_general">General</string> + <string name="pref_xmpp_resource">XMPP resource</string> + <string name="pref_xmpp_resource_summary">The name this client identifies itself with</string> + <string name="pref_accept_files">Accept files</string> + <string name="pref_accept_files_summary">Automatically accept files smaller than…</string> + <string name="pref_notification_settings">Notification Settings</string> + <string name="pref_notifications">Notifications</string> + <string name="pref_notifications_summary">Notify when a new message arrives</string> + <string name="pref_vibrate">Vibrate</string> + <string name="pref_vibrate_summary">Also vibrate when a new message arrives</string> + <string name="pref_sound">Sound</string> + <string name="pref_sound_summary">Play ringtone with notification</string> + <string name="pref_conference_notifications">Conference notifications</string> + <string name="pref_conference_notifications_summary">Always notify when a new conference message arrives instead of only when highlighted</string> + <string name="pref_notification_grace_period">Notification grace period</string> + <string name="pref_notification_grace_period_summary">Disable notifications for a short time after a carbon copy was received</string> + <string name="pref_advanced_options">Advanced Options</string> + <string name="pref_never_send_crash">Never send crash reports</string> + <string name="pref_never_send_crash_summary">By sending in stack traces you are helping the ongoing development of Conversations</string> + <string name="pref_confirm_messages">Confirm Messages</string> + <string name="pref_confirm_messages_summary">Let your contact know when you have received and read a message</string> + <string name="pref_ui_options">UI Options</string> + <string name="openpgp_error">OpenKeychain reported an error</string> + <string name="error_decrypting_file">I/O Error decrypting file</string> + <string name="accept">Accept</string> + <string name="error">An error has occurred</string> + <string name="pref_grant_presence_updates">Grant presence updates</string> + <string name="pref_grant_presence_updates_summary">Preemptively grant and ask for presence subscription for contacts you created</string> + <string name="subscriptions">Subscriptions</string> + <string name="your_account">Your account</string> + <string name="keys">Keys</string> + <string name="send_presence_updates">Send presence updates</string> + <string name="receive_presence_updates">Receive presence updates</string> + <string name="ask_for_presence_updates">Ask for presence updates</string> + <string name="attach_choose_picture">Choose picture</string> + <string name="attach_take_picture">Take picture</string> + <string name="preemptively_grant">Preemptively grant subscription request</string> + <string name="error_not_an_image_file">The file you selected is not an image</string> + <string name="error_compressing_image">Error while converting the image file</string> + <string name="error_file_not_found">File not found</string> + <string name="error_io_exception">General I/O error. Maybe you ran out of storage space?</string> + <string name="error_security_exception_during_image_copy">The app you used to select this image did not provide us with enough permissions to read the file.\n\n<small>Use a different file manager to choose an image</small></string> + <string name="account_status_unknown">Unknown</string> + <string name="account_status_disabled">Temporarily disabled</string> + <string name="account_status_online">Online</string> + <string name="account_status_connecting">Connecting\u2026</string> + <string name="account_status_offline">Offline</string> + <string name="account_status_unauthorized">Unauthorized</string> + <string name="account_status_not_found">Server not found</string> + <string name="account_status_no_internet">No connectivity</string> + <string name="account_status_regis_fail">Registration failed</string> + <string name="account_status_regis_conflict">Username already in use</string> + <string name="account_status_regis_success">Registration completed</string> + <string name="account_status_regis_not_sup">Server does not support registration</string> + <string name="encryption_choice_none">Plain text</string> + <string name="encryption_choice_otr">OTR</string> + <string name="encryption_choice_pgp">OpenPGP</string> + <string name="mgmt_account_edit">Edit account</string> + <string name="mgmt_account_delete">Delete account</string> + <string name="mgmt_account_disable">Temporarily disable</string> + <string name="mgmt_account_publish_avatar">Publish avatar</string> + <string name="mgmt_account_publish_pgp">Publish OpenPGP public key</string> + <string name="mgmt_account_enable">Enable account</string> + <string name="mgmt_account_are_you_sure">Are you sure?</string> + <string name="mgmt_account_delete_confirm_text">If you delete your account your entire conversation history will be lost</string> + <string name="attach_record_voice">Record voice</string> + <string name="account_settings_jabber_id">Jabber ID</string> + <string name="account_settings_password">Password</string> + <string name="account_settings_example_jabber_id">username@example.com</string> + <string name="account_settings_confirm_password">Confirm password</string> + <string name="password">Password</string> + <string name="confirm_password">Confirm password</string> + <string name="passwords_do_not_match">Passwords do not match</string> + <string name="invalid_jid">This is not a valid Jabber ID</string> + <string name="error_out_of_memory">Out of memory. Image is too large</string> + <string name="add_phone_book_text">Do you want to add %s to your phones contact list?</string> + <string name="contact_status_online">online</string> + <string name="contact_status_free_to_chat">free to chat</string> + <string name="contact_status_away">away</string> + <string name="contact_status_extended_away">extended away</string> + <string name="contact_status_do_not_disturb">do not disturb</string> + <string name="contact_status_offline">offline</string> + <string name="muc_details_conference">Conference</string> + <string name="muc_details_other_members">Other Members</string> + <string name="server_info_carbon_messages">XEP-0280: Message Carbons</string> + <string name="server_info_stream_management">XEP-0198: Stream Management</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="server_info_available">available</string> + <string name="server_info_unavailable">unavailable</string> + <string name="missing_public_keys">Missing public key announcements</string> + <string name="last_seen_now">last seen just now</string> + <string name="last_seen_min">last seen 1 minute ago</string> + <string name="last_seen_mins">last seen %d minutes ago</string> + <string name="last_seen_hour">last seen 1 hour ago</string> + <string name="last_seen_hours">last seen %d hours ago</string> + <string name="last_seen_day">last seen 1 day ago</string> + <string name="last_seen_days">last seen %d days ago</string> + <string name="never_seen">never seen</string> + <string name="install_openkeychain">Encrypted message. Please install OpenKeychain to decrypt.</string> + <string name="unknown_otr_fingerprint">Unknown OTR fingerprint</string> + <string name="openpgp_messages_found">OpenPGP encrypted messages found</string> + <string name="reception_failed">Reception failed</string> + <string name="your_fingerprint">Your fingerprint</string> + <string name="otr_fingerprint">OTR fingerprint</string> + <string name="verify">Verify</string> + <string name="decrypt">Decrypt</string> + <string name="conferences">Conferences</string> + <string name="search">Search</string> + <string name="create_contact">Create Contact</string> + <string name="join_conference">Join Conference</string> + <string name="delete_contact">Delete Contact</string> + <string name="view_contact_details">View contact details</string> + <string name="create">Create</string> + <string name="contact_already_exists">The contact already exists</string> + <string name="join">Join</string> + <string name="conference_address">Conference address</string> + <string name="conference_address_example">room@conference.example.com</string> + <string name="save_as_bookmark">Save as bookmark</string> + <string name="delete_bookmark">Delete bookmark</string> + <string name="bookmark_already_exists">This bookmark already exists</string> + <string name="you">You</string> + <string name="action_edit_subject">Edit conference subject</string> + <string name="conference_not_found">Conference not found</string> + <string name="leave">Leave</string> + <string name="contact_added_you">Contact added you to contact list</string> + <string name="add_back">Add back</string> + <string name="contact_has_read_up_to_this_point">%s has read up to this point</string> + <string name="publish">Publish</string> + <string name="touch_to_choose_picture">Touch avatar to select picture from gallery</string> + <string name="publish_avatar_explanation">Please note: Everyone subscribed to your presence updates will be allowed to see this picture.</string> + <string name="publishing">Publishing…</string> + <string name="error_publish_avatar_server_reject">The server rejected your publication</string> + <string name="error_publish_avatar_converting">Something went wrong while converting your picture</string> + <string name="error_saving_avatar">Could not save avatar to disk</string> + <string name="or_long_press_for_default">(Or long press to bring back default)</string> + <string name="error_publish_avatar_no_server_support">Your server does not support the publication of avatars</string> + <string name="private_message">whispered</string> + <string name="private_message_to">to %s</string> + <string name="send_private_message_to">Send private message to %s</string> + <string name="connect">Connect</string> + <string name="account_already_exists">This account does already exist</string> + <string name="next">Next</string> + <string name="server_info_session_established">Current session established</string> + <string name="additional_information">Additional Information</string> + <string name="skip">Skip</string> + <string name="disable_notifications">Disable notifications</string> + <string name="disable_notifications_for_this_conversation">Disable notifications for this conversation</string> + <string name="notifications_disabled">Notifications are disabled</string> + <string name="enable">Enable</string> + <string name="conference_requires_password">Conference requires password</string> + <string name="enter_password">Enter password</string> + <string name="missing_presence_updates">Missing presence updates from contact</string> + <string name="request_presence_updates">Please request presence updates from your contact first.\n\n<small>This will be used to determine what client(s) your contact is using.</small></string> + <string name="request_now">Request now</string> + <string name="delete_fingerprint">Delete Fingerprint</string> + <string name="sure_delete_fingerprint">Are you sure you would like to delete this fingerprint?</string> + <string name="ignore">Ignore</string> + <string name="without_mutual_presence_updates"><b>Warning:</b> Sending this without mutual presence updates could cause unexpected problems.\n\n<small>Go to contact details to verify your presence subscriptions.</small></string> + <string name="pref_encryption_settings">Encryption settings</string> + <string name="pref_force_encryption">Force end-to-end encryption</string> + <string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string> + <string name="pref_dont_save_encrypted">Don’t save encrypted messages</string> + <string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string> + <string name="pref_enable_legacy_ssl">Enable legacy SSL</string> + <string name="pref_enable_legacy_ssl_summary">Enables SSLv3 support for legacy servers. Warning: SSLv3 is considered insecure.</string> + <string name="pref_expert_options">Expert options</string> + <string name="pref_expert_options_summary">Please be careful with these</string> + <string name="pref_use_larger_font">Increase font size</string> + <string name="pref_use_larger_font_summary">Use larger font sizes across the entire app</string> + <string name="pref_use_send_button_to_indicate_status">Send button indicates status</string> + <string name="pref_use_indicate_received">Request message receipts</string> + <string name="pref_use_indicate_received_summary">Received messages will be marked with a green tick if supported</string> + <string name="pref_use_send_button_to_indicate_status_summary">Colorize send button to indicate contact status</string> + <string name="pref_expert_options_other">Other</string> + <string name="pref_conference_name">Conference name</string> + <string name="pref_conference_name_summary">Use room’s subject instead of JID to identify conferences</string> + <string name="toast_message_otr_fingerprint">OTR fingerprint copied to clipboard!</string> + <string name="conference_banned">You are banned from this conference</string> + <string name="conference_members_only">This conference is members only</string> + <string name="conference_kicked">You have been kicked from this conference</string> + <string name="using_account">using account %s</string> + <string name="checking_image">Checking image on HTTP host</string> + <string name="image_file_deleted">The image file has been deleted</string> + <string name="not_connected_try_again">You are not connected. Try again later</string> + <string name="check_image_filesize">Check image file size</string> + <string name="message_options">Message options</string> + <string name="copy_text">Copy text</string> + <string name="share_image">Share image</string> + <string name="copy_original_url">Copy original URL</string> + <string name="send_again">Send again</string> + <string name="image_url">Image URL</string> + <string name="message_text">Message text</string> + <string name="url_copied_to_clipboard">URL copied to clipboard</string> + <string name="message_copied_to_clipboard">Message copied to clipboard</string> + <string name="image_transmission_failed">Image transmission failed</string> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml new file mode 100644 index 00000000..64bde770 --- /dev/null +++ b/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <style name="Divider"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">1.5dp</item> + <item name="android:background">@color/divider</item> + </style> + +</resources>
\ No newline at end of file diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml new file mode 100644 index 00000000..fa7973d2 --- /dev/null +++ b/src/main/res/values/themes.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="ConversationsTheme" parent="@android:style/Theme.Holo.Light.DarkActionBar"> + <item name="android:actionBarStyle">@style/ConversationsActionBar</item> + <item name="android:actionBarWidgetTheme">@style/ConversationsActionBarWidget</item> + <item name="android:actionBarTabStyle">@style/ConversationsActionBarTabs</item> + <item name="TextSizeInfo">12sp</item> + <item name="TextSizeBody">14sp</item> + <item name="TextSizeHeadline">20sp</item> + </style> + + <style name="ConversationsTheme.LargerText" parent="ConversationsTheme"> + <item name="TextSizeInfo">14sp</item> + <item name="TextSizeBody">16sp</item> + <item name="TextSizeHeadline">22sp</item> + </style> + + <style name="ConversationsActionBar" parent="@android:style/Widget.Holo.Light.ActionBar.Solid.Inverse"> + <item name="android:background">@color/primary</item> + <item name="android:backgroundStacked">@color/primarydark</item> + <item name="android:displayOptions">showHome|homeAsUp|showTitle</item> + <item name="android:icon">@android:color/transparent</item> + </style> + + <style name="ConversationsActionBarWidget" parent="android:Theme.Holo.Light"> + <item name="android:popupMenuStyle">@android:style/Widget.Holo.Light.PopupMenu</item> + <item name="android:dropDownListViewStyle">@android:style/Widget.Holo.Light.ListView.DropDown</item> + </style> + + <style name="ConversationsActionBarTabs" parent="@android:style/Widget.Holo.ActionBar.TabView"> + <item name="android:background">@drawable/actionbar_tab_indicator</item> + </style> + +</resources>
\ No newline at end of file diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml new file mode 100644 index 00000000..06ab7560 --- /dev/null +++ b/src/main/res/xml/preferences.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > + + <PreferenceCategory android:title="@string/pref_general" > + <CheckBoxPreference + android:defaultValue="true" + android:key="grant_new_contacts" + android:summary="@string/pref_grant_presence_updates_summary" + android:title="@string/pref_grant_presence_updates" /> + + <ListPreference + android:defaultValue="Mobile" + android:entries="@array/resources" + android:entryValues="@array/resources" + android:key="resource" + android:summary="@string/pref_xmpp_resource_summary" + android:title="@string/pref_xmpp_resource" /> + <ListPreference + android:defaultValue="524288" + android:entries="@array/filesizes" + android:entryValues="@array/filesizes_values" + android:key="auto_accept_file_size" + android:summary="@string/pref_accept_files_summary" + android:title="@string/pref_accept_files" /> + + <CheckBoxPreference + android:defaultValue="true" + android:key="confirm_messages" + android:summary="@string/pref_confirm_messages_summary" + android:title="@string/pref_confirm_messages" /> + </PreferenceCategory> + <PreferenceCategory android:title="@string/pref_notification_settings" > + <CheckBoxPreference + android:defaultValue="true" + android:key="show_notification" + android:summary="@string/pref_notifications_summary" + android:title="@string/pref_notifications" /> + <CheckBoxPreference + android:defaultValue="true" + android:dependency="show_notification" + android:key="vibrate_on_notification" + android:summary="@string/pref_vibrate_summary" + android:title="@string/pref_vibrate" /> + + <RingtonePreference + android:defaultValue="content://settings/system/notification_sound" + android:dependency="show_notification" + android:key="notification_ringtone" + android:ringtoneType="notification" + android:summary="@string/pref_sound_summary" + android:title="@string/pref_sound" /> + + <CheckBoxPreference + android:defaultValue="true" + android:dependency="show_notification" + android:key="always_notify_in_conference" + android:summary="@string/pref_conference_notifications_summary" + android:title="@string/pref_conference_notifications" /> + </PreferenceCategory> + <PreferenceCategory android:title="@string/pref_ui_options" > + <CheckBoxPreference + android:defaultValue="true" + android:key="use_subject" + android:summary="@string/pref_conference_name_summary" + android:title="@string/pref_conference_name" /> + <CheckBoxPreference + android:defaultValue="false" + android:key="use_larger_font" + android:summary="@string/pref_use_larger_font_summary" + android:title="@string/pref_use_larger_font" /> + <CheckBoxPreference + android:defaultValue="false" + android:key="send_button_status" + android:summary="@string/pref_use_send_button_to_indicate_status_summary" + android:title="@string/pref_use_send_button_to_indicate_status" /> + </PreferenceCategory> + <PreferenceCategory android:title="@string/pref_advanced_options" > + <PreferenceScreen + android:summary="@string/pref_expert_options_summary" + android:title="@string/pref_expert_options" > + <PreferenceCategory android:title="@string/pref_encryption_settings" > + <CheckBoxPreference + android:defaultValue="false" + android:key="force_encryption" + android:summary="@string/pref_force_encryption_summary" + android:title="@string/pref_force_encryption" /> + <CheckBoxPreference + android:defaultValue="false" + android:key="dont_save_encrypted" + android:summary="@string/pref_dont_save_encrypted_summary" + android:title="@string/pref_dont_save_encrypted" /> + <CheckBoxPreference + android:defaultValue="false" + android:key="enable_legacy_ssl" + android:summary="@string/pref_enable_legacy_ssl_summary" + android:title="@string/pref_enable_legacy_ssl" /> + </PreferenceCategory> + <PreferenceCategory android:title="@string/pref_expert_options_other" > + <CheckBoxPreference + android:defaultValue="false" + android:key="indicate_received" + android:summary="@string/pref_use_indicate_received_summary" + android:title="@string/pref_use_indicate_received" /> + </PreferenceCategory> + </PreferenceScreen> + + <CheckBoxPreference + android:defaultValue="false" + android:key="never_send" + android:summary="@string/pref_never_send_crash_summary" + android:title="@string/pref_never_send_crash" /> + </PreferenceCategory> + +</PreferenceScreen> |