/* * 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.data; import java.util.Collections; import java.util.HashSet; import java.util.Set; import android.content.AsyncQueryHandler; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; import org.kontalk.provider.KontalkGroupCommands; import org.kontalk.provider.MessagesProvider; import org.kontalk.provider.MessagesProviderUtils; import org.kontalk.provider.MyMessages.Groups; import org.kontalk.provider.MyMessages.Messages; import org.kontalk.provider.MyMessages.Threads; import org.kontalk.service.msgcenter.MessageCenterService; import org.kontalk.service.msgcenter.group.GroupControllerFactory; import org.kontalk.ui.MessagingNotification; import org.kontalk.util.MessageUtils; /** * A class represeting a conversation thread. * @author Daniele Ricci */ public class Conversation { private static final String[] ALL_THREADS_PROJECTION = { Threads._ID, Threads.PEER, Threads.COUNT, Threads.UNREAD, Threads.MIME, Threads.CONTENT, Threads.TIMESTAMP, Threads.STATUS, Threads.ENCRYPTED, Threads.DRAFT, Threads.REQUEST_STATUS, Threads.STICKY, Threads.ENCRYPTION, Groups.GROUP_JID, Groups.SUBJECT, Groups.GROUP_TYPE, Groups.MEMBERSHIP, }; private static final int COLUMN_ID = 0; private static final int COLUMN_PEER = 1; private static final int COLUMN_COUNT = 2; private static final int COLUMN_UNREAD = 3; private static final int COLUMN_MIME = 4; private static final int COLUMN_CONTENT = 5; private static final int COLUMN_TIMESTAMP = 6; private static final int COLUMN_STATUS = 7; private static final int COLUMN_ENCRYPTED = 8; private static final int COLUMN_DRAFT = 9; private static final int COLUMN_REQUEST_STATUS = 10; private static final int COLUMN_STICKY = 11; private static final int COLUMN_ENCRYPTION = 12; private static final int COLUMN_GROUP_JID = 13; private static final int COLUMN_GROUP_SUBJECT = 14; private static final int COLUMN_GROUP_TYPE = 15; private static final int COLUMN_GROUP_MEMBERSHIP = 16; @SuppressWarnings("WeakerAccess") final Context mContext; @SuppressWarnings("WeakerAccess") long mThreadId; private Contact mContact; // for group chats it will be the group JID private String mRecipient; private long mDate; private int mMessageCount; private String mMime; private String mSubject; private int mUnreadCount; private int mStatus; private String mDraft; private String mNumberHint; private boolean mEncrypted; private int mRequestStatus; private boolean mSticky; // set encryption disabled for this chat private boolean mEncryption; // from groups table private String mGroupJid; private String[] mGroupPeers; private String mGroupSubject; private String mGroupType; private int mGroupMembership; private Conversation(Context context) { mContext = context; mThreadId = 0; mEncryption = true; } private Conversation(Context context, Cursor c) { mContext = context; synchronized (this) { mThreadId = c.getLong(COLUMN_ID); mDate = c.getLong(COLUMN_TIMESTAMP); mRecipient = c.getString(COLUMN_PEER); mMime = c.getString(COLUMN_MIME); mSubject = c.getString(COLUMN_CONTENT); mUnreadCount = c.getInt(COLUMN_UNREAD); mMessageCount = c.getInt(COLUMN_COUNT); mStatus = c.getInt(COLUMN_STATUS); mEncrypted = c.getInt(COLUMN_ENCRYPTED) != 0; mDraft = c.getString(COLUMN_DRAFT); mRequestStatus = c.getInt(COLUMN_REQUEST_STATUS); mSticky = c.getInt(COLUMN_STICKY) != 0; mEncryption = c.getInt(COLUMN_ENCRYPTION) != 0; mGroupJid = c.getString(COLUMN_GROUP_JID); mGroupSubject = c.getString(COLUMN_GROUP_SUBJECT); mGroupType = c.getString(COLUMN_GROUP_TYPE); mGroupMembership = c.getInt(COLUMN_GROUP_MEMBERSHIP); // group peers are loaded on demand loadContact(); } } public static Conversation createNew(Context context) { return new Conversation(context); } public static Conversation createFromCursor(Context context, Cursor cursor) { return new Conversation(context, cursor); } public static Conversation loadFromUserId(Context context, String userId) { Conversation cv = null; Cursor cp = context.getContentResolver().query(Threads.CONTENT_URI, ALL_THREADS_PROJECTION, Threads.PEER + " = ?", new String[] { userId }, null); if (cp.moveToFirst()) cv = createFromCursor(context, cp); cp.close(); return cv; } public static Conversation loadFromId(Context context, long id) { Conversation cv = null; Cursor cp = context.getContentResolver().query( ContentUris.withAppendedId(Threads.CONTENT_URI, id), ALL_THREADS_PROJECTION, null, null, null); if (cp.moveToFirst()) cv = createFromCursor(context, cp); cp.close(); return cv; } public static long getMessageId(Cursor cursor) { return cursor.getLong(COLUMN_ID); } public static String getPeer(Cursor cursor) { return cursor.getString(COLUMN_PEER); } public static boolean isGroup(Cursor cursor, int requiredMembership) { return cursor.getString(COLUMN_GROUP_JID) != null && cursor.getInt(COLUMN_GROUP_MEMBERSHIP) == requiredMembership; } public static void deleteFromCursor(Context context, Cursor cursor, boolean leaveGroup) { String groupJid = cursor.getString(COLUMN_GROUP_JID); String[] groupPeers = null; String groupType = null; if (groupJid != null) { groupType = cursor.getString(COLUMN_GROUP_TYPE); groupPeers = loadGroupPeersInternal(context, groupJid); } boolean encrypted = MessageUtils.sendEncrypted(context, cursor.getInt(COLUMN_ENCRYPTED) != 0); deleteInternal(context, cursor.getLong(COLUMN_ID), groupJid, groupPeers, groupType, leaveGroup, encrypted); } public static void deleteAll(Context context, boolean leaveGroups) { Cursor c = context.getContentResolver().query(Threads.CONTENT_URI, ALL_THREADS_PROJECTION, null, null, null); while (c.moveToNext()) { deleteFromCursor(context, c, leaveGroups); } c.close(); } private void loadContact() { if (isGroupChat()) mContact = null; else mContact = Contact.findByUserId(mContext, mRecipient, mNumberHint); } public Contact getContact() { return mContact; } public long getDate() { return mDate; } public void setDate(long date) { this.mDate = date; } public String getMime() { return mMime; } public String getSubject() { return mSubject; } public String getRecipient() { return mRecipient; } public void setRecipient(String recipient) { mRecipient = recipient; // reload contact loadContact(); } public int getMessageCount() { return mMessageCount; } public int getUnreadCount() { return mUnreadCount; } public long getThreadId() { return mThreadId; } public int getStatus() { return mStatus; } public boolean isEncrypted() { return mEncrypted; } public String getDraft() { return mDraft; } public int getRequestStatus() { return mRequestStatus; } public boolean isSticky() { return mSticky; } public String getNumberHint() { return mNumberHint; } public boolean isEncryptionEnabled() { return mEncryption; } /** * Sets a phone number hint that will be used if there is no match in the * users database. */ public void setNumberHint(String numberHint) { mNumberHint = numberHint; } public String getGroupJid() { return mGroupJid; } public String[] getGroupPeers() { return getGroupPeers(false); } public String[] getGroupPeers(boolean force) { loadGroupPeers(force); return mGroupPeers; } public boolean isGroupChat() { loadGroupPeers(false); return mGroupJid != null; } public String getGroupSubject() { return mGroupSubject; } public int getGroupMembership() { return mGroupMembership; } public void cancelGroupChat() { mGroupJid = null; mGroupPeers = null; } public void leaveGroup() { // it makes sense to leave a group if we have someone to tell about it loadGroupPeers(false); if (mGroupJid != null) { boolean encrypted = MessageUtils.sendEncrypted(mContext, mEncryption); // send the command if there is someone to talk to boolean actuallySend = GroupControllerFactory.canSendCommandsWithEmptyGroup(mGroupType) || getGroupPeers().length > 0; String msgId = MessageCenterService.messageId(); Uri cmdMsg = KontalkGroupCommands .leaveGroup(mContext, mThreadId, mGroupJid, msgId, encrypted, !actuallySend); // TODO check for null // mark group as parted MessagesProviderUtils.setGroupMembership(mContext, mGroupJid, Groups.MEMBERSHIP_PARTED); if (actuallySend) { MessageCenterService.leaveGroup(mContext, mGroupJid, mGroupPeers, encrypted, ContentUris.parseId(cmdMsg), msgId); } } } public void delete(boolean leaveGroup) { loadGroupPeers(false); deleteInternal(mContext, mThreadId, mGroupJid, mGroupPeers, mGroupType, leaveGroup, mEncryption); } private static void deleteInternal(Context context, long threadId, String groupJid, String[] groupPeers, String groupType, boolean leaveGroup, boolean encrypted) { // it makes sense to leave a group if we have someone to tell about it boolean groupChat = groupJid != null; boolean actuallySend = groupChat && (GroupControllerFactory.canSendCommandsWithEmptyGroup(groupType) || groupPeers.length > 0); boolean groupCreateSent = false; if (groupChat && actuallySend && leaveGroup) { // retrieve status of the group creation message // otherwise don't send the leave message at all groupCreateSent = KontalkGroupCommands.isGroupCreatedSent(context, threadId); } // delete messages and thread MessagesProviderUtils.deleteThread(context, threadId, groupChat && !leaveGroup); // send leave message only if the group was created in the first place if (groupChat && leaveGroup) { if (groupCreateSent) { String msgId = MessageCenterService.messageId(); Uri cmdMsg = KontalkGroupCommands .leaveGroup(context, Messages.NO_THREAD, groupJid, msgId, encrypted, false); // TODO check for null MessageCenterService.leaveGroup(context, groupJid, groupPeers, encrypted, ContentUris.parseId(cmdMsg), msgId); } else { // delete group immediately (members will cascade) context.getContentResolver() .delete(Groups.getUri(groupJid), null, null); } } } public void setSticky(boolean sticky) { mSticky = sticky; if (mThreadId > 0) MessagesProviderUtils.setThreadSticky(mContext, mThreadId, sticky); } private void loadGroupPeers(boolean force) { if (mGroupJid != null && (mGroupPeers == null || force)) { mGroupPeers = loadGroupPeersInternal(mContext, mGroupJid); } } private static String[] loadGroupPeersInternal(Context context, String groupJid) { return MessagesProviderUtils.getGroupMembers(context, groupJid, 0); } public static void startQuery(AsyncQueryHandler handler, int token) { // cancel previous operations handler.cancelOperation(token); handler.startQuery(token, null, Threads.CONTENT_URI, ALL_THREADS_PROJECTION, null, null, Threads.DEFAULT_SORT_ORDER); } public static void startQuery(AsyncQueryHandler handler, int token, long threadId) { // cancel previous operations handler.cancelOperation(token); handler.startQuery(token, null, Threads.CONTENT_URI, ALL_THREADS_PROJECTION, Threads._ID + " = " + threadId, null, null); } public static Cursor startQuery(Context context) { return context.getContentResolver().query(Threads.CONTENT_URI, ALL_THREADS_PROJECTION, null, null, Threads.DEFAULT_SORT_ORDER); } public static Cursor startQuery(Context context, long threadId) { return context.getContentResolver().query(Threads.CONTENT_URI, ALL_THREADS_PROJECTION, Threads._ID + " = " + threadId, null, null); } /** * Creates a new group chat. * @return a newly created thread ID. */ public static long initGroupChat(Context context, String groupJid, String subject, String[] members, String draft) { return MessagesProviderUtils.createGroupThread(context, groupJid, subject, members, draft); } public void addUsers(String[] members) { if (!isGroupChat()) throw new UnsupportedOperationException("Not a group chat conversation"); // add members to the group MessagesProviderUtils.addGroupMembers(mContext, mGroupJid, members, true); // store add group member command to outbox boolean encrypted = MessageUtils.sendEncrypted(mContext, mEncryption); String msgId = MessageCenterService.messageId(); Uri cmdMsg = KontalkGroupCommands .addGroupMembers(mContext, getThreadId(), mGroupJid, members, msgId, encrypted); // TODO check for null // send add group member command now Set<String> allMembers = new HashSet<>(); Collections.addAll(allMembers, getGroupPeers()); Collections.addAll(allMembers, members); MessageCenterService.addGroupMembers(mContext, mGroupJid, mGroupSubject, allMembers.toArray(new String[allMembers.size()]), members, encrypted, ContentUris.parseId(cmdMsg), msgId); } public void removeUsers(String[] members) { if (!isGroupChat()) throw new UnsupportedOperationException("Not a group chat conversation"); // remove members to the group MessagesProviderUtils.removeGroupMembers(mContext, mGroupJid, members, true); // store remove group member command to outbox boolean encrypted = MessageUtils.sendEncrypted(mContext, mEncryption); String msgId = MessageCenterService.messageId(); Uri cmdMsg = KontalkGroupCommands .removeGroupMembers(mContext, getThreadId(), mGroupJid, members, msgId, encrypted); // TODO check for null // send add group member command now MessageCenterService.removeGroupMembers(mContext, mGroupJid, mGroupSubject, getGroupPeers(), members, encrypted, ContentUris.parseId(cmdMsg), msgId); } public void setGroupSubject(String subject) { if (!isGroupChat()) throw new UnsupportedOperationException("Not a group chat conversation"); // set group subject MessagesProviderUtils.setGroupSubject(mContext, mGroupJid, subject); // send the command if there is someone to talk to boolean actuallySend = GroupControllerFactory.canSendCommandsWithEmptyGroup(mGroupType) || getGroupPeers().length > 0; // store set group subject command to outbox boolean encrypted = MessageUtils.sendEncrypted(mContext, mEncryption); String msgId = MessageCenterService.messageId(); Uri cmdMsg = KontalkGroupCommands .setGroupSubject(mContext, getThreadId(), mGroupJid, subject, msgId, encrypted, !actuallySend); // TODO check for null if (actuallySend) { // send set group subject command now String[] currentMembers = getGroupPeers(); MessageCenterService.setGroupSubject(mContext, mGroupJid, subject, currentMembers, encrypted, ContentUris.parseId(cmdMsg), msgId); } } public void setEncryptionEnabled(boolean encryptionEnabled) { mEncryption = encryptionEnabled; if (mThreadId > 0) MessagesProviderUtils.setEncryption(mContext, mThreadId, encryptionEnabled); } public void markAsRead() { if (mThreadId > 0) { new Thread(new Runnable() { @Override public void run() { MessagesProvider.markThreadAsRead(mContext, mThreadId); MessagingNotification.updateMessagesNotification(mContext.getApplicationContext(), false); } }).start(); } } }