diff options
Diffstat (limited to 'src/de/gultsch')
-rw-r--r-- | src/de/gultsch/chat/entities/Contact.java | 4 | ||||
-rw-r--r-- | src/de/gultsch/chat/persistance/DatabaseBackend.java | 4 | ||||
-rw-r--r-- | src/de/gultsch/chat/services/XmppConnectionService.java | 69 | ||||
-rw-r--r-- | src/de/gultsch/chat/ui/DialogContactDetails.java | 16 | ||||
-rw-r--r-- | src/de/gultsch/chat/ui/NewConversationActivity.java | 5 | ||||
-rw-r--r-- | src/de/gultsch/chat/xmpp/XmppConnection.java | 198 |
6 files changed, 202 insertions, 94 deletions
diff --git a/src/de/gultsch/chat/entities/Contact.java b/src/de/gultsch/chat/entities/Contact.java index da3c0810..25852319 100644 --- a/src/de/gultsch/chat/entities/Contact.java +++ b/src/de/gultsch/chat/entities/Contact.java @@ -157,4 +157,8 @@ public class Contact extends AbstractEntity implements Serializable { public int getMostAvailableStatus() { return this.presences.getMostAvailableStatus(); } + + public void setPresences(Presences pres) { + this.presences = pres; + } } diff --git a/src/de/gultsch/chat/persistance/DatabaseBackend.java b/src/de/gultsch/chat/persistance/DatabaseBackend.java index 135296a6..fe203137 100644 --- a/src/de/gultsch/chat/persistance/DatabaseBackend.java +++ b/src/de/gultsch/chat/persistance/DatabaseBackend.java @@ -8,6 +8,7 @@ import de.gultsch.chat.entities.Account; import de.gultsch.chat.entities.Contact; import de.gultsch.chat.entities.Conversation; import de.gultsch.chat.entities.Message; +import de.gultsch.chat.entities.Presences; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -214,12 +215,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); for (int i = 0; i < contacts.size(); i++) { Contact contact = contacts.get(i); - String[] columns = {Contact.UUID}; + String[] columns = {Contact.UUID, Contact.PRESENCES}; String[] args = {contact.getAccount().getUuid(), contact.getJid()}; Cursor cursor = db.query(Contact.TABLENAME, columns,Contact.ACCOUNT+"=? AND "+Contact.JID+"=?", args, null, null, null); if (cursor.getCount()>=1) { cursor.moveToFirst(); contact.setUuid(cursor.getString(0)); + contact.setPresences(Presences.fromJsonString(cursor.getString(1))); updateContact(contact); } else { contact.setUuid(UUID.randomUUID().toString()); diff --git a/src/de/gultsch/chat/services/XmppConnectionService.java b/src/de/gultsch/chat/services/XmppConnectionService.java index 0a189492..a1db28dd 100644 --- a/src/de/gultsch/chat/services/XmppConnectionService.java +++ b/src/de/gultsch/chat/services/XmppConnectionService.java @@ -62,20 +62,49 @@ public class XmppConnectionService extends Service { if ((packet.getType() == MessagePacket.TYPE_CHAT) || (packet.getType() == MessagePacket.TYPE_GROUPCHAT)) { boolean notify = true; + int status = Message.STATUS_RECIEVED; + String body; + String fullJid; if (!packet.hasChild("body")) { - return; + Element forwarded; + if (packet.hasChild("received")) { + forwarded = packet.findChild("received").findChild( + "forwarded"); + } else if (packet.hasChild("sent")) { + forwarded = packet.findChild("sent").findChild( + "forwarded"); + status = Message.STATUS_SEND; + } else { + return; // massage has no body and is not carbon. just + // skip + } + if (forwarded != null) { + Element message = forwarded.findChild("message"); + if ((message == null) || (!message.hasChild("body"))) + return; // either malformed or boring + if (status == Message.STATUS_RECIEVED) { + fullJid = message.getAttribute("from"); + } else { + fullJid = message.getAttribute("to"); + } + body = message.findChild("body").getContent(); + } else { + return; // packet malformed. has no forwarded element + } + } else { + fullJid = packet.getFrom(); + body = packet.getBody(); } Conversation conversation = null; - String fullJid = packet.getFrom(); String[] fromParts = fullJid.split("/"); String jid = fromParts[0]; Contact contact = findOrCreateContact(account, jid); boolean muc = (packet.getType() == MessagePacket.TYPE_GROUPCHAT); String counterPart = null; - int status = Message.STATUS_RECIEVED; conversation = findOrCreateConversation(account, contact, muc); if (muc) { - if ((fromParts.length==1)||(packet.hasChild("subject"))||(packet.hasChild("delay"))) { + if ((fromParts.length == 1) || (packet.hasChild("subject")) + || (packet.hasChild("delay"))) { return; } counterPart = fromParts[1]; @@ -86,8 +115,8 @@ public class XmppConnectionService extends Service { } else { counterPart = fullJid; } - Message message = new Message(conversation, counterPart, - packet.getBody(), Message.ENCRYPTION_NONE, status); + Message message = new Message(conversation, counterPart, body, + Message.ENCRYPTION_NONE, status); conversation.getMessages().add(message); databaseBackend.createMessage(message); if (convChangedListener != null) { @@ -124,28 +153,28 @@ public class XmppConnectionService extends Service { PresencePacket packet) { String[] fromParts = packet.getAttribute("from").split("/"); Contact contact = findOrCreateContact(account, fromParts[0]); - if (contact.getUuid()==null) { - //most likely muc, self or roster not synced - Log.d(LOGTAG,"got presence for non contact "+packet.toString()); + if (contact.getUuid() == null) { + // most likely muc, self or roster not synced + // Log.d(LOGTAG,"got presence for non contact "+packet.toString()); } String type = packet.getAttribute("type"); if (type == null) { Element show = packet.findChild("show"); - if (show==null) { - contact.updatePresence(fromParts[1],Presences.ONLINE); + if (show == null) { + contact.updatePresence(fromParts[1], Presences.ONLINE); } else if (show.getContent().equals("away")) { - contact.updatePresence(fromParts[1],Presences.AWAY); + contact.updatePresence(fromParts[1], Presences.AWAY); } else if (show.getContent().equals("xa")) { - contact.updatePresence(fromParts[1],Presences.XA); + contact.updatePresence(fromParts[1], Presences.XA); } else if (show.getContent().equals("chat")) { - contact.updatePresence(fromParts[1],Presences.CHAT); + contact.updatePresence(fromParts[1], Presences.CHAT); } else if (show.getContent().equals("dnd")) { - contact.updatePresence(fromParts[1],Presences.DND); + contact.updatePresence(fromParts[1], Presences.DND); } databaseBackend.updateContact(contact); } else if (type.equals("unavailable")) { - if (fromParts.length!=2) { - Log.d(LOGTAG,"received presence with no resource "+packet.toString()); + if (fromParts.length != 2) { + // Log.d(LOGTAG,"received presence with no resource "+packet.toString()); } else { contact.removePresence(fromParts[1]); databaseBackend.updateContact(contact); @@ -194,7 +223,7 @@ public class XmppConnectionService extends Service { } public void sendMessage(final Account account, final Message message) { - if (message.getConversation().getMode()==Conversation.MODE_SINGLE) { + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { databaseBackend.createMessage(message); } MessagePacket packet = new MessagePacket(); @@ -206,9 +235,9 @@ public class XmppConnectionService extends Service { packet.setTo(message.getCounterpart()); packet.setFrom(account.getJid()); packet.setBody(message.getBody()); - if (account.getStatus()==Account.STATUS_ONLINE) { + if (account.getStatus() == Account.STATUS_ONLINE) { connections.get(account).sendMessagePacket(packet); - if (message.getConversation().getMode()==Conversation.MODE_SINGLE) { + if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { message.setStatus(Message.STATUS_SEND); databaseBackend.updateMessage(message); } diff --git a/src/de/gultsch/chat/ui/DialogContactDetails.java b/src/de/gultsch/chat/ui/DialogContactDetails.java index 51819399..58e5f67f 100644 --- a/src/de/gultsch/chat/ui/DialogContactDetails.java +++ b/src/de/gultsch/chat/ui/DialogContactDetails.java @@ -38,13 +38,15 @@ public class DialogContactDetails extends DialogFragment { boolean subscriptionSend = false; boolean subscriptionReceive = false; - if (contact.getSubscription().equals("both")) { - subscriptionReceive = true; - subscriptionSend = true; - } else if (contact.getSubscription().equals("from")) { - subscriptionSend = true; - } else if (contact.getSubscription().equals("to")) { - subscriptionReceive = true; + if (contact.getSubscription()!=null) { + if (contact.getSubscription().equals("both")) { + subscriptionReceive = true; + subscriptionSend = true; + } else if (contact.getSubscription().equals("from")) { + subscriptionSend = true; + } else if (contact.getSubscription().equals("to")) { + subscriptionReceive = true; + } } switch (contact.getMostAvailableStatus()) { diff --git a/src/de/gultsch/chat/ui/NewConversationActivity.java b/src/de/gultsch/chat/ui/NewConversationActivity.java index 4e2628a6..2fb14008 100644 --- a/src/de/gultsch/chat/ui/NewConversationActivity.java +++ b/src/de/gultsch/chat/ui/NewConversationActivity.java @@ -181,7 +181,9 @@ public class NewConversationActivity extends XmppActivity { }); accountChooser.create().show(); } else { - clickedContact.setAccount(accounts.get(0)); + if (clickedContact.getAccount()==null) { + clickedContact.setAccount(accounts.get(0)); + } showIsMucDialogIfNeeded(clickedContact); } } @@ -226,6 +228,7 @@ public class NewConversationActivity extends XmppActivity { } public void startConversation(Contact contact, Account account, boolean muc) { + Log.d("xmppService","starting conversation for account:"+account.getJid()+" and contact:"+contact.getJid()); Conversation conversation = xmppConnectionService .findOrCreateConversation(account, contact, muc); diff --git a/src/de/gultsch/chat/xmpp/XmppConnection.java b/src/de/gultsch/chat/xmpp/XmppConnection.java index 772c2430..337d8c7d 100644 --- a/src/de/gultsch/chat/xmpp/XmppConnection.java +++ b/src/de/gultsch/chat/xmpp/XmppConnection.java @@ -7,7 +7,9 @@ import java.math.BigInteger; import java.net.Socket; import java.net.UnknownHostException; import java.security.SecureRandom; +import java.util.HashSet; import java.util.Hashtable; +import java.util.List; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; @@ -33,23 +35,24 @@ public class XmppConnection implements Runnable { private PowerManager.WakeLock wakeLock; private SecureRandom random = new SecureRandom(); - + private Socket socket; private XmlReader tagReader; private TagWriter tagWriter; private boolean isTlsEncrypted = false; private boolean isAuthenticated = false; - //private boolean shouldUseTLS = false; + // private boolean shouldUseTLS = false; private boolean shouldConnect = true; private boolean shouldBind = true; private boolean shouldAuthenticate = true; private Element streamFeatures; - + private HashSet<String> discoFeatures = new HashSet<String>(); + private static final int PACKET_IQ = 0; private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; - + private Hashtable<String, OnIqPacketReceived> iqPacketCallbacks = new Hashtable<String, OnIqPacketReceived>(); private OnPresencePacketReceived presenceListener = null; private OnIqPacketReceived unregisteredIqListener = null; @@ -69,9 +72,10 @@ public class XmppConnection implements Runnable { Bundle namePort = DNSHelper.getSRVRecord(account.getServer()); String srvRecordServer = namePort.getString("name"); int srvRecordPort = namePort.getInt("port"); - if (srvRecordServer!=null) { - Log.d(LOGTAG,account.getJid()+": using values from dns "+srvRecordServer+":"+srvRecordPort); - socket = new Socket(srvRecordServer,srvRecordPort); + if (srvRecordServer != null) { + Log.d(LOGTAG, account.getJid() + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + socket = new Socket(srvRecordServer, srvRecordPort); } else { socket = new Socket(account.getServer(), 5222); } @@ -96,30 +100,30 @@ public class XmppConnection implements Runnable { } } catch (UnknownHostException e) { account.setStatus(Account.STATUS_SERVER_NOT_FOUND); - if (statusListener!=null) { + if (statusListener != null) { statusListener.onStatusChanged(account); } return; } catch (IOException e) { - Log.d(LOGTAG,"bla "+e.getMessage()); + Log.d(LOGTAG, "bla " + e.getMessage()); if (shouldConnect) { - Log.d(LOGTAG,account.getJid()+": connection lost"); + Log.d(LOGTAG, account.getJid() + ": connection lost"); account.setStatus(Account.STATUS_OFFLINE); - if (statusListener!=null) { + if (statusListener != null) { statusListener.onStatusChanged(account); } } } catch (XmlPullParserException e) { - Log.d(LOGTAG,"xml exception "+e.getMessage()); + Log.d(LOGTAG, "xml exception " + e.getMessage()); return; } - + } @Override public void run() { shouldConnect = true; - while(shouldConnect) { + while (shouldConnect) { connect(); try { if (shouldConnect) { @@ -130,7 +134,7 @@ public class XmppConnection implements Runnable { e.printStackTrace(); } } - Log.d(LOGTAG,"end run"); + Log.d(LOGTAG, "end run"); } private void processStream(Tag currentTag) throws XmlPullParserException, @@ -145,17 +149,18 @@ public class XmppConnection implements Runnable { switchOverToTls(nextTag); } else if (nextTag.isStart("success")) { isAuthenticated = true; - Log.d(LOGTAG,account.getJid()+": read success tag in stream. reset again"); + Log.d(LOGTAG, account.getJid() + + ": read success tag in stream. reset again"); tagReader.readTag(); tagReader.reset(); sendStartStream(); processStream(tagReader.readTag()); break; - } else if(nextTag.isStart("failure")) { + } else if (nextTag.isStart("failure")) { Element failure = tagReader.readElement(nextTag); - Log.d(LOGTAG,"read failure element"+failure.toString()); + Log.d(LOGTAG, "read failure element" + failure.toString()); account.setStatus(Account.STATUS_UNAUTHORIZED); - if (statusListener!=null) { + if (statusListener != null) { statusListener.onStatusChanged(account); } tagWriter.writeTag(Tag.end("stream")); @@ -173,13 +178,14 @@ public class XmppConnection implements Runnable { } if (account.getStatus() == Account.STATUS_ONLINE) { account.setStatus(Account.STATUS_OFFLINE); - if (statusListener!=null) { + if (statusListener != null) { statusListener.onStatusChanged(account); } } } - - private Element processPacket(Tag currentTag, int packetType) throws XmlPullParserException, IOException { + + private Element processPacket(Tag currentTag, int packetType) + throws XmlPullParserException, IOException { Element element; switch (packetType) { case PACKET_IQ: @@ -196,7 +202,7 @@ public class XmppConnection implements Runnable { } element.setAttributes(currentTag.getAttributes()); Tag nextTag = tagReader.readTag(); - while(!nextTag.isEnd(element.getName())) { + while (!nextTag.isEnd(element.getName())) { if (!nextTag.isNo()) { Element child = tagReader.readElement(nextTag); element.addChild(child); @@ -205,44 +211,49 @@ public class XmppConnection implements Runnable { } return element; } - - private IqPacket processIq(Tag currentTag) throws XmlPullParserException, IOException { - IqPacket packet = (IqPacket) processPacket(currentTag,PACKET_IQ); + private IqPacket processIq(Tag currentTag) throws XmlPullParserException, + IOException { + IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); if (iqPacketCallbacks.containsKey(packet.getId())) { - iqPacketCallbacks.get(packet.getId()).onIqPacketReceived(account,packet); + iqPacketCallbacks.get(packet.getId()).onIqPacketReceived(account, + packet); iqPacketCallbacks.remove(packet.getId()); } else if (this.unregisteredIqListener != null) { - this.unregisteredIqListener.onIqPacketReceived(account,packet); + this.unregisteredIqListener.onIqPacketReceived(account, packet); } return packet; } - - private void processMessage(Tag currentTag) throws XmlPullParserException, IOException { - MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE); + + private void processMessage(Tag currentTag) throws XmlPullParserException, + IOException { + MessagePacket packet = (MessagePacket) processPacket(currentTag, + PACKET_MESSAGE); if (this.messageListener != null) { - this.messageListener.onMessagePacketReceived(account,packet); + this.messageListener.onMessagePacketReceived(account, packet); } } - - private void processPresence(Tag currentTag) throws XmlPullParserException, IOException { - PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); + + private void processPresence(Tag currentTag) throws XmlPullParserException, + IOException { + PresencePacket packet = (PresencePacket) processPacket(currentTag, + PACKET_PRESENCE); if (this.presenceListener != null) { - this.presenceListener.onPresencePacketReceived(account,packet); + this.presenceListener.onPresencePacketReceived(account, packet); } } private void sendStartTLS() { Tag startTLS = Tag.empty("starttls"); startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); - Log.d(LOGTAG,account.getJid()+": sending starttls"); + Log.d(LOGTAG, account.getJid() + ": sending starttls"); tagWriter.writeTag(startTLS); } private void switchOverToTls(Tag currentTag) throws XmlPullParserException, IOException { Tag nextTag = tagReader.readTag(); // should be proceed end tag - Log.d(LOGTAG,account.getJid()+": now switch to ssl"); + Log.d(LOGTAG, account.getJid() + ": now switch to ssl"); SSLSocket sslSocket; try { sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory @@ -257,7 +268,9 @@ public class XmppConnection implements Runnable { processStream(tagReader.readTag()); sslSocket.close(); } catch (IOException e) { - Log.d(LOGTAG, account.getJid()+": error on ssl '" + e.getMessage()+"'"); + Log.d(LOGTAG, + account.getJid() + ": error on ssl '" + e.getMessage() + + "'"); } } @@ -268,32 +281,36 @@ public class XmppConnection implements Runnable { auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); auth.setAttribute("mechanism", "PLAIN"); auth.setContent(saslString); - Log.d(LOGTAG,account.getJid()+": sending sasl "+auth.toString()); + Log.d(LOGTAG, account.getJid() + ": sending sasl " + auth.toString()); tagWriter.writeElement(auth); } private void processStreamFeatures(Tag currentTag) throws XmlPullParserException, IOException { this.streamFeatures = tagReader.readElement(currentTag); - Log.d(LOGTAG,account.getJid()+": process stream features "+streamFeatures); - if (this.streamFeatures.hasChild("starttls")&&account.isOptionSet(Account.OPTION_USETLS)) { + Log.d(LOGTAG, account.getJid() + ": process stream features " + + streamFeatures); + if (this.streamFeatures.hasChild("starttls") + && account.isOptionSet(Account.OPTION_USETLS)) { sendStartTLS(); - } else if (this.streamFeatures.hasChild("mechanisms")&&shouldAuthenticate) { + } else if (this.streamFeatures.hasChild("mechanisms") + && shouldAuthenticate) { sendSaslAuth(); } - if (this.streamFeatures.hasChild("bind")&&shouldBind) { + if (this.streamFeatures.hasChild("bind") && shouldBind) { sendBindRequest(); if (this.streamFeatures.hasChild("session")) { IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); Element session = new Element("session"); - session.setAttribute("xmlns","urn:ietf:params:xml:ns:xmpp-session"); + session.setAttribute("xmlns", + "urn:ietf:params:xml:ns:xmpp-session"); session.setContent(""); startSession.addChild(session); sendIqPacket(startSession, null); tagWriter.writeElement(startSession); } Element presence = new Element("presence"); - + tagWriter.writeElement(presence); } } @@ -301,17 +318,65 @@ public class XmppConnection implements Runnable { private void sendBindRequest() throws IOException { IqPacket iq = new IqPacket(IqPacket.TYPE_SET); Element bind = new Element("bind"); - bind.setAttribute("xmlns","urn:ietf:params:xml:ns:xmpp-bind"); + bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind"); iq.addChild(bind); - this.sendIqPacket(iq, new OnIqPacketReceived() { + this.sendIqPacket(iq, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - String resource = packet.findChild("bind").findChild("jid").getContent().split("/")[1]; + String resource = packet.findChild("bind").findChild("jid") + .getContent().split("/")[1]; account.setResource(resource); account.setStatus(Account.STATUS_ONLINE); - if (statusListener!=null) { + if (statusListener != null) { statusListener.onStatusChanged(account); } + sendServiceDiscovery(); + } + }); + } + + private void sendServiceDiscovery() { + IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.setAttribute("to", account.getServer()); + Element query = new Element("query"); + query.setAttribute("xmlns", "http://jabber.org/protocol/disco#info"); + iq.addChild(query); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.hasChild("query")) { + List<Element> elements = packet.findChild("query") + .getChildren(); + for (int i = 0; i < elements.size(); ++i) { + if (elements.get(i).getName().equals("feature")) { + discoFeatures.add(elements.get(i).getAttribute( + "var")); + } + } + } + if (discoFeatures.contains("urn:xmpp:carbons:2")) { + sendEnableCarbons(); + } + } + }); + } + + private void sendEnableCarbons() { + Log.d(LOGTAG,account.getJid()+": enable carbons"); + IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + Element enable = new Element("enable"); + enable.setAttribute("xmlns", "urn:xmpp:carbons:2"); + iq.addChild(enable); + this.sendIqPacket(iq, new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (!packet.hasChild("error")) { + Log.d(LOGTAG,account.getJid()+": successfully enabled carbons"); + } else { + Log.d(LOGTAG,account.getJid()+": error enableing carbons "+packet.toString()); + } } }); } @@ -334,38 +399,41 @@ public class XmppConnection implements Runnable { private String nextRandomId() { return new BigInteger(50, random).toString(32); } - + public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { String id = nextRandomId(); - packet.setAttribute("id",id); + packet.setAttribute("id", id); tagWriter.writeElement(packet); if (callback != null) { iqPacketCallbacks.put(id, callback); } - Log.d(LOGTAG,account.getJid()+": sending: "+packet.toString()); + //Log.d(LOGTAG, account.getJid() + ": sending: " + packet.toString()); } - - public void sendMessagePacket(MessagePacket packet){ + + public void sendMessagePacket(MessagePacket packet) { tagWriter.writeElement(packet); } - - public void sendPresencePacket(PresencePacket packet) { + + public void sendPresencePacket(PresencePacket packet) { tagWriter.writeElement(packet); - Log.d(LOGTAG,account.getJid()+": sending: "+packet.toString()); + Log.d(LOGTAG, account.getJid() + ": sending: " + packet.toString()); } - - public void setOnMessagePacketReceivedListener(OnMessagePacketReceived listener) { + + public void setOnMessagePacketReceivedListener( + OnMessagePacketReceived listener) { this.messageListener = listener; } - - public void setOnUnregisteredIqPacketReceivedListener(OnIqPacketReceived listener) { + + public void setOnUnregisteredIqPacketReceivedListener( + OnIqPacketReceived listener) { this.unregisteredIqListener = listener; } - - public void setOnPresencePacketReceivedListener(OnPresencePacketReceived listener) { + + public void setOnPresencePacketReceivedListener( + OnPresencePacketReceived listener) { this.presenceListener = listener; } - + public void setOnStatusChangedListener(OnStatusChanged listener) { this.statusListener = listener; } |