package info.guardianproject.otr.app.im.plugin.xmpp; import info.guardianproject.otr.app.im.engine.Address; import info.guardianproject.otr.app.im.engine.ChatGroupManager; import info.guardianproject.otr.app.im.engine.ChatSession; import info.guardianproject.otr.app.im.engine.ChatSessionManager; import info.guardianproject.otr.app.im.engine.Contact; import info.guardianproject.otr.app.im.engine.ContactList; import info.guardianproject.otr.app.im.engine.ContactListListener; import info.guardianproject.otr.app.im.engine.ContactListManager; import info.guardianproject.otr.app.im.engine.ImConnection; import info.guardianproject.otr.app.im.engine.ImErrorInfo; import info.guardianproject.otr.app.im.engine.ImException; import info.guardianproject.otr.app.im.engine.Message; import info.guardianproject.otr.app.im.engine.Presence; import info.guardianproject.otr.app.im.provider.Imps; import info.guardianproject.util.LogCleaner; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.harmony.javax.security.auth.callback.Callback; import org.apache.harmony.javax.security.auth.callback.CallbackHandler; import org.jivesoftware.smack.JmDNSService; import org.jivesoftware.smack.LLChat; import org.jivesoftware.smack.LLChatListener; import org.jivesoftware.smack.LLMessageListener; import org.jivesoftware.smack.LLPresence; import org.jivesoftware.smack.LLPresence.Mode; import org.jivesoftware.smack.LLPresenceListener; import org.jivesoftware.smack.LLService; import org.jivesoftware.smack.LLServiceStateListener; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smackx.LLServiceDiscoveryManager; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.MulticastLock; import android.net.wifi.WifiManager.WifiLock; import android.util.Log; public class LLXmppConnection extends ImConnection implements CallbackHandler { final static String TAG = "ChatSecure.LLXmppConnection"; private XmppContactListManager mContactListManager; private Contact mUser; private XmppChatSessionManager mSessionManager; private ThreadPoolExecutor mExecutor; private long mAccountId = -1; private long mProviderId = -1; private final static int SOTIMEOUT = 15000; private LLService mService; private MulticastLock mcLock; private WifiLock wifiLock; private InetAddress ipAddress; private String mServiceName; private String mResource; static { LLServiceDiscoveryManager.addServiceListener(); } public LLXmppConnection(Context context) { super(context); SmackConfiguration.setPacketReplyTimeout(SOTIMEOUT); // Create a single threaded executor. This will serialize actions on the // underlying connection. createExecutor(); DeliveryReceipts.addExtensionProviders(); String identityResource = "ChatSecure"; String identityType = "phone"; LLServiceDiscoveryManager.setIdentityName(identityResource); LLServiceDiscoveryManager.setIdentityType(identityType); } private void createExecutor() { mExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } private boolean execute(Runnable runnable) { try { mExecutor.execute(runnable); } catch (RejectedExecutionException ex) { return false; } return true; } // Execute a runnable only if we are idle private boolean executeIfIdle(Runnable runnable) { if (mExecutor.getActiveCount() + mExecutor.getQueue().size() == 0) { return execute(runnable); } return false; } public void join() throws InterruptedException { ExecutorService oldExecutor = mExecutor; createExecutor(); oldExecutor.shutdown(); oldExecutor.awaitTermination(10, TimeUnit.SECONDS); } public void sendPacket(final org.jivesoftware.smack.packet.Message message) { execute(new Runnable() { @Override public void run() { LLChat chat; try { chat = mService.getChat(Address.stripResource(message.getTo())); chat.sendMessage(message); } catch (XMPPException e) { Log.e(TAG, "Could not send message", e); } } }); } @Override protected void doUpdateUserPresenceAsync(Presence presence) { String statusText = presence.getStatusText(); Mode mode = Mode.avail; if (presence.getStatus() == Presence.AWAY) { mode = Mode.away; } else if (presence.getStatus() == Presence.IDLE) { mode = Mode.away; } else if (presence.getStatus() == Presence.DO_NOT_DISTURB) { mode = Mode.dnd; } else if (presence.getStatus() == Presence.OFFLINE) { statusText = "Offline"; } mService.getLocalPresence().setStatus(mode); mService.getLocalPresence().setMsg(statusText); try { mService.updatePresence(mService.getLocalPresence()); } catch (XMPPException e) { Log.e(TAG, "Could not update presence", e); } mUserPresence = presence; notifyUserPresenceUpdated(); } @Override public int getCapability() { return ImConnection.CAPABILITY_SESSION_REESTABLISHMENT; } @Override public ChatGroupManager getChatGroupManager() { return null; } @Override public synchronized ChatSessionManager getChatSessionManager() { if (mSessionManager == null) mSessionManager = new XmppChatSessionManager(); return mSessionManager; } @Override public synchronized XmppContactListManager getContactListManager() { if (mContactListManager == null) mContactListManager = new XmppContactListManager(); return mContactListManager; } @Override public Contact getLoginUser() { return mUser; } @Override public Map<String, String> getSessionContext() { // Empty state for now (but must have at least one key) return Collections.singletonMap("state", "empty"); } @Override public int[] getSupportedPresenceStatus() { return new int[] { Presence.AVAILABLE, Presence.AWAY, Presence.DO_NOT_DISTURB, }; } @Override public boolean isUsingTor() { return false; // link-local will never use Tor } @Override public void loginAsync(long accountId, String passwordTemp, long providerId, boolean retry) { mAccountId = accountId; mProviderId = providerId; execute(new Runnable() { @Override public void run() { do_login(); } }); } public void do_login() { ContentResolver contentResolver = mContext.getContentResolver(); try { Cursor cursor = contentResolver.query(Imps.ProviderSettings.CONTENT_URI,new String[] {Imps.ProviderSettings.NAME, Imps.ProviderSettings.VALUE},Imps.ProviderSettings.PROVIDER + "=?",new String[] { Long.toString(mProviderId)},null); if (cursor == null) throw new ImException ("unable to query the provider settings"); Imps.ProviderSettings.QueryMap providerSettings = new Imps.ProviderSettings.QueryMap( cursor, contentResolver, mProviderId, false, null); // providerSettings is closed in initConnection() String userName = Imps.Account.getUserName(contentResolver, mAccountId); String domain = providerSettings.getDomain(); mResource = providerSettings.getXmppResource(); initConnection(userName, domain, providerSettings); } catch (Exception e) { Log.w(TAG, "login failed", e); ImErrorInfo info = new ImErrorInfo(ImErrorInfo.UNKNOWN_ERROR, e.getMessage()); setState(DISCONNECTED, info); mService = null; } } @Override public void setProxy(String type, String host, int port) { // Ignore proxies for mDNS } // Runs in executor thread private void initConnection(String userName, String domain, Imps.ProviderSettings.QueryMap providerSettings) throws Exception { setState(LOGGING_IN, null); mServiceName = userName + '@' + domain;// + '/' + mResource; ipAddress = getMyAddress(mServiceName, true); if (ipAddress == null) { ImErrorInfo info = new ImErrorInfo(ImErrorInfo.WIFI_NOT_CONNECTED_ERROR, "network connection is required"); setState(DISCONNECTED, info); return; } mUserPresence = new Presence(Presence.AVAILABLE, "", null, null, Presence.CLIENT_TYPE_MOBILE); LLPresence presence = new LLPresence(mServiceName); presence.setNick(userName); presence.setJID(mServiceName); presence.setServiceName(mServiceName); mService = JmDNSService.create(presence, ipAddress); mService.addServiceStateListener(new LLServiceStateListener() { @Override public void serviceNameChanged(String newName, String oldName) { debug(TAG, "Service named changed from " + oldName + " to " + newName + "."); } @Override public void serviceClosed() { debug(TAG, "Service closed"); if (getState() != SUSPENDED) { setState(DISCONNECTED, null); } releaseLocks(); } @Override public void serviceClosedOnError(Exception e) { debug(TAG, "Service closed on error"); ImErrorInfo info = new ImErrorInfo(ImErrorInfo.UNKNOWN_ERROR, e.getMessage()); setState(DISCONNECTED, info); releaseLocks(); } @Override public void unknownOriginMessage(org.jivesoftware.smack.packet.Message m) { debug(TAG, "This message has unknown origin:"); debug(TAG, m.toXML()); } }); // Adding presence listener. mService.addPresenceListener(new LLPresenceListener() { @Override public void presenceRemove(final LLPresence presence) { execute(new Runnable() { @Override public void run() { mContactListManager.handlePresenceChanged(presence, true); } }); } @Override public void presenceNew(final LLPresence presence) { execute(new Runnable() { @Override public void run() { mContactListManager.handlePresenceChanged(presence, false); } }); } }); debug(TAG, "Preparing link-local service discovery"); LLServiceDiscoveryManager disco = LLServiceDiscoveryManager.getInstanceFor(mService); disco.addFeature(DeliveryReceipts.NAMESPACE); // Start listen for Link-local chats mService.addLLChatListener(new LLChatListener() { @Override public void newChat(LLChat chat) { chat.addMessageListener(new LLMessageListener() { @Override public void processMessage(LLChat chat, org.jivesoftware.smack.packet.Message message) { String address = message.getFrom(); ChatSession session = findOrCreateSession(address); DeliveryReceipts.DeliveryReceipt dr = (DeliveryReceipts.DeliveryReceipt) message .getExtension("received", DeliveryReceipts.NAMESPACE); if (dr != null) { debug(TAG, "got delivery receipt for " + dr.getId()); session.onMessageReceipt(dr.getId()); } if (message.getBody() == null) return; Message rec = new Message(message.getBody()); rec.setTo(mUser.getAddress()); rec.setFrom(session.getParticipant().getAddress()); rec.setDateTime(new Date()); rec.setType(Imps.MessageType.INCOMING); session.onReceiveMessage(rec); if (message.getExtension("request", DeliveryReceipts.NAMESPACE) != null) { debug(TAG, "got delivery receipt request"); // got XEP-0184 request, send receipt sendReceipt(message); session.onReceiptsExpected(); } } }); } @Override public void chatInvalidated(LLChat chat) { // TODO } }); makeUser(providerSettings);//mUser = new Contact(new XmppAddress(mServiceName), userName); // Initiate Link-local message session mService.init(); debug(TAG, "logged in"); setState(LOGGED_IN, null); } @Override public void initUser(long providerId, long accountId) throws ImException { ContentResolver contentResolver = mContext.getContentResolver(); Cursor cursor = contentResolver.query(Imps.ProviderSettings.CONTENT_URI,new String[] {Imps.ProviderSettings.NAME, Imps.ProviderSettings.VALUE},Imps.ProviderSettings.PROVIDER + "=?",new String[] { Long.toString(mProviderId)},null); if (cursor == null) throw new ImException("unable to query settings"); Imps.ProviderSettings.QueryMap providerSettings = new Imps.ProviderSettings.QueryMap( cursor, contentResolver, mProviderId, false, null); mProviderId = providerId; mAccountId = accountId; mUser = makeUser(providerSettings); providerSettings.close(); } private Contact makeUser(Imps.ProviderSettings.QueryMap providerSettings) { ContentResolver contentResolver = mContext.getContentResolver(); String userName = Imps.Account.getUserName(contentResolver, mAccountId); String domain = providerSettings.getDomain(); String xmppName = userName + '@' + domain + '/' + providerSettings.getXmppResource(); return new Contact(new XmppAddress(xmppName), userName); } private InetAddress getMyAddress(final String serviceName, boolean doLock) { if (mServiceName == null) return null; WifiManager wifi = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); WifiInfo connectionInfo = wifi.getConnectionInfo(); if (connectionInfo == null || connectionInfo.getBSSID() == null) { //not on wifi, nothing to do return null; /* Log.w(TAG, "Not connected to wifi. This may not work."); // Get the IP the usual Java way try { for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en .hasMoreElements();) { NetworkInterface intf = en.nextElement(); for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr .hasMoreElements();) { InetAddress inetAddress = enumIpAddr.nextElement(); if (!inetAddress.isLoopbackAddress()) { return inetAddress; } } } } catch (SocketException e) { Log.e(TAG, "while enumerating interfaces", e); return null; }*/ } int ip = connectionInfo.getIpAddress(); InetAddress address; try { address = InetAddress.getByAddress(new byte[] { (byte) ((ip) & 0xff), (byte) ((ip >> 8) & 0xff), (byte) ((ip >> 16) & 0xff), (byte) ((ip >> 24) & 0xff) }); } catch (UnknownHostException e) { Log.e(TAG, "unknown host exception when converting ip address"); return null; } if (doLock) { mcLock = wifi.createMulticastLock(serviceName); mcLock.acquire(); // HIGH_PERF is only available on android-12 and above int wifiMode; try { wifiMode = (Integer) WifiManager.class.getField("WIFI_MODE_FULL_HIGH_PERF").get( null); } catch (Exception e) { wifiMode = WifiManager.WIFI_MODE_FULL; } wifiLock = wifi.createWifiLock(wifiMode, serviceName); wifiLock.acquire(); } return address; } private void releaseLocks() { if (mcLock != null) mcLock.release(); mcLock = null; if (wifiLock != null) wifiLock.release(); wifiLock = null; } public void sendReceipt(org.jivesoftware.smack.packet.Message msg) { debug(TAG, "sending XEP-0184 ack to " + msg.getFrom() + " id=" + msg.getPacketID()); org.jivesoftware.smack.packet.Message ack = new org.jivesoftware.smack.packet.Message( msg.getFrom(), msg.getType()); ack.addExtension(new DeliveryReceipts.DeliveryReceipt(msg.getPacketID())); sendPacket(ack); } void disconnected(ImErrorInfo info) { Log.w(TAG, "disconnected"); setState(DISCONNECTED, info); } protected static int parsePresence(LLPresence presence, boolean offline) { if (offline) return Presence.OFFLINE; int type = Presence.AVAILABLE; Mode rmode = presence.getStatus(); if (rmode == Mode.away) type = Presence.AWAY; else if (rmode == Mode.dnd) type = Presence.DO_NOT_DISTURB; return type; } protected static String parseAddressBase(String from) { return from.replaceFirst("/.*", ""); } protected static String parseAddressName(String from) { return from.replaceFirst("@.*", ""); } @Override public void logoutAsync() { // TODO invoke join() here? execute(new Runnable() { @Override public void run() { logout(); } }); } // Force immediate logout @Override public void logout() { try { if (mService != null) { mService.close(); mService = null; } } catch (Exception e) { debug(TAG, "error logging out"); } } @Override public void suspend() { execute(new Runnable() { @Override public void run() { do_suspend(); } }); } private void do_suspend() { debug(TAG, "suspend"); setState(SUSPENDED, null); logout(); } private ChatSession findOrCreateSession(String address) { ChatSession session = mSessionManager.findSession(address); if (session == null) { Contact contact = findOrCreateContact(parseAddressName(address), address); session = mSessionManager.createChatSession(contact,true); } return session; } Contact findOrCreateContact(String name, String address) { Contact contact = mContactListManager.getContact(address); if (contact == null) { contact = makeContact(name, address); } return contact; } private static Contact makeContact(String name, String address) { Contact contact = new Contact(new XmppAddress(address), name); return contact; } private final class XmppChatSessionManager extends ChatSessionManager { @Override public void sendMessageAsync(ChatSession session, Message message) { org.jivesoftware.smack.packet.Message msgXmpp = new org.jivesoftware.smack.packet.Message( message.getTo().getAddress(), org.jivesoftware.smack.packet.Message.Type.chat); msgXmpp.addExtension(new DeliveryReceipts.DeliveryReceiptRequest()); msgXmpp.setBody(message.getBody()); if (message.getID() != null) msgXmpp.setPacketID(message.getID()); else message.setID(msgXmpp.getPacketID()); sendPacket(msgXmpp); } ChatSession findSession(String address) { return mSessions.get(Address.stripResource(address)); } } public ChatSession findSession(String address) { return mSessionManager.findSession(address); } public ChatSession createChatSession(Contact contact) { return mSessionManager.createChatSession(contact,true); } public class XmppContactListManager extends ContactListManager { public XmppContactListManager () { super(); } private void do_loadContactLists() { String generalGroupName = "Buddies"; Collection<Contact> contacts = new ArrayList<Contact>(); ContactList cl = new ContactList(mUser.getAddress(), generalGroupName, true, contacts, this); notifyContactListCreated(cl); notifyContactListsLoaded(); } @Override protected void setListNameAsync(final String name, final ContactList list) { execute(new Runnable() { @Override public void run() { // TODO } }); } @Override public String normalizeAddress(String address) { return new XmppAddress(address).getBareAddress(); } @Override public void loadContactListsAsync() { execute(new Runnable() { @Override public void run() { do_loadContactLists(); } }); } private void handlePresenceChanged(LLPresence presence, boolean offline) { if (presence.getServiceName().equals(mServiceName)) return; //this is from us! // Create default lists on first presence received if (getState() != ContactListManager.LISTS_LOADED) { loadContactListsAsync(); } String name = presence.getNick(); String address = presence.getJID(); if (address == null) //sometimes with zeroconf/bonjour there may not be a JID address = presence.getServiceName(); XmppAddress xaddress = new XmppAddress(address); if (name == null) name = xaddress.getUser(); Contact contact = findOrCreateContact(name,xaddress.getAddress()); try { if (!mContactListManager.getDefaultContactList().containsContact(contact)) { mContactListManager.getDefaultContactList().addExistingContact(contact); notifyContactListUpdated(mContactListManager.getDefaultContactList(), ContactListListener.LIST_CONTACT_ADDED, contact); } } catch (ImException e) { LogCleaner.error(TAG, "unable to add contact to list", e); } Presence p = new Presence(parsePresence(presence, offline), presence.getMsg(), null, null, Presence.CLIENT_TYPE_DEFAULT); contact.setPresence(p); Contact[] contacts = new Contact[] { contact }; notifyContactsPresenceUpdated(contacts); } @Override protected ImConnection getConnection() { return LLXmppConnection.this; } @Override protected void doRemoveContactFromListAsync(Contact contact, ContactList list) { // TODO } @Override protected void doDeleteContactListAsync(ContactList list) { // TODO delete contact list debug(TAG, "delete contact list " + list.getName()); } @Override protected void doCreateContactListAsync(String name, Collection<Contact> contacts, boolean isDefault) { // TODO create contact list debug(TAG, "create contact list " + name + " default " + isDefault); } @Override protected void doBlockContactAsync(String address, boolean block) { // TODO block contact } private void doAddContact(String name, String address, ContactList list) { Contact contact = makeContact(name, address); if (!containsContact(contact)) notifyContactListUpdated(list, ContactListListener.LIST_CONTACT_ADDED, contact); } private void doAddContact(String name, String address) { try { doAddContact(name, address, getDefaultContactList()); } catch (ImException e) { Log.e(TAG, "Failed to add contact", e); } } @Override protected void doAddContactToListAsync(Contact address, ContactList list) throws ImException { debug(TAG, "add contact to " + list.getName()); // TODO } @Override public void declineSubscriptionRequest(Contact contact) { debug(TAG, "decline subscription"); // TODO } @Override public void approveSubscriptionRequest(Contact contact) { debug(TAG, "approve subscription"); // TODO } @Override public Contact[] createTemporaryContacts(String[] addresses) { Contact[] contacts = new Contact[addresses.length]; int i = 0; for (String address : addresses) { debug(TAG, "create temporary " + address); contacts[i++] = makeContact(parseAddressName(address), address); } return contacts; } @Override protected void doSetContactName(String address, String name) throws ImException { // stub - no server } } @Override public void networkTypeChanged() { super.networkTypeChanged(); } @Override protected void setState(int state, ImErrorInfo error) { debug(TAG, "setState to " + state); super.setState(state, error); } public static void debug(String tag, String msg) { LogCleaner.debug(tag, msg); } @Override public void handle(Callback[] arg0) throws IOException { for (Callback cb : arg0) { debug(TAG, cb.toString()); } } @Override public void reestablishSessionAsync(Map<String, String> sessionContext) { execute(new Runnable() { @Override public void run() { do_login(); } }); } @Override public void sendHeartbeat(long heartbeatInterval) { InetAddress newAddress = getMyAddress(mServiceName, false); if (newAddress != null && !ipAddress.equals(newAddress)) { debug(TAG, "new address, reconnect"); execute(new Runnable() { @Override public void run() { do_suspend(); do_login(); } }); } } }