Implement xmpp-api v1

This commit is contained in:
Travis Burtrum 2017-12-22 00:37:38 -05:00
parent 8f0cd86090
commit 949c724259
11 changed files with 455 additions and 7 deletions

View File

@ -13,6 +13,7 @@ apply plugin: 'com.android.application'
repositories { repositories {
jcenter() jcenter()
mavenLocal()
mavenCentral() mavenCentral()
maven { maven {
url 'https://maven.google.com' url 'https://maven.google.com'
@ -51,6 +52,7 @@ dependencies {
compile 'com.makeramen:roundedimageview:2.3.0' compile 'com.makeramen:roundedimageview:2.3.0'
compile "com.wefika:flowlayout:0.4.1" compile "com.wefika:flowlayout:0.4.1"
compile 'net.ypresto.androidtranscoder:android-transcoder:0.2.0' compile 'net.ypresto.androidtranscoder:android-transcoder:0.2.0'
compile 'com.moparisthebest:xmpp-api:1.0-SNAPSHOT'
} }
ext { ext {
@ -68,7 +70,7 @@ android {
versionCode 236 versionCode 236
versionName "1.21.0" versionName "1.21.0"
archivesBaseName += "-$versionName" archivesBaseName += "-$versionName"
applicationId "eu.siacs.conversations" applicationId "eu.siacs.conversations.sms"
} }
dexOptions { dexOptions {

View File

@ -218,6 +218,18 @@
android:exported="false" android:exported="false"
android:grantUriPermissions="true"/> android:grantUriPermissions="true"/>
<!-- Xmpp Remote API, this service has explicitly no permission requirements
because we are using our own package based allow/disallow system
-->
<service
android:name=".remote.XmppPluginService"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="org.openintents.xmpp.IXmppService" />
</intent-filter>
</service>
</application> </application>

View File

@ -39,6 +39,7 @@ public class PgpDecryptionService {
} }
public synchronized boolean decrypt(final Message message, boolean notify) { public synchronized boolean decrypt(final Message message, boolean notify) {
// todo: wtf to do with these
messages.add(message); messages.add(message);
if (notify && pendingIntent == null) { if (notify && pendingIntent == null) {
pendingNotifications.add(message); pendingNotifications.add(message);

View File

@ -301,6 +301,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
} }
public void populateWithMessages(final List<Message> messages) { public void populateWithMessages(final List<Message> messages) {
// todo: what about what calls this method???
synchronized (this.messages) { synchronized (this.messages) {
messages.clear(); messages.clear();
messages.addAll(this.messages); messages.addAll(this.messages);

View File

@ -576,6 +576,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
return; return;
} }
// todo: hook here but prepend? put this off for later
mXmppConnectionService.newMessage(message);
if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
conversation.prepend(message); conversation.prepend(message);
} else { } else {

View File

@ -0,0 +1,364 @@
package eu.siacs.conversations.remote;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
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.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.stanzas.*;
import org.openintents.xmpp.IXmppPluginCallback;
import org.openintents.xmpp.XmppError;
import org.openintents.xmpp.util.XmppPluginCallbackApi;
import java.io.*;
import java.util.*;
import static org.openintents.xmpp.util.XmppPluginCallbackApi.*;
import static org.openintents.xmpp.util.XmppServiceApi.*;
import static org.openintents.xmpp.util.XmppUtils.*;
public class XmppPluginService extends Service {
private static final int[] SUPPORTED_VERSIONS = new int[]{1};
private XmppConnectionService xmppConnectionService = null;
private boolean xmppConnectionServiceBound = false;
private RemoteCallbackList<XmppPluginCallbackApi> pluginCallbacks = new RemoteCallbackList<>();
@Override
public void onCreate() {
super.onCreate();
if (!xmppConnectionServiceBound) {
connectToBackend();
}
}
protected ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
final XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service;
xmppConnectionService = binder.getService();
xmppConnectionService.setXmppPluginService(XmppPluginService.this);
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
xmppConnectionService = null;
xmppConnectionServiceBound = false;
}
};
public void connectToBackend() {
final Intent intent = new Intent(this, XmppConnectionService.class);
intent.setAction("ui");
startService(intent);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
private final org.openintents.xmpp.AbstractXmppService mBinder = new org.openintents.xmpp.AbstractXmppService() {
@Override
public Intent execute(final Intent data, final InputStream input, final OutputStream output) {
try {
return executeInternal(data, input, output);
} catch (Exception e) {
Log.e(Config.LOGTAG, "error in execute", e);
return getExceptionError(e);
}
}
@Override
public Intent callback(final Intent data, final IXmppPluginCallback iXmppPluginCallback) {
try {
return callbackInternal(data, iXmppPluginCallback);
} catch (Exception e) {
Log.e(Config.LOGTAG, "error in callback", e);
return getExceptionError(e);
}
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
protected Intent executeInternal(final Intent data, final InputStream inputStream, final OutputStream outputStream) throws Exception {
// We need to be able to load our own parcelables
data.setExtrasClassLoader(getClassLoader());
final Intent errorResult = checkRequirements(data);
if (errorResult != null) {
return errorResult;
}
final String action = data.getAction();
switch (action) {
case ACTION_CHECK_PERMISSION: {
return null;// todo: checkPermissionImpl(data);
}
case ACTION_GET_ACCOUNT_JID: {
return getAccountJid(data);
}
case ACTION_SEND_RAW_XML: {
return sendRawXml(data, null);
}
default: {
return getError(XmppError.GENERIC_ERROR, "invalid action!");
}
}
}
protected Intent callbackInternal(final Intent data, final IXmppPluginCallback iXmppPluginCallback) throws Exception {
// We need to be able to load our own parcelables
data.setExtrasClassLoader(getClassLoader());
final Intent errorResult = checkRequirements(data);
if (errorResult != null) {
return errorResult;
}
final String action = data.getAction();
switch (action) {
case ACTION_REGISTER_PLUGIN_CALLBACK: {
return registerPluginCallback(data, iXmppPluginCallback);
}
case ACTION_UNREGISTER_PLUGIN_CALLBACK: {
return unRegisterPluginCallback(data, iXmppPluginCallback);
}
case ACTION_SEND_RAW_XML: {
return sendRawXml(data, iXmppPluginCallback);
}
default: {
return getError(XmppError.GENERIC_ERROR, "invalid action!");
}
}
}
private Intent registerPluginCallback(final Intent data, final IXmppPluginCallback iXmppPluginCallback) {
if(pluginCallbacks.register(new XmppPluginCallbackApi(getApplicationContext(), iXmppPluginCallback,
data.getStringExtra(EXTRA_ACCOUNT_JID), data.getStringExtra(EXTRA_JID_LOCAL_PART), data.getStringExtra(EXTRA_JID_DOMAIN))))
return getSuccess();
return getError(XmppError.GENERIC_ERROR, "callback register failed");
}
private Intent unRegisterPluginCallback(final Intent data, final IXmppPluginCallback iXmppPluginCallback) {
if(pluginCallbacks.unregister(new XmppPluginCallbackApi(getApplicationContext(), iXmppPluginCallback, "fake", null, null)))
return getSuccess();
/*
for(Iterator<XmppPluginCallbackApi> i = pluginCallbacks.iterator(); i.hasNext();) {
final XmppPluginCallbackApi pluginCallback = i.next();
if(pluginCallback.getXmppPluginCallback().getDelegate().equals(iXmppPluginCallback)) {
i.remove();
return getSuccess();
}
}*/
return getError(XmppError.GENERIC_ERROR, "callback unregister failed");
}
/**
* Check requirements:
* - params != null
* - has supported API version
* - is allowed to call the service (access has been granted)
*
* @return null if everything is okay, or a Bundle with an createErrorPendingIntent/PendingIntent
*/
private Intent checkRequirements(Intent data) {
// params Bundle is required!
if (data == null) {
return getError(XmppError.GENERIC_ERROR, "params Bundle required!");
}
if(data.getAction().equals(ACTION_GET_SUPPORTED_VERSIONS)) {
// for this action, we want to skip both version AND permission checks
return getSuccess().putExtra(EXTRA_SUPPORTED_VERSIONS, SUPPORTED_VERSIONS);
}
// version code is required and needs to correspond to version code of service!
// History of versions in Xmpp-api's CHANGELOG.md
final int version = data.getIntExtra(EXTRA_API_VERSION, -1);
if (version == -1 || Arrays.binarySearch(SUPPORTED_VERSIONS, version) < 0) {
return getError
(XmppError.INCOMPATIBLE_API_VERSIONS, "Incompatible API versions!\n"
+ "used API version: " + version + "\n"
+ "supported API versions: " + Arrays.toString(SUPPORTED_VERSIONS))
.putExtra(EXTRA_SUPPORTED_VERSIONS, SUPPORTED_VERSIONS);
}
// check if caller is allowed to access Conversations
// todo:
/*
Intent result = mApiPermissionHelper.isAllowedOrReturnIntent(data);
if (result != null) {
return result;
}
*/
return null;
}
private Intent getAccountJid(Intent data) {
// if data already contains EXTRA_ACCOUNT_JID, it has been executed again
// after user interaction. Then, we just need to return the JID again!
if (data.hasExtra(EXTRA_ACCOUNT_JID)) {
final String accountJid = data.getStringExtra(EXTRA_ACCOUNT_JID);
return getSuccess().putExtra(EXTRA_ACCOUNT_JID, accountJid);
} else {
/*
String currentPkg = mApiPermissionHelper.getCurrentCallingPackage();
String preferredUserId = data.getStringExtra(EXTRA_USER_ID);
PendingIntent pi = mApiPendingIntentFactory.createSelectSignKeyIdPendingIntent(data, currentPkg, preferredUserId);
// return PendingIntent to be executed by client
Intent result = new Intent();
result.putExtra(RESULT_CODE, RESULT_CODE_USER_INTERACTION_REQUIRED);
result.putExtra(RESULT_INTENT, pi);
*/
// todo: implement chooser above, until then, first account will do
final String accountJid = xmppConnectionService.getAccounts().get(0).getJid().toBareJid().toString();
return getSuccess().putExtra(EXTRA_ACCOUNT_JID, accountJid);
}
}
private Intent sendRawXml(Intent data, final IXmppPluginCallback iXmppPluginCallback) throws Exception {
final Account account = findAccount(data.getStringExtra(EXTRA_ACCOUNT_JID));
final XmlReader reader = new XmlReader(new StringReader(data.getStringExtra(EXTRA_RAW_XML)));
final Element parsedRawXml = reader.nextWholeElement(); // todo: should do this in loop or????
Log.d(Config.LOGTAG, "rawXml: " + parsedRawXml.toString());
switch(parsedRawXml.getName()) {
case "message": {
final MessagePacket packet = create(parsedRawXml, new MessagePacket());
packet.setFrom(account.getJid());
Log.d(Config.LOGTAG, "msgXml: " + packet.toString());
xmppConnectionService.sendMessagePacket(account, packet);
return getSuccess();
}
case "iq": {
final IqPacket packet = create(parsedRawXml, new IqPacket());
packet.setFrom(account.getJid());
OnIqPacketReceived callback = null;
if(iXmppPluginCallback != null) {
// plugin wants notified of response
callback = new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
final Intent result = new Intent();
result.setAction(ACTION_IQ_RESPONSE);
final String accountJid = account.getJid().toBareJid().toString();
result.putExtra(EXTRA_ACCOUNT_JID, accountJid);
result.putExtra(EXTRA_RAW_XML, packet.toString());
try {
iXmppPluginCallback.execute(result, null, -1);
} catch (RemoteException e) {
Log.e(Config.LOGTAG, "error returning iq to callback", e);
}
}
};
}
xmppConnectionService.sendIqPacket(account, packet, callback);
return getSuccess();
}
case "presence": {
final PresencePacket packet = create(parsedRawXml, new PresencePacket());
packet.setFrom(account.getJid());
xmppConnectionService.sendPresencePacket(account, packet);
return getSuccess();
}
default: {
// must be some type of nonza
XmppConnection connection = account.getXmppConnection();
if (connection != null) {
connection.sendPacket(create(parsedRawXml, new GenericStanza(parsedRawXml.getName())));
}
return getSuccess();
}
}
}
private Account findAccount(final String accountJid) {
if(accountJid != null)
for(Account account : xmppConnectionService.getAccounts())
if(accountJid.equals(account.getJid().toBareJid().toString()))
return account;
return null;
}
private static <T extends Element> T create(final Element element, final T packet) {
packet.setAttributes(element.getAttributes());
packet.setContent(element.getContent());
packet.setChildren(element.getChildren());
return packet;
}
private final IXmppCallback newMessageReturn = new IXmppCallback() {
@Override
public void onReturn(final Intent result) {
if(result.getIntExtra(RESULT_CODE, RESULT_CODE_ERROR) == RESULT_CODE_ERROR) {
// We need to be able to load our own parcelables
result.setExtrasClassLoader(getClassLoader());
Log.e(Config.LOGTAG, "error calling callback: " + result.getParcelableExtra(RESULT_ERROR));
// todo: should remove from pluginCallbacks on error or?
}
}
};
public void newMessage(final Message message) {
final int N = pluginCallbacks.beginBroadcast();
try {
if (N == 0)
return;
final String accountJid = message.getConversation().getAccount().getJid().toBareJid().toString();
final String localPart = message.getConversation().getJid().getLocalpart();
final String domain = message.getConversation().getJid().getDomainpart();
Intent result = null; // lazy load this
//for(final XmppPluginCallbackApi api : pluginCallbacks) {
for (int i = 0; i < N; ++i) {
final XmppPluginCallbackApi api = pluginCallbacks.getBroadcastItem(i);
if (api.matches(accountJid, localPart, domain)) {
if (result == null) {
result = new Intent();
result.setAction(ACTION_NEW_MESSAGE);
result.putExtra(EXTRA_ACCOUNT_JID, accountJid);
result.putExtra(EXTRA_MESSAGE_STATUS, message.getStatus());
final String partnerJid = message.getConversation().getJid().toBareJid().toString();
// need to set from/to based on status
if (message.getStatus() == 0) {
result.putExtra(EXTRA_MESSAGE_TO, accountJid);
result.putExtra(EXTRA_MESSAGE_FROM, partnerJid);
} else {
result.putExtra(EXTRA_MESSAGE_FROM, accountJid);
result.putExtra(EXTRA_MESSAGE_TO, partnerJid);
}
result.putExtra(EXTRA_MESSAGE_BODY, message.getBody());
}
api.executeApiAsync(result, null, null, newMessageReturn);
}
}
} finally {
pluginCallbacks.finishBroadcast();
}
}
}

View File

@ -34,6 +34,7 @@ import android.util.Log;
import android.util.LruCache; import android.util.LruCache;
import android.util.Pair; import android.util.Pair;
import eu.siacs.conversations.remote.XmppPluginService;
import net.java.otr4j.OtrException; import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session; import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionID;
@ -166,6 +167,7 @@ public class XmppConnectionService extends Service {
private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>(); private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
private long mLastActivity = 0; private long mLastActivity = 0;
private XmppPluginService xmppPluginService;
public DatabaseBackend databaseBackend; public DatabaseBackend databaseBackend;
private ContentObserver contactObserver = new ContentObserver(null) { private ContentObserver contactObserver = new ContentObserver(null) {
@ -1279,10 +1281,12 @@ public class XmppConnectionService extends Service {
} else { } else {
if (addToConversation) { if (addToConversation) {
conversation.add(message); conversation.add(message);
this.newMessage(message);
} }
if (saveInDb) { if (saveInDb) {
databaseBackend.createMessage(message); databaseBackend.createMessage(message);
} else if (message.edited()) { } else if (message.edited()) {
// todo: wtf to do with edited messages? SOL I guess?
databaseBackend.updateMessage(message, message.getEditedId()); databaseBackend.updateMessage(message, message.getEditedId());
} }
updateConversationUi(); updateConversationUi();
@ -3926,6 +3930,19 @@ public class XmppConnectionService extends Service {
return mShortcutService; return mShortcutService;
} }
public void setXmppPluginService(final XmppPluginService xmppPluginService) {
this.xmppPluginService = xmppPluginService;
}
public void newMessage(final Message message) {
if(xmppPluginService != null)
try {
xmppPluginService.newMessage(message);
} catch(Exception e) {
Log.e(Config.LOGTAG, "error sending message to xmppPluginService", e);
}
}
public interface OnMamPreferencesFetched { public interface OnMamPreferencesFetched {
void onPreferencesFetched(Element prefs); void onPreferencesFetched(Element prefs);

View File

@ -11,6 +11,7 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
@ -18,6 +19,7 @@ public class XmlReader {
private XmlPullParser parser; private XmlPullParser parser;
private PowerManager.WakeLock wakeLock; private PowerManager.WakeLock wakeLock;
private InputStream is; private InputStream is;
private boolean inputSet = false;
public XmlReader(WakeLock wakeLock) { public XmlReader(WakeLock wakeLock) {
this.parser = Xml.newPullParser(); this.parser = Xml.newPullParser();
@ -29,11 +31,24 @@ public class XmlReader {
this.wakeLock = wakeLock; this.wakeLock = wakeLock;
} }
public XmlReader(final Reader input) throws IOException {
this((WakeLock) null);
if(input == null)
throw new IOException();
this.inputSet = true;
try {
parser.setInput(input);
} catch (XmlPullParserException e) {
throw new IOException("error resetting parser");
}
}
public void setInputStream(InputStream inputStream) throws IOException { public void setInputStream(InputStream inputStream) throws IOException {
if (inputStream == null) { if (inputStream == null) {
throw new IOException(); throw new IOException();
} }
this.is = inputStream; this.is = inputStream;
this.inputSet = true;
try { try {
parser.setInput(new InputStreamReader(this.is)); parser.setInput(new InputStreamReader(this.is));
} catch (XmlPullParserException e) { } catch (XmlPullParserException e) {
@ -53,7 +68,7 @@ public class XmlReader {
} }
public Tag readTag() throws XmlPullParserException, IOException { public Tag readTag() throws XmlPullParserException, IOException {
if (wakeLock.isHeld()) { if (wakeLock != null && wakeLock.isHeld()) {
try { try {
wakeLock.release(); wakeLock.release();
} catch (RuntimeException re) { } catch (RuntimeException re) {
@ -61,8 +76,10 @@ public class XmlReader {
} }
} }
try { try {
while (this.is != null && parser.next() != XmlPullParser.END_DOCUMENT) { while (inputSet && parser.next() != XmlPullParser.END_DOCUMENT) {
if(wakeLock != null) {
wakeLock.acquire(); wakeLock.acquire();
}
if (parser.getEventType() == XmlPullParser.START_TAG) { if (parser.getEventType() == XmlPullParser.START_TAG) {
Tag tag = Tag.start(parser.getName()); Tag tag = Tag.start(parser.getName());
final String xmlns = parser.getNamespace(); final String xmlns = parser.getNamespace();
@ -90,7 +107,7 @@ public class XmlReader {
} catch (Throwable throwable) { } catch (Throwable throwable) {
throw new IOException("xml parser mishandled "+throwable.getClass().getSimpleName()+"("+throwable.getMessage()+")", throwable); throw new IOException("xml parser mishandled "+throwable.getClass().getSimpleName()+"("+throwable.getMessage()+")", throwable);
} finally { } finally {
if (wakeLock.isHeld()) { if (wakeLock != null && wakeLock.isHeld()) {
try { try {
wakeLock.release(); wakeLock.release();
} catch (RuntimeException re) { } catch (RuntimeException re) {
@ -128,4 +145,29 @@ public class XmlReader {
} }
return element; return element;
} }
public Element nextWholeElement() throws XmlPullParserException, IOException {
final Tag currentTag = this.readTag();
if(currentTag == null)
return null;
final Element element = new Element(currentTag.name);
int tagCount = 1;
element.setAttributes(currentTag.getAttributes());
Tag nextTag = this.readTag();
if (nextTag == null) {
throw new IOException("interrupted mid tag");
}
while (!nextTag.isEnd(element.getName()) && --tagCount < 1) {
if (nextTag.isStart(element.getName()))
++tagCount;
if (!nextTag.isNo()) {
element.addChild(this.readElement(nextTag));
}
nextTag = this.readTag();
if (nextTag == null) {
throw new IOException("interrupted mid tag");
}
}
return element;
}
} }

View File

@ -1364,7 +1364,7 @@ public class XmppConnection implements Runnable {
this.sendPacket(packet); this.sendPacket(packet);
} }
private synchronized void sendPacket(final AbstractStanza packet) { public synchronized void sendPacket(final AbstractStanza packet) {
if (stanzasSent == Integer.MAX_VALUE) { if (stanzasSent == Integer.MAX_VALUE) {
resetStreamId(); resetStreamId();
disconnect(true); disconnect(true);

View File

@ -227,7 +227,7 @@ public final class Jid {
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
final Jid jid = (Jid) o; final Jid jid = (Jid) o;
// todo: this is a bug, hashcodes could be the same for different JIDs...
return jid.hashCode() == this.hashCode(); return jid.hashCode() == this.hashCode();
} }

View File

@ -0,0 +1,7 @@
package eu.siacs.conversations.xmpp.stanzas;
public class GenericStanza extends AbstractStanza {
public GenericStanza(final String name) {
super(name);
}
}