Use Gradle build system

This commit is contained in:
Sam Whited 2014-10-22 12:38:44 -04:00
commit 64bdd7e731
323 changed files with 25921 additions and 0 deletions

41
.gitignore vendored Normal file
View file

@ -0,0 +1,41 @@
.classpath
*.swp
.settings
# https://github.com/github/gitignore/blob/master/Gradle.gitignore
.gradle/
gradle/
build/
# Ignore Gradle GUI config
gradle-app.setting
# https://github.com/github/gitignore/blob/master/Android.gitignore
# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
*.iml
.idea
import-summary.txt
*.jar

9
.gitmodules vendored Normal file
View file

@ -0,0 +1,9 @@
[submodule "minidns"]
path = minidns
url = https://github.com/rtreffer/minidns.git
[submodule "openpgpapilib"]
path = openpgpapilib
url = https://github.com/open-keychain/openpgp-api-lib.git
[submodule "memorizingTrustManager"]
path = memorizingTrustManager
url = https://github.com/iNPUTmice/MemorizingTrustManager.git

16
build.gradle Normal file
View file

@ -0,0 +1,16 @@
// Top-level build file where you can add configuration options common to all
// sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.2'
}
}
allprojects {
repositories {
jcenter()
}
}

View file

@ -0,0 +1,28 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 19
buildToolsVersion "20.0.0"
defaultConfig {
applicationId "eu.siacs.conversations"
minSdkVersion 14
targetSdkVersion 19
}
buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
}
dependencies {
compile project(':minidns')
compile project(':openpgpapilib')
compile project(':memorizingTrustManager')
compile files('libs/android-support-v13.jar')
compile files('libs/bcprov-jdk15on-150.jar')
compile files('libs/otr4j-0.10.jar')
}

View file

@ -0,0 +1,124 @@
<?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"
android:versionCode="32"
android:versionName="0.8-alpha" >
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />
<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>

View file

@ -0,0 +1,25 @@
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 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() {
}
}

View file

@ -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 doesnt 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
}
}

View file

@ -0,0 +1,385 @@
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 org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
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.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.util.Log;
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) {
Log.d(Config.LOGTAG, "decrypting message " + message.getUuid());
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);
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:
OpenPgpError error = result
.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
Log.d(Config.LOGTAG,
"openpgp error: " + error.getMessage());
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:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(
outputFile.getAbsolutePath(), options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
message.setBody(Long.toString(outputFile.getSize())
+ ',' + imageWidth + ',' + imageHeight);
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
PgpEngine.this.mXmppConnectionService
.updateMessage(message);
;
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) {
Log.d(Config.LOGTAG, "file not found: " + e.getMessage());
} catch (IOException e) {
Log.d(Config.LOGTAG, "io exception during file encrypt");
}
}
}
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:
Log.d(Config.LOGTAG,
"openpgp error: "
+ ((OpenPgpError) result
.getParcelableExtra(OpenPgpApi.RESULT_ERROR))
.getMessage());
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);
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -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;
}
}
}
}

View file

@ -0,0 +1,7 @@
package eu.siacs.conversations.entities;
public interface ListItem extends Comparable<ListItem> {
public String getDisplayName();
public String getJid();
}

View file

@ -0,0 +1,478 @@
package eu.siacs.conversations.entities;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import android.content.ContentValues;
import android.content.Context;
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_SEND_REJECTED = 4;
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 String getReadableBody(Context context) {
if (encryption == ENCRYPTION_PGP) {
return context.getText(R.string.encrypted_message_received)
.toString();
} else if (encryption == ENCRYPTION_DECRYPTION_FAILED) {
return context.getText(R.string.decryption_failed).toString();
} else if (type == TYPE_IMAGE) {
return context.getText(R.string.image_file).toString();
} else {
return body.trim();
}
}
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)))));
}
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 bodyContainsDownloadable() {
Contact contact = this.getContact();
if (status <= STATUS_RECEIVED
&& (contact == null || !contact.trusted())) {
return false;
}
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 = pathParts[pathParts.length - 1];
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 = 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];
}
} 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.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 class ImageParams {
public long size = 0;
public int width = 0;
public int height = 0;
public String origin;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,255 @@
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 android.util.Log;
import eu.siacs.conversations.Config;
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;
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());
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);
}
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);
return;
} catch (IOException e) {
cancel();
return;
}
file.setExpectedSize(size);
if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
new Thread(new FileDownloader(interactive)).start();
} else {
changeStatus(STATUS_OFFER);
}
}
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;
}
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}
}

