/* * 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.provider; import java.io.File; import java.util.Random; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import org.kontalk.crypto.Coder; import org.kontalk.message.TextComponent; import org.kontalk.provider.MyMessages.Groups; import org.kontalk.provider.MyMessages.Messages; import org.kontalk.provider.MyMessages.Threads; import org.kontalk.service.msgcenter.group.KontalkGroupController; import org.kontalk.util.MessageUtils; import org.kontalk.util.Preferences; /** * Utility class for interacting with the {@link MessagesProvider}. * @author Daniele Ricci */ public class MessagesProviderUtils { private MessagesProviderUtils() { } /** Checks if the message lives. */ public static boolean exists(Context context, long msgId) { boolean b = false; Cursor c = context.getContentResolver(). query(ContentUris.withAppendedId(Messages.CONTENT_URI, msgId), null, null, null, null); if (c.moveToFirst()) b = true; c.close(); return b; } /** Inserts a new outgoing text message. */ public static Uri newOutgoingMessage(Context context, String msgId, String userId, String text, boolean encrypted) { byte[] bytes = text.getBytes(); ContentValues values = new ContentValues(11); // must supply a message ID... values.put(Messages.MESSAGE_ID, msgId); values.put(Messages.PEER, userId); values.put(Messages.BODY_MIME, TextComponent.MIME_TYPE); values.put(Messages.BODY_CONTENT, bytes); values.put(Messages.BODY_LENGTH, bytes.length); values.put(Messages.UNREAD, false); values.put(Messages.DIRECTION, Messages.DIRECTION_OUT); values.put(Messages.TIMESTAMP, System.currentTimeMillis()); values.put(Messages.STATUS, Messages.STATUS_SENDING); // of course outgoing messages are not encrypted in database values.put(Messages.ENCRYPTED, false); values.put(Threads.ENCRYPTION, encrypted); values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); return context.getContentResolver().insert( Messages.CONTENT_URI, values); } /** Inserts a new outgoing binary message. */ public static Uri newOutgoingMessage(Context context, String msgId, String userId, String mime, Uri uri, long length, int compress, File previewFile, boolean encrypted) { ContentValues values = new ContentValues(13); values.put(Messages.MESSAGE_ID, msgId); values.put(Messages.PEER, userId); /* TODO one day we'll ask for a text to send with the image values.put(Messages.BODY_MIME, TextComponent.MIME_TYPE); values.put(Messages.BODY_CONTENT, content.getBytes()); values.put(Messages.BODY_LENGTH, content.length()); */ values.put(Messages.UNREAD, false); // of course outgoing messages are not encrypted in database values.put(Messages.ENCRYPTED, false); values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); values.put(Messages.DIRECTION, Messages.DIRECTION_OUT); values.put(Messages.TIMESTAMP, System.currentTimeMillis()); values.put(Messages.STATUS, Messages.STATUS_QUEUED); if (previewFile != null) values.put(Messages.ATTACHMENT_PREVIEW_PATH, previewFile.getAbsolutePath()); values.put(Messages.ATTACHMENT_MIME, mime); values.put(Messages.ATTACHMENT_LOCAL_URI, uri.toString()); values.put(Messages.ATTACHMENT_LENGTH, length); values.put(Messages.ATTACHMENT_COMPRESS, compress); return context.getContentResolver().insert(Messages.CONTENT_URI, values); } /** Returns the thread associated with the given message. */ public static long getThreadByMessage(Context context, Uri message) { Cursor c = context.getContentResolver().query(message, new String[] { Messages.THREAD_ID }, null, null, null); try { if (c.moveToFirst()) return c.getLong(0); return Messages.NO_THREAD; } finally { c.close(); } } public static int updateDraft(Context context, long threadId, String draft) { ContentValues values = new ContentValues(1); if (draft != null && draft.length() > 0) values.put(Threads.DRAFT, draft); else values.putNull(Threads.DRAFT); return context.getContentResolver().update( ContentUris.withAppendedId(Threads.CONTENT_URI, threadId), values, null, null); } /** * Fills a media message with preview file and local uri, for use e.g. * after compressing. Also updates the message status to SENDING. */ public static int updateMedia(Context context, long id, String previewFile, Uri localUri, long length) { ContentValues values = new ContentValues(3); values.put(Messages.ATTACHMENT_PREVIEW_PATH, previewFile); values.put(Messages.ATTACHMENT_LOCAL_URI, localUri.toString()); values.put(Messages.ATTACHMENT_LENGTH, length); values.put(Messages.STATUS, Messages.STATUS_SENDING); return context.getContentResolver().update(ContentUris .withAppendedId(Messages.CONTENT_URI, id), values, null, null); } public static int deleteMessage(Context context, long id) { return context.getContentResolver().delete(ContentUris .withAppendedId(Messages.CONTENT_URI, id), null, null); } public static boolean deleteThread(Context context, long id, boolean keepGroup) { ContentResolver c = context.getContentResolver(); return (c.delete(ContentUris.withAppendedId(Threads.Conversations.CONTENT_URI, id) .buildUpon().appendQueryParameter(Messages.KEEP_GROUP, String.valueOf(keepGroup)) .build(), null, null) > 0); } public static int setThreadSticky(Context context, long id, boolean sticky) { ContentValues values = new ContentValues(1); values.put(Threads.STICKY, sticky); return context.getContentResolver().update( ContentUris.withAppendedId(Threads.CONTENT_URI, id), values, null, null); } /** Marks the given message as SENDING, regardless of its current status. */ public static int retryMessage(Context context, Uri uri, boolean encrypted) { ContentValues values = new ContentValues(2); values.put(Messages.STATUS, Messages.STATUS_SENDING); values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); return context.getContentResolver().update(uri, values, null, null); } /** Marks all pending messages to the given recipient as SENDING. */ public static int retryMessagesTo(Context context, String to) { Cursor c = context.getContentResolver().query(Messages.CONTENT_URI, new String[] { Messages._ID }, Messages.PEER + "=? AND " + Messages.STATUS + "=" + Messages.STATUS_PENDING, new String[] { to }, Messages._ID); while (c.moveToNext()) { long msgID = c.getLong(0); Uri msgURI = ContentUris.withAppendedId(Messages.CONTENT_URI, msgID); long threadID = getThreadByMessage(context, msgURI); if (threadID == Messages.NO_THREAD) continue; Uri threadURI = ContentUris.withAppendedId(Threads.CONTENT_URI, threadID); Cursor cThread = context.getContentResolver().query(threadURI, new String[] { Threads.ENCRYPTION }, null, null, null); if (cThread.moveToFirst()) { boolean encrypted = MessageUtils.sendEncrypted(context, cThread.getInt(0) != 0); retryMessage(context, msgURI, encrypted); } cThread.close(); } int count = c.getCount(); c.close(); return count; } /** Marks all pending messages as SENDING. */ public static int retryAllMessages(Context context) { boolean encrypted = Preferences.getEncryptionEnabled(context); ContentValues values = new ContentValues(2); values.put(Messages.STATUS, Messages.STATUS_SENDING); values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); return context.getContentResolver().update(Messages.CONTENT_URI, values, Messages.STATUS + "=" + Messages.STATUS_PENDING, null); } /** Inserts an empty thread (that is, with no messages). */ public static long insertEmptyThread(Context context, String peer, String draft) { ContentValues msgValues = new ContentValues(9); // must supply a message ID... msgValues.put(Messages.MESSAGE_ID, "draft" + (new Random().nextInt())); // use group id as the peer msgValues.put(Messages.PEER, peer); msgValues.put(Messages.BODY_CONTENT, new byte[0]); msgValues.put(Messages.BODY_LENGTH, 0); msgValues.put(Messages.BODY_MIME, TextComponent.MIME_TYPE); msgValues.put(Messages.DIRECTION, Messages.DIRECTION_OUT); msgValues.put(Messages.TIMESTAMP, System.currentTimeMillis()); msgValues.put(Messages.ENCRYPTED, false); if (draft != null) msgValues.put(Threads.DRAFT, draft); Uri newThread = context.getContentResolver().insert(Messages.CONTENT_URI, msgValues); return newThread != null ? ContentUris.parseId(newThread) : Messages.NO_THREAD; } public static long createGroupThread(Context context, String groupJid, String subject, String[] members, String draft) { // insert group ContentValues values = new ContentValues(); values.put(Groups.GROUP_JID, groupJid); // create new conversation long threadId = MessagesProviderUtils.insertEmptyThread(context, groupJid, draft); values.put(Groups.THREAD_ID, threadId); values.put(Groups.SUBJECT, subject); values.put(Groups.GROUP_TYPE, KontalkGroupController.GROUP_TYPE); context.getContentResolver().insert(Groups.CONTENT_URI, values); // remove values not for members table values.remove(Groups.GROUP_JID); values.remove(Groups.THREAD_ID); values.remove(Groups.SUBJECT); values.remove(Groups.GROUP_TYPE); // insert group members for (String member : members) { // FIXME turn this into batch operations values.put(Groups.PEER, member); context.getContentResolver() .insert(Groups.getMembersUri(groupJid), values); } return threadId; } public static void addGroupMembers(Context context, String groupJid, String[] members, boolean pending) { ContentValues values = new ContentValues(); values.put(Groups.GROUP_JID, groupJid); for (String member : members) { // FIXME turn this into batch operations values.put(Groups.PEER, member); values.put(Groups.PENDING, pending ? Groups.MEMBER_PENDING_ADDED : 0); context.getContentResolver() .insert(Groups.getMembersUri(groupJid), values); } } public static void removeGroupMembers(Context context, String groupJid, String[] members, boolean pending) { if (pending) { ContentValues values = new ContentValues(1); values.put(Groups.PENDING, Groups.MEMBER_PENDING_REMOVED); for (String member : members) { // FIXME turn this into batch operations context.getContentResolver() .update(Groups.getMembersUri(groupJid).buildUpon() .appendPath(member).build(), values, null, null); } } else { for (String member : members) { // just beat it! context.getContentResolver() .delete(Groups.getMembersUri(groupJid).buildUpon() .appendPath(member).build(), null, null); } } } public static int setGroupSubject(Context context, String groupJid, String subject) { ContentValues values = new ContentValues(); if (subject != null) values.put(Groups.SUBJECT, subject); else values.putNull(Groups.SUBJECT); return context.getContentResolver().update(Groups.getUri(groupJid), values, null, null); } public static boolean isGroupExisting(Context context, String groupJid) { Cursor c = context.getContentResolver().query( Groups.getUri(groupJid), new String[] { Groups.GROUP_JID }, null, null, null); boolean exist = c.moveToFirst(); c.close(); return exist; } public static String[] getGroupMembers(Context context, String groupJid, int flags) { String where; if (flags > 0) { where = "(" + Groups.PENDING + " & " + flags + ") = " + flags; } else if (flags == 0) { // handle zero flags special case (means all flags cleared) where = Groups.PENDING + "=0"; } else { // any flag where = null; } Cursor c = context.getContentResolver() .query(Groups.getMembersUri(groupJid), new String[] { Groups.PEER }, where, null, null); String[] members = new String[c.getCount()]; int i = 0; while (c.moveToNext()) { members[i++] = c.getString(0); } c.close(); return members; } public static int setGroupMembership(Context context, String groupJid, int membership) { ContentValues values = new ContentValues(1); values.put(Groups.MEMBERSHIP, membership); return context.getContentResolver().update(Groups.getUri(groupJid), values, null, null); } /** Returns the current known membership of a user in a group. */ public static boolean isGroupMember(Context context, String groupJid, String jid) { Cursor c = null; try { c = context.getContentResolver().query(Groups.getMembersUri(groupJid), new String[] { Groups.PENDING }, Groups.PEER + "=?", new String[] { jid }, null); return c.moveToNext() && c.getInt(0) == 0; } finally { if (c != null) c.close(); } } public static final class GroupThreadContent { public final String sender; public final String command; /** Parse thread content text for a special case: incoming group command. */ public GroupThreadContent(String sender, String command) { this.sender = sender; this.command = command; } public static GroupThreadContent parseIncoming(String content) { String[] parsed = content.split(";", 2); if (parsed.length < 2) { return new GroupThreadContent(null, content); } if (parsed[1].length() == 0) parsed[1] = null; return new GroupThreadContent(parsed[0], parsed[1]); } } public static int setEncryption(Context context, long threadId, boolean encryption) { ContentValues values = new ContentValues(1); values.put(Threads.ENCRYPTION, encryption); return context.getContentResolver().update( ContentUris.withAppendedId(Threads.CONTENT_URI, threadId), values, null, null); } }