Use Gradle build system
This commit is contained in:
		
						commit
						64bdd7e731
					
				
							
								
								
									
										41
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										9
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										16
									
								
								build.gradle
									
									
									
									
									
										Normal 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() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								conversations/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								conversations/build.gradle
									
									
									
									
									
										Normal 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') | ||||
| } | ||||
							
								
								
									
										124
									
								
								conversations/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								conversations/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal 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> | ||||
| @ -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() { | ||||
| 
 | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,231 @@ | ||||
| package eu.siacs.conversations.crypto; | ||||
| 
 | ||||
| import java.math.BigInteger; | ||||
| import java.security.KeyFactory; | ||||
| import java.security.KeyPair; | ||||
| import java.security.KeyPairGenerator; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.security.PrivateKey; | ||||
| import java.security.PublicKey; | ||||
| import java.security.spec.DSAPrivateKeySpec; | ||||
| import java.security.spec.DSAPublicKeySpec; | ||||
| import java.security.spec.InvalidKeySpecException; | ||||
| 
 | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| import android.util.Log; | ||||
| 
 | ||||
| import eu.siacs.conversations.Config; | ||||
| import eu.siacs.conversations.entities.Account; | ||||
| import eu.siacs.conversations.services.XmppConnectionService; | ||||
| import eu.siacs.conversations.xmpp.stanzas.MessagePacket; | ||||
| 
 | ||||
| import net.java.otr4j.OtrEngineHost; | ||||
| import net.java.otr4j.OtrException; | ||||
| import net.java.otr4j.OtrPolicy; | ||||
| import net.java.otr4j.OtrPolicyImpl; | ||||
| import net.java.otr4j.session.InstanceTag; | ||||
| import net.java.otr4j.session.SessionID; | ||||
| 
 | ||||
| public class OtrEngine implements OtrEngineHost { | ||||
| 
 | ||||
| 	private Account account; | ||||
| 	private OtrPolicy otrPolicy; | ||||
| 	private KeyPair keyPair; | ||||
| 	private XmppConnectionService mXmppConnectionService; | ||||
| 
 | ||||
| 	public OtrEngine(XmppConnectionService service, Account account) { | ||||
| 		this.account = account; | ||||
| 		this.otrPolicy = new OtrPolicyImpl(); | ||||
| 		this.otrPolicy.setAllowV1(false); | ||||
| 		this.otrPolicy.setAllowV2(true); | ||||
| 		this.otrPolicy.setAllowV3(true); | ||||
| 		this.keyPair = loadKey(account.getKeys()); | ||||
| 		this.mXmppConnectionService = service; | ||||
| 	} | ||||
| 
 | ||||
| 	private KeyPair loadKey(JSONObject keys) { | ||||
| 		if (keys == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		try { | ||||
| 			BigInteger x = new BigInteger(keys.getString("otr_x"), 16); | ||||
| 			BigInteger y = new BigInteger(keys.getString("otr_y"), 16); | ||||
| 			BigInteger p = new BigInteger(keys.getString("otr_p"), 16); | ||||
| 			BigInteger q = new BigInteger(keys.getString("otr_q"), 16); | ||||
| 			BigInteger g = new BigInteger(keys.getString("otr_g"), 16); | ||||
| 			KeyFactory keyFactory = KeyFactory.getInstance("DSA"); | ||||
| 			DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g); | ||||
| 			DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g); | ||||
| 			PublicKey publicKey = keyFactory.generatePublic(pubKeySpec); | ||||
| 			PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec); | ||||
| 			return new KeyPair(publicKey, privateKey); | ||||
| 		} catch (JSONException e) { | ||||
| 			return null; | ||||
| 		} catch (NoSuchAlgorithmException e) { | ||||
| 			return null; | ||||
| 		} catch (InvalidKeySpecException e) { | ||||
| 			return null; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private void saveKey() { | ||||
| 		PublicKey publicKey = keyPair.getPublic(); | ||||
| 		PrivateKey privateKey = keyPair.getPrivate(); | ||||
| 		KeyFactory keyFactory; | ||||
| 		try { | ||||
| 			keyFactory = KeyFactory.getInstance("DSA"); | ||||
| 			DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec( | ||||
| 					privateKey, DSAPrivateKeySpec.class); | ||||
| 			DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, | ||||
| 					DSAPublicKeySpec.class); | ||||
| 			this.account.setKey("otr_x", privateKeySpec.getX().toString(16)); | ||||
| 			this.account.setKey("otr_g", privateKeySpec.getG().toString(16)); | ||||
| 			this.account.setKey("otr_p", privateKeySpec.getP().toString(16)); | ||||
| 			this.account.setKey("otr_q", privateKeySpec.getQ().toString(16)); | ||||
| 			this.account.setKey("otr_y", publicKeySpec.getY().toString(16)); | ||||
| 		} catch (NoSuchAlgorithmException e) { | ||||
| 			e.printStackTrace(); | ||||
| 		} catch (InvalidKeySpecException e) { | ||||
| 			e.printStackTrace(); | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) { | ||||
| 		// TODO Auto-generated method stub | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void finishedSessionMessage(SessionID arg0, String arg1) | ||||
| 			throws OtrException { | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public String getFallbackMessage(SessionID arg0) { | ||||
| 		return "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that"; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public byte[] getLocalFingerprintRaw(SessionID arg0) { | ||||
| 		// TODO Auto-generated method stub | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	public PublicKey getPublicKey() { | ||||
| 		if (this.keyPair == null) { | ||||
| 			return null; | ||||
| 		} | ||||
| 		return this.keyPair.getPublic(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException { | ||||
| 		if (this.keyPair == null) { | ||||
| 			KeyPairGenerator kg; | ||||
| 			try { | ||||
| 				kg = KeyPairGenerator.getInstance("DSA"); | ||||
| 				this.keyPair = kg.genKeyPair(); | ||||
| 				this.saveKey(); | ||||
| 				mXmppConnectionService.databaseBackend.updateAccount(account); | ||||
| 			} catch (NoSuchAlgorithmException e) { | ||||
| 				Log.d(Config.LOGTAG, | ||||
| 						"error generating key pair " + e.getMessage()); | ||||
| 			} | ||||
| 		} | ||||
| 		return this.keyPair; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public String getReplyForUnreadableMessage(SessionID arg0) { | ||||
| 		// TODO Auto-generated method stub | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public OtrPolicy getSessionPolicy(SessionID arg0) { | ||||
| 		return otrPolicy; | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void injectMessage(SessionID session, String body) | ||||
| 			throws OtrException { | ||||
| 		MessagePacket packet = new MessagePacket(); | ||||
| 		packet.setFrom(account.getFullJid()); | ||||
| 		if (session.getUserID().isEmpty()) { | ||||
| 			packet.setTo(session.getAccountID()); | ||||
| 		} else { | ||||
| 			packet.setTo(session.getAccountID() + "/" + session.getUserID()); | ||||
| 		} | ||||
| 		packet.setBody(body); | ||||
| 		packet.addChild("private", "urn:xmpp:carbons:2"); | ||||
| 		packet.addChild("no-copy", "urn:xmpp:hints"); | ||||
| 		packet.setType(MessagePacket.TYPE_CHAT); | ||||
| 		account.getXmppConnection().sendMessagePacket(packet); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void messageFromAnotherInstanceReceived(SessionID id) { | ||||
| 		Log.d(Config.LOGTAG, | ||||
| 				"unreadable message received from " + id.getAccountID()); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void multipleInstancesDetected(SessionID arg0) { | ||||
| 		// TODO Auto-generated method stub | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void requireEncryptedMessage(SessionID arg0, String arg1) | ||||
| 			throws OtrException { | ||||
| 		// TODO Auto-generated method stub | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void showError(SessionID arg0, String arg1) throws OtrException { | ||||
| 		// TODO Auto-generated method stub | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void smpAborted(SessionID arg0) throws OtrException { | ||||
| 		// TODO Auto-generated method stub | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void smpError(SessionID arg0, int arg1, boolean arg2) | ||||
| 			throws OtrException { | ||||
| 		throw new OtrException(new Exception("smp error")); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void unencryptedMessageReceived(SessionID arg0, String arg1) | ||||
| 			throws OtrException { | ||||
| 		throw new OtrException(new Exception("unencrypted message received")); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void unreadableMessageReceived(SessionID arg0) throws OtrException { | ||||
| 		throw new OtrException(new Exception("unreadable message received")); | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void unverify(SessionID arg0, String arg1) { | ||||
| 		// TODO Auto-generated method stub | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Override | ||||
| 	public void verify(SessionID arg0, String arg1, boolean arg2) { | ||||
| 		// TODO Auto-generated method stub | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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()); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| } | ||||
| @ -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; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| package eu.siacs.conversations.entities; | ||||
| 
 | ||||
| public interface ListItem extends Comparable<ListItem> { | ||||
| 	public String getDisplayName(); | ||||
| 
 | ||||
| 	public String getJid(); | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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()); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,5 @@ | ||||
| package eu.siacs.conversations.persistance; | ||||
| 
 | ||||
| public interface OnPhoneContactsMerged { | ||||
| 	public void phoneContactsMerged(); | ||||
| } | ||||
| @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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)]; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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)); | ||||
| 	} | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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(); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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) { | ||||
| 
 | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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(); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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(""); | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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()); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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); | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -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(); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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; | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -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"); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| package eu.siacs.conversations.utils; | ||||
| 
 | ||||
| public class XmlHelper { | ||||
| 	public static String encodeEntities(String content) { | ||||
| 		content = content.replace("&", "&"); | ||||
| 		content = content.replace("<", "<"); | ||||
| 		content = content.replace(">", ">"); | ||||
| 		content = content.replace("\"", """); | ||||
| 		content = content.replace("'", "'"); | ||||
| 		return content; | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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(); | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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)); | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										104
									
								
								conversations/src/main/java/eu/siacs/conversations/xml/Tag.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								conversations/src/main/java/eu/siacs/conversations/xml/Tag.java
									
									
									
									
									
										Normal 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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| package eu.siacs.conversations.xmpp; | ||||
| 
 | ||||
| import eu.siacs.conversations.entities.Account; | ||||
| 
 | ||||
| public interface OnBindListener { | ||||
| 	public void onBind(Account account); | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| package eu.siacs.conversations.xmpp; | ||||
| 
 | ||||
| import eu.siacs.conversations.entities.Account; | ||||
| 
 | ||||
| public interface OnStatusChanged { | ||||
| 	public void onStatusChanged(Account account); | ||||
| } | ||||
| @ -0,0 +1,5 @@ | ||||
| package eu.siacs.conversations.xmpp; | ||||
| 
 | ||||
| public abstract interface PacketReceived { | ||||
| 
 | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -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() + ")"; | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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(); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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 | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -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(); | ||||
| } | ||||
| @ -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); | ||||
| } | ||||
| @ -0,0 +1,6 @@ | ||||
| package eu.siacs.conversations.xmpp.jingle; | ||||
| 
 | ||||
| public interface OnPrimaryCandidateFound { | ||||
| 	public void onPrimaryCandidateFound(boolean success, | ||||
| 			JingleCandidate canditate); | ||||
| } | ||||
| @ -0,0 +1,7 @@ | ||||
| package eu.siacs.conversations.xmpp.jingle; | ||||
| 
 | ||||
| public interface OnTransportConnected { | ||||
| 	public void failed(); | ||||
| 
 | ||||
| 	public void established(); | ||||
| } | ||||
| @ -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"); | ||||
| 	} | ||||
| } | ||||
| @ -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()); | ||||
| 	} | ||||
| } | ||||
| @ -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"); | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| } | ||||
| @ -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); | ||||
| 	} | ||||
| } | ||||
| @ -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; | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -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
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Sam Whited
						Sam Whited