View file

@ -0,0 +1,517 @@
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 (packet.getType() != MessagePacket.TYPE_ERROR) {
if (message.getEncryption() == Message.ENCRYPTION_NONE
|| mXmppConnectionService.saveEncryptedMessages()) {
mXmppConnectionService.databaseBackend.createMessage(message);
}
}
if (message.bodyContainsDownloadable()) {
this.mXmppConnectionService.getHttpConnectionManager()
.createNewConnection(message);
}
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());
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,335 @@
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);
}
}

View file

@ -0,0 +1,480 @@
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.media.ExifInterface;
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.Conversation;
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.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 {
ExifInterface exif;
try {
exif = new ExifInterface(image.toString());
if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
.equalsIgnoreCase("6")) {
return 90;
} else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
.equalsIgnoreCase("8")) {
return 270;
} else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
.equalsIgnoreCase("3")) {
return 180;
} else {
return 0;
}
} catch (IOException e) {
return -1;
}
}
}
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 void removeFiles(Conversation conversation) {
String prefix = mXmppConnectionService.getFilesDir().getAbsolutePath();
String path = prefix + "/" + conversation.getAccount().getJid() + "/"
+ conversation.getContactJid();
File file = new File(path);
try {
this.deleteFile(file);
} catch (IOException e) {
Log.d(Config.LOGTAG,
"error deleting file: " + file.getAbsolutePath());
}
}
private void deleteFile(File f) throws IOException {
if (f.isDirectory()) {
for (File c : f.listFiles())
deleteFile(c);
}
f.delete();
}
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();
}
}

View file

@ -0,0 +1,5 @@
package eu.siacs.conversations.persistance;
public interface OnPhoneContactsMerged {
public void phoneContactsMerged();
}

View file

@ -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;
}
}
}

View file

@ -0,0 +1,298 @@
package eu.siacs.conversations.services;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
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;
import android.util.Log;
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;
}
Log.d(Config.LOGTAG, "no cache hit for " + KEY);
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;
}
Log.d(Config.LOGTAG, "no cache hit for " + KEY);
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;
}
Log.d(Config.LOGTAG, "no cache hit for " + KEY);
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;
}
Log.d(Config.LOGTAG, "no cache hit for " + KEY);
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)];
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,237 @@
package eu.siacs.conversations.services;
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.net.Uri;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.Html;
import android.util.DisplayMetrics;
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.ConversationActivity;
public class NotificationService {
private XmppConnectionService mXmppConnectionService;
private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>();
public int NOTIFICATION_ID = 0x2342;
private Conversation mOpenConversation;
private boolean mIsInForeground;
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());
}
}
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 {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
mXmppConnectionService);
mBuilder.setSmallIcon(R.drawable.ic_notification);
if (notifications.size() == 1) {
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());
StringBuilder text = new StringBuilder();
for (int i = 0; i < messages.size(); ++i) {
text.append(messages.get(i).getReadableBody(
mXmppConnectionService));
if (i != messages.size() - 1) {
text.append("\n");
}
}
mBuilder.setStyle(new NotificationCompat.BigTextStyle()
.bigText(text.toString()));
mBuilder.setContentText(messages.get(0).getReadableBody(
mXmppConnectionService));
if (notify) {
mBuilder.setTicker(messages.get(messages.size() - 1)
.getReadableBody(mXmppConnectionService));
}
mBuilder.setContentIntent(createContentIntent(conversation
.getUuid()));
} else {
notificationManager.cancel(NOTIFICATION_ID);
return;
}
} else {
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> "
+ messages.get(0).getReadableBody(
mXmppConnectionService)));
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()));
}
}
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.setDeleteIntent(createDeleteIntent());
mBuilder.setLights(0xffffffff, 2000, 4000);
Notification notification = mBuilder.build();
notificationManager.notify(NOTIFICATION_ID, notification);
}
}
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));
}
}

