From 6be0d241c3888a9e30a027a664d6a9a4fdb3d6ad Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 17 Dec 2017 17:13:13 +0100 Subject: [PATCH] Fetch conversation messages remotely --- .idea/misc.xml | 2 +- .../conversations/ConversationMessage.java | 20 +- .../ConversationMessagesDbHelper.java | 228 ++++++++++++++++++ .../ConversationMessagesHelper.java | 181 ++++++++++++++ .../ConversationRefreshRunnable.java | 163 +++++++++++++ .../fragments/ConversationFragment.java | 52 +++- 6 files changed, 640 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessagesDbHelper.java create mode 100644 app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessagesHelper.java create mode 100644 app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationRefreshRunnable.java diff --git a/.idea/misc.xml b/.idea/misc.xml index 503aca7..33952c6 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -55,7 +55,7 @@ - + diff --git a/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessage.java b/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessage.java index bf720dd..fee1a7e 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessage.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessage.java @@ -1,5 +1,6 @@ package org.communiquons.android.comunic.client.data.conversations; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.Objects; @@ -46,7 +47,7 @@ public class ConversationMessage { * * @param conversation_id The ID of the conversation */ - public void setConversation_id(int conversation_id) { + void setConversation_id(int conversation_id) { this.conversation_id = conversation_id; } @@ -64,7 +65,7 @@ public class ConversationMessage { * * @param user_id The ID of the user */ - public void setUser_id(int user_id) { + void setUser_id(int user_id) { this.user_id = user_id; } @@ -99,6 +100,19 @@ public class ConversationMessage { return image_path; } + /** + * Get the path of the image associated with the content + * + * Warning ! if no image path were specified, "null" will be returned, but as a string instead + * of an empty pointer + * + * @return The path of the image + */ + @NonNull + public String getImagePathNotNull() { + return image_path != null ? image_path : "null"; + } + /** * Set the content of the message * @@ -122,7 +136,7 @@ public class ConversationMessage { * * @param time_insert The time of insertion of the message */ - public void setTime_insert(int time_insert) { + void setTime_insert(int time_insert) { this.time_insert = time_insert; } diff --git a/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessagesDbHelper.java b/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessagesDbHelper.java new file mode 100644 index 0000000..9993149 --- /dev/null +++ b/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessagesDbHelper.java @@ -0,0 +1,228 @@ +package org.communiquons.android.comunic.client.data.conversations; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.communiquons.android.comunic.client.data.DatabaseHelper; +import org.communiquons.android.comunic.client.data.DatabaseContract.ConversationsMessagesSchema; + +import java.util.ArrayList; + +/** + * Conversation messages database helper + * + * @author Pierre HUBERT + * Created by pierre on 12/16/17. + */ + +class ConversationMessagesDbHelper { + + /** + * Debug tag + */ + private static final String TAG = "ConversationMessagesDbH"; + + /** + * Database helper object + */ + private DatabaseHelper dbHelper; + + /** + * Conversations messages table name + */ + private static final String TABLE_NAME = ConversationsMessagesSchema.TABLE_NAME; + + /** + * Conversation messages table column + */ + private final String[] columns = { + ConversationsMessagesSchema.COLUMN_NAME_MESSAGE_ID, + ConversationsMessagesSchema.COLUMN_NAME_CONVERSATION_ID, + ConversationsMessagesSchema.COLUMN_NAME_USER_ID, + ConversationsMessagesSchema.COLUMN_NAME_IMAGE_PATH, + ConversationsMessagesSchema.COLUMN_NAME_MESSAGE, + ConversationsMessagesSchema.COLUMN_NAME_TIME_INSERT + }; + + /** + * Class constructor + * + * @param dbHelper Database helper + */ + ConversationMessagesDbHelper(@NonNull DatabaseHelper dbHelper){ + this.dbHelper = dbHelper; + } + + + /** + * Get the last message stored of a conversation + * + * @param conversationID The ID of the target conversation + * @return The last message of the conversation or null if no message were found + */ + @Nullable + ConversationMessage getLast(int conversationID){ + + ConversationMessage message = null; + + SQLiteDatabase db = dbHelper.getReadableDatabase(); + + //Prepare the query on the database + String selection = ConversationsMessagesSchema.COLUMN_NAME_CONVERSATION_ID + " = ?"; + String[] selectionArgs = {""+conversationID}; + String orderBy = ConversationsMessagesSchema.COLUMN_NAME_MESSAGE_ID + " DESC"; + String limit = "1"; + + //Perform the request + Cursor response = db.query(TABLE_NAME, columns, selection, selectionArgs, null, + null, orderBy, limit); + + //Check for response + if(response.getCount() != 0){ + response.moveToFirst(); + message = getMessageFromCursorPos(response); + } + + response.close(); + db.close(); + + return message; + } + + /** + * Insert a list of messages into the database + * + * @param list The list of messages to insert + * @return The result of the operation + */ + boolean insertMultiple(ArrayList list){ + + SQLiteDatabase db = dbHelper.getWritableDatabase(); + + boolean success = true; + + //Process the list of messages + for(ConversationMessage message : list){ + + //Try to insert the message + if(!insertOne(db, message)) + success = false; + + } + + db.close(); + + return success; + } + + /** + * Get an interval of messages from the database + * + * @param conv The conversation ID + * @param start The ID of the oldest message to fetch + * @param end The ID of the newest message to fetch + * @return The list of messages | null in case of failure + */ + ArrayList getInterval(int conv, int start, int end){ + + SQLiteDatabase db = dbHelper.getReadableDatabase(); + + //Perform a request on the database + String selection = ConversationsMessagesSchema.COLUMN_NAME_CONVERSATION_ID + " = ? AND " + + " " + ConversationsMessagesSchema.COLUMN_NAME_MESSAGE_ID + " >= ? AND " + + ConversationsMessagesSchema.COLUMN_NAME_MESSAGE_ID + " <= ?"; + String[] selectionArgs = { + ""+conv, + ""+start, + ""+end + }; + String order = ConversationsMessagesSchema.COLUMN_NAME_MESSAGE_ID; + Cursor cur = db.query(TABLE_NAME, columns, selection, selectionArgs, + null, null, order); + + //Process each response + ArrayList list = new ArrayList<>(); + cur.moveToFirst(); + while(!cur.isAfterLast()){ + list.add(getMessageFromCursorPos(cur)); + cur.moveToNext(); + } + + //Close objects + cur.close(); + db.close(); + + return list; + } + + /** + * Insert a single message into the database + * + * @param db Database object (with writeable access) + * @param message The message to insert into the database + * @return TRUE in case of success / FALSE else + */ + private boolean insertOne(SQLiteDatabase db, ConversationMessage message){ + + //Perform the query + return db.insert(TABLE_NAME, null, getContentValues(message)) != -1; + + } + + /** + * Fill a message object based on a current cursor positin + * + * @param cursor The response cursor + * @return ConversationMessage object + */ + private ConversationMessage getMessageFromCursorPos(Cursor cursor){ + + ConversationMessage message = new ConversationMessage(); + + //Query database result + message.setId(cursor.getInt(cursor.getColumnIndexOrThrow( + ConversationsMessagesSchema.COLUMN_NAME_MESSAGE_ID))); + + message.setConversation_id(cursor.getInt(cursor.getColumnIndexOrThrow( + ConversationsMessagesSchema.COLUMN_NAME_CONVERSATION_ID))); + + message.setUser_id(cursor.getInt(cursor.getColumnIndexOrThrow( + ConversationsMessagesSchema.COLUMN_NAME_USER_ID))); + + message.setImage_path(cursor.getString(cursor.getColumnIndexOrThrow( + ConversationsMessagesSchema.COLUMN_NAME_IMAGE_PATH))); + + message.setContent(cursor.getString(cursor.getColumnIndexOrThrow( + ConversationsMessagesSchema.COLUMN_NAME_MESSAGE))); + + message.setTime_insert(cursor.getInt(cursor.getColumnIndexOrThrow( + ConversationsMessagesSchema.COLUMN_NAME_TIME_INSERT))); + + return message; + } + + /** + * Generate a ContentValue from a message object + * + * @param message The message to convert + * @return The generated ContentValues + */ + private ContentValues getContentValues(ConversationMessage message){ + + //Generate the ContentValues and return it + ContentValues cv = new ContentValues(); + cv.put(ConversationsMessagesSchema.COLUMN_NAME_MESSAGE_ID, message.getId()); + cv.put(ConversationsMessagesSchema.COLUMN_NAME_CONVERSATION_ID, + message.getConversation_id()); + cv.put(ConversationsMessagesSchema.COLUMN_NAME_USER_ID, message.getUser_id()); + cv.put(ConversationsMessagesSchema.COLUMN_NAME_IMAGE_PATH, message.getImagePathNotNull()); + cv.put(ConversationsMessagesSchema.COLUMN_NAME_MESSAGE, message.getContent()); + cv.put(ConversationsMessagesSchema.COLUMN_NAME_TIME_INSERT, message.getTime_insert()); + return cv; + + } +} diff --git a/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessagesHelper.java b/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessagesHelper.java new file mode 100644 index 0000000..e590af5 --- /dev/null +++ b/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationMessagesHelper.java @@ -0,0 +1,181 @@ +package org.communiquons.android.comunic.client.data.conversations; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.communiquons.android.comunic.client.api.APIRequest; +import org.communiquons.android.comunic.client.api.APIRequestParameters; +import org.communiquons.android.comunic.client.api.APIResponse; +import org.communiquons.android.comunic.client.data.DatabaseHelper; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +/** + * Conversation messages helper + * + * @author Pierre HUBERT + * Created by pierre on 12/16/17. + */ + +public class ConversationMessagesHelper { + + /** + * Debug tag + */ + private static final String TAG = "ConversationMessagesHel"; + + /** + * Conversations messages database helper + */ + private ConversationMessagesDbHelper mDbHelper; + + /** + * Context of execution of the application + */ + private Context mContext; + + /** + * Public constructor of the helper + * + * @param context The context of execution of the application + * @param dbHelper Database helper associated with the context + */ + public ConversationMessagesHelper(Context context, DatabaseHelper dbHelper){ + mContext = context; + mDbHelper = new ConversationMessagesDbHelper(dbHelper); + } + + /** + * Get the latest messages of a conversation + * + * @param conversation_id The ID of the conversation to refresh + * @return The ID of the last message available in the database + */ + int refresh_conversation(int conversation_id){ + + //Get the ID of the last message available in the database + int last_message_id = getLastIDFromDb(conversation_id); + + //Perform a request on the database + ArrayList new_messages = downloadNew(conversation_id, last_message_id); + + //Check for errors + if(new_messages == null){ + //An error occurred + return -1; + } + + //Add the new messages to the database (if any) + if(new_messages.size() > 0) { + mDbHelper.insertMultiple(new_messages); + } + + //Get the last message ID from database again + return getLastIDFromDb(conversation_id); + } + + /** + * Fetch messages from the database + * + * @param conv Conversation ID + * @param start The ID of the oldest message to fetch + * @param end The ID of the last message to fetch + * @return The message of the interval, or null in case of failure + */ + @Nullable + ArrayList getInDb(int conv, int start, int end){ + + return mDbHelper.getInterval(conv, start, end); + + } + + /** + * Get the ID of the last message of the conversation from the database + * + * @param conversation_id Target conversation + * @return The ID of the last message available in the database or 0 in case of failure + */ + private int getLastIDFromDb(int conversation_id){ + + //Get the id of the last message available in the database + ConversationMessage last_message = mDbHelper.getLast(conversation_id); + + //Check if there isn't any message + if(last_message == null) + return 0; //There is no message in the database yet + + //Return the ID of the last message available + else + return last_message.getId(); + } + + /** + * Download the latest messages available in the API + * + * @param conversationID The ID of the target conversation + * @param last_message_id The ID of the last known message (0 for none) + * @return null in case of failure, an empty array if there is no new messages available of the + * list of new messages for the specified conversation + */ + @Nullable + private ArrayList downloadNew(int conversationID, int last_message_id){ + + //Prepare a request on the API + APIRequestParameters params = new APIRequestParameters(mContext, + "conversations/refresh_single"); + params.addParameter("conversationID", ""+conversationID); + params.addParameter("last_message_id", ""+last_message_id); + + ArrayList list = new ArrayList<>(); + + try { + //Perform the request + APIResponse response = new APIRequest().exec(params); + + //Get the list of new messages + JSONArray messages = response.getJSONArray(); + + for(int i = 0; i < messages.length(); i++){ + + //Convert the message into a message object + list.add(getMessageObject(conversationID, messages.getJSONObject(i))); + + } + + } catch (Exception e){ + Log.e(TAG, "Couldn't refresh the list of messages!"); + e.printStackTrace(); + return null; + } + + return list; + } + + /** + * Convert a JSON object into a conversation message element + * + * @param convID The ID of the current conversation + * @param obj The target object + * @return Generation Conversation Message object + * @throws JSONException If the JSON objected couldn't be decoded + */ + private ConversationMessage getMessageObject(int convID, JSONObject obj) throws JSONException{ + + ConversationMessage message = new ConversationMessage(); + + //Get the message values + message.setId(obj.getInt("ID")); + message.setConversation_id(convID); + message.setUser_id(obj.getInt("ID_user")); + message.setImage_path(obj.getString("image_path")); + message.setContent(obj.getString("message")); + message.setTime_insert(obj.getInt("time_insert")); + + return message; + + } +} diff --git a/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationRefreshRunnable.java b/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationRefreshRunnable.java new file mode 100644 index 0000000..d707d4a --- /dev/null +++ b/app/src/main/java/org/communiquons/android/comunic/client/data/conversations/ConversationRefreshRunnable.java @@ -0,0 +1,163 @@ +package org.communiquons.android.comunic.client.data.conversations; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Runnable that refresh in the background the list of messages + * + * @author Pierre HUBERT + * Created by pierre on 12/16/17. + */ + +public class ConversationRefreshRunnable implements Runnable { + + /** + * Debug tag + */ + private String TAG = "ConversationRefreshRunn"; + + /** + * The ID of the conversation + */ + private int conversation_id; + + /** + * The ID of the last message available + */ + private int last_message_id; + + /** + * Conversation message helper + */ + private ConversationMessagesHelper convMessHelper; + + /** + * Set to true to make the thread exit + */ + private boolean quit = false; + + /** + * Object that helps to make breaks between refreshes + */ + private final Object object = new Object(); + + /** + * Create a new conversation refresh runnable + * + * @param conversation_id The ID of the conversation + * @param last_message_id The ID of the last message already present in the list (set to 0 + * for no message) + * @param conversationMessagesHelper Conversation message helper to get access to the database + * and to be able to query the API through helper + */ + public ConversationRefreshRunnable(int conversation_id, int last_message_id, + @NonNull ConversationMessagesHelper conversationMessagesHelper){ + this.conversation_id = conversation_id; + this.last_message_id = last_message_id; + this.convMessHelper = conversationMessagesHelper; + } + + /** + * onMessagesChangeListener + * + * This interface is used to perform callback actions on the UI Thread to add messages + * to a list for example + * + * This method also changes in the conversations + */ + public interface onMessagesChangeListener { + + /** + * Add new messages to a previous list of messages + * + * @param messages The new messagess + */ + void onAddMessage(@NonNull ArrayList messages); + + /** + * This method is called when there is not any message in the conversation + * + * Warning ! This method may be called several time + */ + void onNoMessage(); + + /** + * This method is called when an error occur on a request on the database and / or on the + * remote server + */ + void onError(); + + } + + @Override + public void run() { + //Log action + Log.v(TAG, "Started conversation refresh runnable."); + + synchronized (object) { + //Loop that execute indefinitely until the fragment is stopped + while (!quit) { + + //Refresh the list of message from the server - the function return the ID of the last + // message available + int lastMessageInDb = convMessHelper.refresh_conversation(conversation_id); + + //If the last message in the database is newer than the last message of the caller + if (lastMessageInDb > last_message_id) { + + //Fetch all the messages available in the database since the last request + ArrayList newMessages = convMessHelper.getInDb( + conversation_id, + last_message_id + 1, + lastMessageInDb + ); + + //Check for errors + if(newMessages == null){ + + //Callback : an error occurred. + Log.e(TAG, "Couldn't get the list of new messages from local database !"); + + } + else { + //Use the callback to send the messages to the UI thread + for(ConversationMessage message: newMessages) + Log.v(TAG, "Message: " + message.getContent()); + + //Update the ID of the last message fetched + last_message_id = lastMessageInDb; + } + + } + + if(lastMessageInDb == -1){ + + //Callback : an error occurred + Log.e(TAG, "Couldn't get the list of new messages !"); + + } + + //Make a small break + try { + object.wait(500); + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } + } + } + + Log.v(TAG, "Stopped conversation refresh runnable."); + } + + /** + * Make the thread quit safely (does not interrupt currently running operation) + */ + public void quitSafely(){ + quit = true; + } +} diff --git a/app/src/main/java/org/communiquons/android/comunic/client/fragments/ConversationFragment.java b/app/src/main/java/org/communiquons/android/comunic/client/fragments/ConversationFragment.java index 2140c7a..a3933c9 100644 --- a/app/src/main/java/org/communiquons/android/comunic/client/fragments/ConversationFragment.java +++ b/app/src/main/java/org/communiquons/android/comunic/client/fragments/ConversationFragment.java @@ -9,6 +9,12 @@ import android.view.ViewGroup; import android.widget.Toast; import org.communiquons.android.comunic.client.R; +import org.communiquons.android.comunic.client.data.DatabaseHelper; +import org.communiquons.android.comunic.client.data.conversations.ConversationMessage; +import org.communiquons.android.comunic.client.data.conversations.ConversationMessagesHelper; +import org.communiquons.android.comunic.client.data.conversations.ConversationRefreshRunnable; + +import java.util.ArrayList; /** * Conversation fragment @@ -36,11 +42,36 @@ public class ConversationFragment extends Fragment { */ private int conversation_id; + /** + * The last available message id + */ + private int last_message_id = 0; + + /** + * The list of messages + */ + private ArrayList messagesList; + + /** + * Conversation refresh runnable + */ + private ConversationRefreshRunnable refreshRunnable; + + /** + * Conversation messages helper + */ + private ConversationMessagesHelper convMessHelper; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + //Database helper + DatabaseHelper dbHelper = new DatabaseHelper(getActivity()); + + //Set conversation message helper + convMessHelper = new ConversationMessagesHelper(getActivity(), dbHelper); + //Get the conversation ID conversation_id = getArguments().getInt(ARG_CONVERSATION_ID); @@ -52,14 +83,31 @@ public class ConversationFragment extends Fragment { @Nullable @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_conversation, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + } - Toast.makeText(getActivity(), "Open : " + conversation_id, Toast.LENGTH_SHORT).show(); + @Override + public void onResume() { + super.onResume(); + + refreshRunnable = new ConversationRefreshRunnable(conversation_id, last_message_id, + convMessHelper); + + //Create and start the thread + new Thread(refreshRunnable).start(); + } + + @Override + public void onPause() { + super.onPause(); + + refreshRunnable.quitSafely(); } }