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