View file

@ -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 = (MenuItem) 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();
}
}

View file

@ -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 = (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) {
}
}
}
}
}

View file

@ -0,0 +1,436 @@
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 = (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 = (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();
}
}

View file

@ -0,0 +1,947 @@
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);
}
}
@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 = (MenuItem) menu.findItem(R.id.action_security);
MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive);
MenuItem menuMucDetails = (MenuItem) menu
.findItem(R.id.action_muc_details);
MenuItem menuContactDetails = (MenuItem) menu
.findItem(R.id.action_contact_details);
MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file);
MenuItem menuClearHistory = (MenuItem) menu
.findItem(R.id.action_clear_history);
MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add);
MenuItem menuInviteContact = (MenuItem) menu
.findItem(R.id.action_invite);
MenuItem menuMute = (MenuItem) 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.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION
.equals(intent.getType())))) {
String convToView = (String) intent.getExtras().get(
CONVERSATION);
updateConversationList();
for (int i = 0; i < conversationList.size(); ++i) {
if (conversationList.get(i).getUuid().equals(convToView)) {
setSelectedConversation(conversationList.get(i));
break;
}
}
paneShouldBeOpen = false;
String text = intent.getExtras().getString(TEXT, null);
swapConversationFragment().setText(text);
}
} else {
handledViewIntent = false;
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 (mOpenConverstaion != null) {
selectConversationByUuid(mOpenConverstaion);
paneShouldBeOpen = mPanelOpen;
if (paneShouldBeOpen) {
showConversationsOverview();
}
swapConversationFragment();
mOpenConverstaion = null;
} else if (getIntent() != null
&& VIEW_CONVERSATION.equals(getIntent().getType())) {
String uuid = (String) getIntent().getExtras().get(CONVERSATION);
String text = getIntent().getExtras().getString(TEXT, null);
selectConversationByUuid(uuid);
paneShouldBeOpen = false;
swapConversationFragment().setText(text);
setIntent(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 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();
}
});
}
}
}

View file

@ -0,0 +1,781 @@
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.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
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.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 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);
return view;
}
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))) {
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(conversation.getLatestEncryption());
}
} 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(int latestEncryption) {
Set<String> knownFingerprints = conversation.getContact()
.getOtrFingerprints();
if ((latestEncryption == Message.ENCRYPTION_OTR)
&& (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("");
}
}

View file

@ -0,0 +1,423 @@
package eu.siacs.conversations.ui;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipboardManager;
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 (OtrFingerprintToClipBoard(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);
}
}
private boolean OtrFingerprintToClipBoard(String fingerprint) {
ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
String label = getResources().getString(R.string.otr_fingerprint);
if (mClipBoardManager != null) {
ClipData mClipData = ClipData.newPlainText(label, fingerprint);
mClipBoardManager.setPrimaryClip(mClipData);
return true;
}
return false;
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,217 @@
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);
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);
}
}
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}
}
}
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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 = (MenuItem) menu
.findItem(R.id.action_create_contact);
MenuItem menuCreateConference = (MenuItem) menu
.findItem(R.id.action_join_conference);
mMenuSearchView = (MenuItem) 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;
}
}
}

View file

@ -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);
}

View file

@ -0,0 +1,637 @@
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.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 boolean handledViewIntent = 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 = (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 = (String) 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 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();
}
}
}

View file

@ -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 = (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;
}
}

View file

