/* * Kontalk Android client * Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org> * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk; import java.io.IOException; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDiskIOException; import android.net.Uri; import org.kontalk.authenticator.Authenticator; import org.kontalk.crypto.Coder; import org.kontalk.data.Conversation; import org.kontalk.message.AttachmentComponent; import org.kontalk.message.AudioComponent; import org.kontalk.message.CompositeMessage; import org.kontalk.message.GroupCommandComponent; import org.kontalk.message.GroupComponent; import org.kontalk.message.ImageComponent; import org.kontalk.message.MessageComponent; import org.kontalk.message.VCardComponent; import org.kontalk.provider.Keyring; import org.kontalk.provider.MessagesProviderUtils; import org.kontalk.provider.MyMessages; import org.kontalk.provider.MyUsers; import org.kontalk.provider.UsersProvider; import org.kontalk.service.DownloadService; import org.kontalk.service.MediaService; import org.kontalk.service.msgcenter.MessageCenterService; import org.kontalk.service.msgcenter.group.KontalkGroupController; import org.kontalk.ui.MessagingNotification; import org.kontalk.util.MessageUtils; import org.kontalk.util.Preferences; /** * Utility class to handle message transmission transactions. * @author Daniele Ricci */ public class MessagesController { private final Context mContext; MessagesController(Context context) { mContext = context; } public Uri sendTextMessage(Conversation conv, String text) { boolean encrypted = MessageUtils.sendEncrypted(mContext, conv.isEncryptionEnabled()); String msgId = MessageUtils.messageId(); String userId = conv.isGroupChat() ? conv.getGroupJid() : conv.getRecipient(); // save to local storage Uri newMsg = MessagesProviderUtils.newOutgoingMessage(mContext, msgId, userId, text, encrypted); if (newMsg != null) { // send message! if (conv.isGroupChat()) { MessageCenterService.sendGroupTextMessage(mContext, conv.getGroupJid(), conv.getGroupSubject(), conv.getGroupPeers(), text, encrypted, ContentUris.parseId(newMsg), msgId); } else { MessageCenterService.sendTextMessage(mContext, userId, text, encrypted, ContentUris.parseId(newMsg), msgId); } return newMsg; } else { throw new SQLiteDiskIOException(); } } public Uri sendBinaryMessage(Conversation conv, Uri uri, String mime, boolean media, Class<? extends MessageComponent<?>> klass) throws IOException { String msgId = MessageCenterService.messageId(); boolean encrypted = MessageUtils.sendEncrypted(mContext, conv.isEncryptionEnabled()); int compress = 0; if (klass == ImageComponent.class) { compress = Preferences.getImageCompression(mContext); } // save to database String userId = conv.isGroupChat() ? conv.getGroupJid() : conv.getRecipient(); Uri newMsg = MessagesProviderUtils.newOutgoingMessage(mContext, msgId, userId, mime, uri, 0, compress, null, encrypted); if (newMsg != null) { // prepare message and send (thumbnail, compression -> send) MediaService.prepareMessage(mContext, msgId, ContentUris.parseId(newMsg), uri, mime, media, compress); return newMsg; } else { throw new SQLiteDiskIOException(); } } /** * Set the trust level for the given key and, if the trust level is high * enough, send pending messages to the given user. * @return true if the trust level is high enough to retry messages */ public boolean setTrustLevelAndRetryMessages(Context context, String jid, String fingerprint, int trustLevel) { Keyring.setTrustLevel(context, jid, fingerprint, trustLevel); if (trustLevel >= MyUsers.Keys.TRUST_IGNORED) { MessageCenterService.retryMessagesTo(context, jid); return true; } return false; } private void addMembersInternal(GroupCommandComponent group, String[] members) { ContentValues membersValues = new ContentValues(); for (String member : members) { // do not add ourselves... if (Authenticator.isSelfJID(mContext, member)) { // ...but mark our membership MessagesProviderUtils.setGroupMembership(mContext, group.getContent().getJID(), MyMessages.Groups.MEMBERSHIP_MEMBER); continue; } // add member to group membersValues.put(MyMessages.Groups.PEER, member); mContext.getContentResolver().insert(MyMessages.Groups .getMembersUri(group.getContent().getJID()), membersValues); } } /** Process an incoming message. */ public Uri incoming(CompositeMessage msg) { final String sender = msg.getSender(true); // save to local storage ContentValues values = new ContentValues(); values.put(MyMessages.Messages.MESSAGE_ID, msg.getId()); values.put(MyMessages.Messages.PEER, sender); MessageUtils.fillContentValues(values, msg); GroupCommandComponent group = msg.getComponent(GroupCommandComponent.class); // notify for 1-to-1 messages and group creation and part group commands boolean notify = (group == null || group.isCreateCommand() || group.isPartCommand()); values.put(MyMessages.Messages.STATUS, msg.getStatus()); // group commands don't get notifications values.put(MyMessages.Messages.UNREAD, notify); values.put(MyMessages.Messages.NEW, notify); values.put(MyMessages.Messages.DIRECTION, MyMessages.Messages.DIRECTION_IN); values.put(MyMessages.Messages.TIMESTAMP, System.currentTimeMillis()); GroupComponent groupInfo = msg.getComponent(GroupComponent.class); if (groupInfo != null) { values.put(MyMessages.Groups.GROUP_JID, groupInfo.getContent().getJid()); values.put(MyMessages.Groups.GROUP_TYPE, KontalkGroupController.GROUP_TYPE); String groupSubject = groupInfo.getContent().getSubject(); if (groupSubject != null) values.put(MyMessages.Groups.SUBJECT, groupSubject); } if (group != null) { // the following operations will work because we are operating with // groups and group_members table directly (that is, no foreign keys) String[] added = null, removed = null, existing = null; // group creation if (group.isCreateCommand()) { added = group.getCreateMembers(); } // add/remove users else if (group.isAddOrRemoveCommand()) { added = group.getAddedMembers(); removed = group.getRemovedMembers(); existing = group.getExistingMembers(); } if (added != null) { ContentValues membersValues = new ContentValues(); if (added.length > 0) { addMembersInternal(group, added); } if (existing != null && existing.length > 0) { addMembersInternal(group, existing); } // add owner as member (since the owner is adding us) membersValues.put(MyMessages.Groups.PEER, group.getContent().getOwner()); mContext.getContentResolver().insert(MyMessages.Groups .getMembersUri(group.getContent().getJID()), membersValues); } if (removed != null) { // remove members from group MessagesProviderUtils.removeGroupMembers(mContext, group.getContent().getJID(), removed, false); // set our membership to parted if we were removed from the group for (String removedJid : removed) { if (Authenticator.isSelfJID(mContext, removedJid)) { MessagesProviderUtils.setGroupMembership(mContext, group.getContent().getJID(), MyMessages.Groups.MEMBERSHIP_KICKED); break; } } } // set subject if (group.isSetSubjectCommand()) { ContentValues groupValues = new ContentValues(); groupValues.put(MyMessages.Groups.SUBJECT, group.getContent().getSubject()); mContext.getContentResolver().update(MyMessages.Groups .getUri(group.getContent().getJID()), groupValues, null, null); } // a user is leaving the group else if (group.isPartCommand()) { String partMember = group.getFrom(); // remove member from group mContext.getContentResolver().delete(MyMessages.Groups.getMembersUri(group.getContent().getJID()) .buildUpon().appendEncodedPath(partMember).build(), null, null); } } Uri msgUri = null; try { msgUri = mContext.getContentResolver().insert(MyMessages.Messages.CONTENT_URI, values); } catch (SQLiteConstraintException econstr) { // duplicated message, skip it } if (groupInfo == null) { // mark sender as registered in the users database final Context context = mContext.getApplicationContext(); new Thread(new Runnable() { public void run() { try { UsersProvider.markRegistered(context, sender); } catch (SQLiteConstraintException e) { // this might happen during an online/offline switch } } }).start(); } // fire notification only if message was actually inserted to database // and the conversation is not open already String paused = groupInfo != null ? groupInfo.getContent().getJid() : sender; if (notify && msgUri != null && !MessagingNotification.isPaused(paused)) { // update notifications (delayed) MessagingNotification.delayedUpdateMessagesNotification(mContext.getApplicationContext(), true); } // check if we need to autodownload @SuppressWarnings("unchecked") Class<AttachmentComponent>[] tryComponents = new Class[] { ImageComponent.class, VCardComponent.class, AudioComponent.class, }; for (Class<AttachmentComponent> klass : tryComponents) { AttachmentComponent att = msg.getComponent(klass); if (att != null && att.getFetchUrl() != null && Preferences.canAutodownloadMedia(mContext, att.getLength())) { long databaseId = ContentUris.parseId(msgUri); DownloadService.start(mContext, databaseId, sender, att.getMime(), msg.getTimestamp(), att.getSecurityFlags() != Coder.SECURITY_CLEARTEXT, att.getFetchUrl(), false); // only one attachment is supported break; } } return msgUri; } }