@ -0,0 +1,135 @@
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 = (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 (d != null) {
mLastMessage.setVisibility(View.VISIBLE);
imagePreview.setVisibility(View.GONE);
if (conversation.isRead()) {
mLastMessage.setTypeface(null, Typeface.ITALIC);
} else {
mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC);
}
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 {
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;
}
}

View file

@ -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;
}
}

View file

@ -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 = (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;
}
}

View file

@ -0,0 +1,560 @@
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;
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";
}
}
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;
case Message.STATUS_SEND_REJECTED:
info = getContext().getString(R.string.send_rejected);
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);
}
});
}
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(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
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");
getContext().startActivity(
Intent.createChooser(shareIntent,
getContext().getText(R.string.share_with)));
return true;
}
});
}
@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 = (View) activity.getLayoutInflater().inflate(
R.layout.message_null, parent, false);
break;
case SENT:
view = (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 = (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 = (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 ((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);
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,185 @@
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("name")) {
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 namePort = 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) {
namePort.putString("error", "nosrv");
return namePort;
}
// we now have a list of servers to try :-)
// classic name/port pair
String resultName = result.get(0).getName();
namePort.putString("name", resultName);
namePort.putInt("port", result.get(0).getPort());
if (ips4.containsKey(resultName)) {
// we have an ip!
ArrayList<String> ip = ips4.get(resultName);
Collections.shuffle(ip, rnd);
namePort.putString("ipv4", ip.get(0));
}
if (ips6.containsKey(resultName)) {
ArrayList<String> ip = ips6.get(resultName);
Collections.shuffle(ip, rnd);
namePort.putString("ipv6", ip.get(0));
}
// add all other records
int i = 0;
for (SRV srv : result) {
namePort.putString("name" + i, srv.getName());
namePort.putInt("port" + i, srv.getPort());
i++;
}
} catch (SocketTimeoutException e) {
namePort.putString("error", "timeout");
} catch (Exception e) {
namePort.putString("error", "unhandled");
}
return namePort;
}
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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -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");
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,12 @@
package eu.siacs.conversations.utils;
public class XmlHelper {
public static String encodeEntities(String content) {
content = content.replace("&", "&amp;");
content = content.replace("<", "&lt;");
content = content.replace(">", "&gt;");
content = content.replace("\"", "&quot;");
content = content.replace("'", "&apos;");
return content;
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,7 @@
package eu.siacs.conversations.xmpp;
import eu.siacs.conversations.entities.Account;
public interface OnBindListener {
public void onBind(Account account);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -0,0 +1,7 @@
package eu.siacs.conversations.xmpp;
import eu.siacs.conversations.entities.Account;
public interface OnStatusChanged {
public void onStatusChanged(Account account);
}

View file

@ -0,0 +1,5 @@
package eu.siacs.conversations.xmpp;
public abstract interface PacketReceived {
}

File diff suppressed because it is too large Load diff

View file

@ -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() + ")";
}
}

View file

@ -0,0 +1,910 @@
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) {
if (initiator.equals(account.getFullJid())) {
mXmppConnectionService.markMessage(message,
Message.STATUS_SEND_FAILED);
}
mJingleStatus = JINGLE_STATUS_FAILED;
}
}
};
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() {
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 {
if (this.mJingleStatus == JINGLE_STATUS_INITIATED) {
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_REJECTED);
} 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;
}
}
}

View file

@ -0,0 +1,163 @@
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;
}
}
account.getXmppConnection().sendIqPacket(
packet.generateRespone(IqPacket.TYPE_ERROR), 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();
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -0,0 +1,6 @@
package eu.siacs.conversations.xmpp.jingle;
public interface OnPrimaryCandidateFound {
public void onPrimaryCandidateFound(boolean success,
JingleCandidate canditate);
}

View file

@ -0,0 +1,7 @@
package eu.siacs.conversations.xmpp.jingle;
public interface OnTransportConnected {
public void failed();
public void established();
}

View file

@ -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");
}
}

View file

@ -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());
}
}

View file

@ -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");
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

Some files were not shown because too many files have changed in this diff Show more