/* * Copyright 2015 OpenMarket Ltd * Copyright 2017 Vector Creations Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.matrix.androidsdk.fragments; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.media.ThumbnailUtils; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.provider.Browser; import android.provider.MediaStore; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.text.TextUtils; import org.matrix.androidsdk.crypto.MXCryptoError; import org.matrix.androidsdk.rest.model.AudioMessage; import org.matrix.androidsdk.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.ListView; import android.widget.Toast; import com.google.gson.JsonObject; import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.R; import org.matrix.androidsdk.adapters.MessageRow; import org.matrix.androidsdk.adapters.MessagesAdapter; import org.matrix.androidsdk.crypto.MXEncryptedAttachments; import org.matrix.androidsdk.data.EventTimeline; import org.matrix.androidsdk.data.store.IMXStore; import org.matrix.androidsdk.data.Room; import org.matrix.androidsdk.data.RoomPreviewData; import org.matrix.androidsdk.data.RoomState; import org.matrix.androidsdk.data.RoomSummary; import org.matrix.androidsdk.db.MXMediasCache; import org.matrix.androidsdk.listeners.IMXEventListener; import org.matrix.androidsdk.listeners.MXEventListener; import org.matrix.androidsdk.listeners.MXMediaUploadListener; import org.matrix.androidsdk.rest.callback.ApiCallback; import org.matrix.androidsdk.rest.callback.SimpleApiCallback; import org.matrix.androidsdk.rest.model.Event; import org.matrix.androidsdk.rest.model.FileMessage; import org.matrix.androidsdk.rest.model.ImageMessage; import org.matrix.androidsdk.rest.model.LocationMessage; import org.matrix.androidsdk.rest.model.MatrixError; import org.matrix.androidsdk.rest.model.Message; import org.matrix.androidsdk.rest.model.ReceiptData; import org.matrix.androidsdk.rest.model.Search.SearchResponse; import org.matrix.androidsdk.rest.model.Search.SearchResult; import org.matrix.androidsdk.rest.model.User; import org.matrix.androidsdk.rest.model.VideoMessage; import org.matrix.androidsdk.rest.model.bingrules.BingRule; import org.matrix.androidsdk.util.EventDisplay; import org.matrix.androidsdk.util.JsonUtils; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Timer; import java.util.TimerTask; import retrofit.RetrofitError; /** * UI Fragment containing matrix messages for a given room. * Contains {@link MatrixMessagesFragment} as a nested fragment to do the work. */ public class MatrixMessageListFragment extends Fragment implements MatrixMessagesFragment.MatrixMessagesListener, MessagesAdapter.MessagesAdapterEventsListener { // search interface public interface OnSearchResultListener { void onSearchSucceed(int nbrMessages); void onSearchFailed(); } // room preview interface public interface IRoomPreviewDataListener { RoomPreviewData getRoomPreviewData(); } // be warned when a message sending failed or succeeded public interface IEventSendingListener { /** * A message has been successfully sent. * * @param event the event */ void onMessageSendingSucceeded(Event event); /** * A message sending has failed. * * @param event the event */ void onMessageSendingFailed(Event event); /** * An event has been successfully redacted by the user. * * @param event the event */ void onMessageRedacted(Event event); /** * An event sending failed because some unknown devices have been detected * * @param event the event * @param error the crypto error */ void onUnknownDevices(Event event, MXCryptoError error); } // scroll listener public interface IOnScrollListener { /** * The events list has been scrolled. * * @param firstVisibleItem the index of the first visible cell * @param visibleItemCount the number of visible cells * @param totalItemCount the number of items in the list adaptor */ void onScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount); /** * Tell if the latest event is fully displayed * * @param isDisplayed true if the latest event is fully displayed */ void onLatestEventDisplay(boolean isDisplayed); } protected static final String TAG_FRAGMENT_MESSAGE_OPTIONS = "org.matrix.androidsdk.RoomActivity.TAG_FRAGMENT_MESSAGE_OPTIONS"; protected static final String TAG_FRAGMENT_MESSAGE_DETAILS = "org.matrix.androidsdk.RoomActivity.TAG_FRAGMENT_MESSAGE_DETAILS"; // fragment parameters public static final String ARG_LAYOUT_ID = "MatrixMessageListFragment.ARG_LAYOUT_ID"; public static final String ARG_MATRIX_ID = "MatrixMessageListFragment.ARG_MATRIX_ID"; public static final String ARG_ROOM_ID = "MatrixMessageListFragment.ARG_ROOM_ID"; public static final String ARG_EVENT_ID = "MatrixMessageListFragment.ARG_EVENT_ID"; public static final String ARG_PREVIEW_MODE_ID = "MatrixMessageListFragment.ARG_PREVIEW_MODE_ID"; // default preview mode public static final String PREVIEW_MODE_READ_ONLY = "PREVIEW_MODE_READ_ONLY"; private static final String LOG_TAG = "MatrixMsgsListFrag"; private static final int UNDEFINED_VIEW_Y_POS = -12345678; public static MatrixMessageListFragment newInstance(String matrixId, String roomId, int layoutResId) { MatrixMessageListFragment f = new MatrixMessageListFragment(); Bundle args = new Bundle(); args.putString(ARG_ROOM_ID, roomId); args.putInt(ARG_LAYOUT_ID, layoutResId); args.putString(ARG_MATRIX_ID, matrixId); return f; } private MatrixMessagesFragment mMatrixMessagesFragment; protected MessagesAdapter mAdapter; public ListView mMessageListView; protected Handler mUiHandler; protected MXSession mSession; protected String mMatrixId; protected Room mRoom; protected String mPattern = null; protected boolean mIsMediaSearch; protected String mNextBatch = null; private boolean mDisplayAllEvents = true; public boolean mCheckSlideToHide = false; private boolean mIsScrollListenerSet; // timeline management protected final boolean mIsLive = true; // by default the protected EventTimeline mEventTimeLine; protected String mEventId; // pagination statuses protected boolean mIsInitialSyncing = true; protected boolean mIsBackPaginating = false; protected boolean mIsFwdPaginating = false; // lock the pagination while refreshing the list view to avoid having twice or thrice refreshes sequence. private boolean mLockBackPagination = false; private boolean mLockFwdPagination = true; protected ArrayList<Event> mResendingEventsList; private final HashMap<String, Timer> mPendingRelaunchTimersByEventId = new HashMap<>(); private final HashMap<String, Object> mBingRulesByEventId = new HashMap<>(); // scroll to to the dedicated index when the device has been rotated private int mFirstVisibleRow = -1; // scroll to the index when loaded private int mScrollToIndex = -1; // y pos of the first visible row private int mFirstVisibleRowY = UNDEFINED_VIEW_Y_POS; // used to retrieve the preview data protected IRoomPreviewDataListener mRoomPreviewDataListener; // be warned that an event sending has failed. protected IEventSendingListener mEventSendingListener; // listen when the events list is scrolled. protected IOnScrollListener mActivityOnScrollListener; public MXMediasCache getMXMediasCache() { return null; } public MXSession getSession(String matrixId) { return null; } public MXSession getSession() { // if the session has not been set if (null == mSession) { // find it out mSession = getSession(mMatrixId); } return mSession; } /** * @return an UI handler */ private Handler getUiHandler() { if (null == mUiHandler) { mUiHandler = new Handler(Looper.getMainLooper()); } return mUiHandler; } private final IMXEventListener mEventsListener = new MXEventListener() { @Override public void onPresenceUpdate(Event event, final User user) { // Someone's presence has changed, reprocess the whole list getUiHandler().post(new Runnable() { @Override public void run() { // check first if the userID has sent some messages in the room history boolean refresh = mAdapter.isDisplayedUser(user.user_id); if (refresh) { // check, if the avatar is currently displayed // The Math.min is required because the adapter and mMessageListView could be unsynchronized. // ensure there is no IndexOfOutBound exception. int firstVisibleRow = Math.min(mMessageListView.getFirstVisiblePosition(), mAdapter.getCount()); int lastVisibleRow = Math.min(mMessageListView.getLastVisiblePosition(), mAdapter.getCount()); refresh = false; for (int i = firstVisibleRow; i <= lastVisibleRow; i++) { MessageRow row = mAdapter.getItem(i); refresh |= TextUtils.equals(user.user_id, row.getEvent().getSender()); } } if (refresh) { mAdapter.notifyDataSetChanged(); } } }); } @Override public void onBingRulesUpdate() { mBingRulesByEventId.clear(); } @Override public void onEventEncrypted(Event event) { getUiHandler().post(new Runnable() { @Override public void run() { mAdapter.notifyDataSetChanged(); } }); } @Override public void onEventDecrypted(Event event) { getUiHandler().post(new Runnable() { @Override public void run() { mAdapter.notifyDataSetChanged(); } }); } }; /** * Customize the scrolls behaviour. * -> scroll over the top triggers a back pagination * -> scroll over the bottom triggers a forward pagination */ protected final AbsListView.OnScrollListener mScrollListener = new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { mCheckSlideToHide = (scrollState == SCROLL_STATE_TOUCH_SCROLL); //check only when the user scrolls the content if (scrollState == SCROLL_STATE_TOUCH_SCROLL) { int firstVisibleRow = mMessageListView.getFirstVisiblePosition(); int lastVisibleRow = mMessageListView.getLastVisiblePosition(); int count = mMessageListView.getCount(); if ((lastVisibleRow + 10) >= count) { Log.d(LOG_TAG, "onScrollStateChanged - forwardPaginate"); forwardPaginate(); } else if (firstVisibleRow < 2) { Log.d(LOG_TAG, "onScrollStateChanged - request history"); backPaginate(false); } } } /** * Warns that the list has been scrolled. * @param view the list view * @param firstVisibleItem the first visible indew * @param visibleItemCount the number of visible items * @param totalItemCount the total number of items */ private void manageScrollListener(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (null != mActivityOnScrollListener) { try { mActivityOnScrollListener.onScroll(firstVisibleItem, visibleItemCount, totalItemCount); } catch (Exception e) { Log.e(LOG_TAG, "## manageScrollListener : onScroll failed " + e.getMessage()); } boolean isLatestEventDisplayed; // test if the latest message is not displayed if ((firstVisibleItem + visibleItemCount) < totalItemCount) { // the latest event is not displayed isLatestEventDisplayed = false; } else { View childView = view.getChildAt(visibleItemCount - 1); // test if the bottom of the latest item is equals to the list height isLatestEventDisplayed = (null != childView) && ((childView.getTop() + childView.getHeight()) <= view.getHeight()); } try { mActivityOnScrollListener.onLatestEventDisplay(isLatestEventDisplayed); } catch (Exception e) { Log.e(LOG_TAG, "## manageScrollListener : onLatestEventDisplay failed " + e.getMessage()); } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // store the current Y pos to jump to the right pos when backpaginating mFirstVisibleRowY = UNDEFINED_VIEW_Y_POS; View v = mMessageListView.getChildAt(firstVisibleItem); if (null != v) { mFirstVisibleRowY = v.getTop(); } if ((firstVisibleItem < 2) && (visibleItemCount != totalItemCount) && (0 != visibleItemCount)) { // Log.d(LOG_TAG, "onScroll - backPaginate"); backPaginate(false); } else if ((firstVisibleItem + visibleItemCount + 10) >= totalItemCount) { // Log.d(LOG_TAG, "onScroll - forwardPaginate"); forwardPaginate(); } manageScrollListener(view, firstVisibleItem, visibleItemCount, totalItemCount); } }; @Override public void onCreate(Bundle savedInstanceState) { Log.d(LOG_TAG, "onCreate"); super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(LOG_TAG, "onCreateView"); View defaultView = super.onCreateView(inflater, container, savedInstanceState); Bundle args = getArguments(); // for dispatching data to add to the adapter we need to be on the main thread mUiHandler = new Handler(Looper.getMainLooper()); mMatrixId = args.getString(ARG_MATRIX_ID); mSession = getSession(mMatrixId); if (null == mSession) { if (null != getActivity()) { Log.e(LOG_TAG, "Must have valid default MXSession."); getActivity().finish(); return defaultView; } throw new RuntimeException("Must have valid default MXSession."); } if (null == getMXMediasCache()) { if (null != getActivity()) { Log.e(LOG_TAG, "Must have valid default MediasCache."); getActivity().finish(); return defaultView; } throw new RuntimeException("Must have valid default MediasCache."); } String roomId = args.getString(ARG_ROOM_ID); View v = inflater.inflate(args.getInt(ARG_LAYOUT_ID), container, false); mMessageListView = ((ListView) v.findViewById(R.id.listView_messages)); mIsScrollListenerSet = false; if (mAdapter == null) { // only init the adapter if it wasn't before, so we can preserve messages/position. mAdapter = createMessagesAdapter(); if (null == getMXMediasCache()) { throw new RuntimeException("Must have valid default MessagesAdapter."); } } else if (null != savedInstanceState) { mFirstVisibleRow = savedInstanceState.getInt("FIRST_VISIBLE_ROW", -1); } mAdapter.setIsPreviewMode(false); if (null == mEventTimeLine) { mEventId = args.getString(ARG_EVENT_ID); // the fragment displays the history around a message if (!TextUtils.isEmpty(mEventId)) { mEventTimeLine = new EventTimeline(mSession.getDataHandler(), roomId, mEventId); mRoom = mEventTimeLine.getRoom(); } // display a room preview else if (null != args.getString(ARG_PREVIEW_MODE_ID)) { mAdapter.setIsPreviewMode(true); mEventTimeLine = new EventTimeline(mSession.getDataHandler(), roomId); mRoom = mEventTimeLine.getRoom(); } // standard case else { if (!TextUtils.isEmpty(roomId)) { mRoom = mSession.getDataHandler().getRoom(roomId); mEventTimeLine = mRoom.getLiveTimeLine(); } } } // GA reported some weird room content // so ensure that the room fields are properly initialized mSession.getDataHandler().checkRoom(mRoom); // sanity check if (null != mRoom) { mAdapter.setTypingUsers(mRoom.getTypingUsers()); } mMessageListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { MatrixMessageListFragment.this.onRowClick(position); } }); mMessageListView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { onListTouch(event); return false; } }); mAdapter.setMessagesAdapterEventsListener(this); mDisplayAllEvents = isDisplayAllEvents(); return v; } /** * Called when a fragment is first attached to its activity. * {@link #onCreate(Bundle)} will be called after this. * * @param aHostActivity parent activity */ @Override public void onAttach(Activity aHostActivity) { super.onAttach(aHostActivity); try { mEventSendingListener = (IEventSendingListener) aHostActivity; } catch (ClassCastException e) { // if host activity does not provide the implementation, just ignore it Log.w(LOG_TAG, "## onAttach(): host activity does not implement IEventSendingListener " + aHostActivity); } try { mActivityOnScrollListener = (IOnScrollListener) aHostActivity; } catch (ClassCastException e) { // if host activity does not provide the implementation, just ignore it Log.w(LOG_TAG, "## onAttach(): host activity does not implement IOnScrollListener " + aHostActivity); } } /** * Called when the fragment is no longer attached to its activity. This * is called after {@link #onDestroy()}. */ @Override public void onDetach() { super.onDetach(); mEventSendingListener = null; mActivityOnScrollListener = null; } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (null != mMessageListView) { int selected = mMessageListView.getFirstVisiblePosition(); // ListView always returns the previous index while filling from bottom if (selected > 0) { selected++; } outState.putInt("FIRST_VISIBLE_ROW", selected); } } /** * Called when the fragment is no longer in use. This is called * after {@link #onStop()} and before {@link #onDetach()}. */ @Override public void onDestroy() { super.onDestroy(); // remove listeners to prevent memory leak if (null != mMatrixMessagesFragment) { mMatrixMessagesFragment.setMatrixMessagesListener(null); } if (null != mAdapter) { mAdapter.setMessagesAdapterEventsListener(null); } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Bundle args = getArguments(); FragmentManager fm = getActivity().getSupportFragmentManager(); mMatrixMessagesFragment = (MatrixMessagesFragment) fm.findFragmentByTag(getMatrixMessagesFragmentTag()); if (mMatrixMessagesFragment == null) { Log.d(LOG_TAG, "onActivityCreated create"); // this fragment controls all the logic for handling messages / API calls mMatrixMessagesFragment = createMessagesFragmentInstance(args.getString(ARG_ROOM_ID)); fm.beginTransaction().add(mMatrixMessagesFragment, getMatrixMessagesFragmentTag()).commit(); } else { Log.d(LOG_TAG, "onActivityCreated - reuse"); // Reset the listener because this is not done when the system restores the fragment (newInstance is not called) mMatrixMessagesFragment.setMatrixMessagesListener(this); } mMatrixMessagesFragment.mKeepRoomHistory = (-1 != mFirstVisibleRow); } @Override public void onPause() { super.onPause(); // mBingRulesByEventId.clear(); // check if the session has not been logged out if (mSession.isAlive() && (null != mRoom) && mIsLive) { mRoom.removeEventListener(mEventsListener); } cancelCatchingRequests(); } @Override public void onResume() { super.onResume(); // sanity check if ((null != mRoom) && mIsLive) { Room room = mSession.getDataHandler().getRoom(mRoom.getRoomId(), false); if (null != room) { room.addEventListener(mEventsListener); } else { Log.e(LOG_TAG, "the room " + mRoom.getRoomId() + " does not exist anymore"); } } } //============================================================================================================== // general methods //============================================================================================================== /** * Create the messageFragment. * Should be inherited. * * @param roomId the roomID * @return the MatrixMessagesFragment */ public MatrixMessagesFragment createMessagesFragmentInstance(String roomId) { return MatrixMessagesFragment.newInstance(getSession(), roomId, this); } /** * @return the fragment tag to use to restore the matrix messages fragment */ protected String getMatrixMessagesFragmentTag() { return "org.matrix.androidsdk.RoomActivity.TAG_FRAGMENT_MATRIX_MESSAGES"; } /** * Create the messages adapter. * This method must be overriden to provide a valid creation * * @return the messages adapter. */ public MessagesAdapter createMessagesAdapter() { return null; } /** * The user scrolls the list. * Apply an expected behaviour * * @param event the scroll event */ public void onListTouch(MotionEvent event) { } /** * Scroll the listview to a dedicated index when the list is loaded. * * @param index the index */ public void scrollToIndexWhenLoaded(int index) { mScrollToIndex = index; } /** * return true to display all the events. * else the unknown events will be hidden. */ public boolean isDisplayAllEvents() { return true; } /** * @return the max thumbnail width */ public int getMaxThumbnailWith() { return mAdapter.getMaxThumbnailWith(); } /** * @return the max thumbnail height */ public int getMaxThumbnailHeight() { return mAdapter.getMaxThumbnailHeight(); } /** * Notify the fragment that some bing rules could have been updated. */ public void onBingRulesUpdate() { mAdapter.onBingRulesUpdate(); } /** * Scroll the listView to the last item. * * @param delayMs the delay before jumping to the latest event. */ public void scrollToBottom(int delayMs) { mMessageListView.postDelayed(new Runnable() { @Override public void run() { mMessageListView.setSelection(mAdapter.getCount() - 1); } }, Math.max(delayMs, 0)); } /** * Scroll the listview to the last item. */ public void scrollToBottom() { scrollToBottom(300); } /** * Provides the event for a dedicated row. * * @param row the row * @return the event */ public Event getEvent(int row) { Event event = null; if (mAdapter.getCount() > row) { event = mAdapter.getItem(row).getEvent(); } return event; } // create a dummy message row for the message // It is added to the Adapter // return the created Message private MessageRow addMessageRow(Message message) { // a message row can only be added if there is a defined room if (null != mRoom) { Event event = new Event(message, mSession.getCredentials().userId, mRoom.getRoomId()); mRoom.storeOutgoingEvent(event); MessageRow messageRow = new MessageRow(event, mRoom.getState()); mAdapter.add(messageRow); scrollToBottom(); Log.d(LOG_TAG, "AddMessage Row : commit"); getSession().getDataHandler().getStore().commit(); return messageRow; } else { return null; } } /** * Redact an event from its event id. * * @param eventId the event id. */ protected void redactEvent(final String eventId) { // Do nothing on success, the event will be hidden when the redaction event comes down the event stream mMatrixMessagesFragment.redact(eventId, new ApiCallback<Event>() { @Override public void onSuccess(final Event redactedEvent) { if (null != redactedEvent) { getUiHandler().post(new Runnable() { @Override public void run() { // create a dummy redacted event to manage the redaction. // some redacted events are not removed from the history but they are pruned. Event redacterEvent = new Event(); redacterEvent.roomId = redactedEvent.roomId; redacterEvent.redacts = redactedEvent.eventId; redacterEvent.setType(Event.EVENT_TYPE_REDACTION); onEvent(redacterEvent, EventTimeline.Direction.FORWARDS, mRoom.getLiveState()); if (null != mEventSendingListener) { try { mEventSendingListener.onMessageRedacted(redactedEvent); } catch (Exception e) { Log.e(LOG_TAG, "redactEvent fails : " + e.getMessage()); } } } }); } } private void onError() { if (null != getActivity()) { Toast.makeText(getActivity(), getActivity().getString(R.string.could_not_redact), Toast.LENGTH_SHORT).show(); } } @Override public void onNetworkError(Exception e) { onError(); } @Override public void onMatrixError(MatrixError e) { onError(); } @Override public void onUnexpectedError(Exception e) { onError(); } }); } /** * Tells if an event is supported by the fragment. * * @param event the event to test * @return true it is supported. */ private boolean canAddEvent(Event event) { String type = event.getType(); return mDisplayAllEvents || Event.EVENT_TYPE_MESSAGE.equals(type) || Event.EVENT_TYPE_MESSAGE_ENCRYPTED.equals(type) || Event.EVENT_TYPE_MESSAGE_ENCRYPTION.equals(type) || Event.EVENT_TYPE_STATE_ROOM_NAME.equals(type) || Event.EVENT_TYPE_STATE_ROOM_TOPIC.equals(type) || Event.EVENT_TYPE_STATE_ROOM_MEMBER.equals(type) || Event.EVENT_TYPE_STATE_ROOM_THIRD_PARTY_INVITE.equals(type) || Event.EVENT_TYPE_STATE_HISTORY_VISIBILITY.equals(type) || (event.isCallEvent() && (!Event.EVENT_TYPE_CALL_CANDIDATES.equals(type))) ; } //============================================================================================================== // Messages sending method. //============================================================================================================== /** * Warns that a message sending has failed. * * @param event the event */ private void onMessageSendingFailed(Event event) { if (null != mEventSendingListener) { try { mEventSendingListener.onMessageSendingFailed(event); } catch (Exception e) { Log.e(LOG_TAG, "onMessageSendingFailed failed " + e.getLocalizedMessage()); } } } /** * Warns that a message sending succeeds. * * @param event the event */ private void onMessageSendingSucceeded(Event event) { if (null != mEventSendingListener) { try { mEventSendingListener.onMessageSendingSucceeded(event); } catch (Exception e) { Log.e(LOG_TAG, "onMessageSendingSucceeded failed " + e.getLocalizedMessage()); } } } /** * Warns that a message sending failed because some unknown devices have been detected. * * @param event the event * @param cryptoError the crypto error */ private void onUnknownDevices(Event event, MXCryptoError cryptoError) { if (null != mEventSendingListener) { try { mEventSendingListener.onUnknownDevices(event, cryptoError); } catch (Exception e) { Log.e(LOG_TAG, "onUnknownDevices failed " + e.getLocalizedMessage()); } } } /** * Send a message in the room. * * @param message the message to send. */ private void send(final Message message) { send(addMessageRow(message)); } /** * Send a message row in the dedicated room. * * @param messageRow the message row to send. */ private void send(final MessageRow messageRow) { // add sanity check if (null == messageRow) { return; } final Event event = messageRow.getEvent(); if (!event.isUndeliverable()) { final String prevEventId = event.eventId; mMatrixMessagesFragment.sendEvent(event, new ApiCallback<Void>() { @Override public void onSuccess(Void info) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingSucceeded(event); mAdapter.updateEventById(event, prevEventId); // pending resending ? if ((null != mResendingEventsList) && (mResendingEventsList.size() > 0)) { resend(mResendingEventsList.get(0)); mResendingEventsList.remove(0); } } }); } private void commonFailure(final Event event) { if (null != MatrixMessageListFragment.this.getActivity()) { getUiHandler().post(new Runnable() { @Override public void run() { // display the error message only if the message cannot be resent if ((null != event.unsentException) && (event.isUndeliverable())) { if ((event.unsentException instanceof RetrofitError) && ((RetrofitError) event.unsentException).isNetworkError()) { Toast.makeText(getActivity(), getActivity().getString(R.string.unable_to_send_message) + " : " + getActivity().getString(R.string.network_error), Toast.LENGTH_LONG).show(); } else { Toast.makeText(getActivity(), getActivity().getString(R.string.unable_to_send_message) + " : " + event.unsentException.getLocalizedMessage(), Toast.LENGTH_LONG).show(); } } else if (null != event.unsentMatrixError) { Toast.makeText(getActivity(), getActivity().getString(R.string.unable_to_send_message) + " : " + event.unsentMatrixError.getLocalizedMessage() + ".", Toast.LENGTH_LONG).show(); } mAdapter.notifyDataSetChanged(); onMessageSendingFailed(event); } }); } } @Override public void onNetworkError(final Exception e) { commonFailure(event); } @Override public void onMatrixError(final MatrixError e) { // do not display toast if the sending failed because of unknown deviced (e2e issue) if (event.mSentState == Event.SentState.FAILED_UNKNOWN_DEVICES) { getUiHandler().post(new Runnable() { @Override public void run() { mAdapter.notifyDataSetChanged(); onUnknownDevices(event, (MXCryptoError) e); } }); } else { commonFailure(event); } } @Override public void onUnexpectedError(final Exception e) { commonFailure(event); } }); } } /** * Send a text message. * * @param body the text message to send. */ public void sendTextMessage(String body) { sendMessage(Message.MSGTYPE_TEXT, body, null, null); } /** * Send a formatted text message. * * @param body the unformatted text message * @param formattedBody the formatted text message (optional) * @param format the format */ public void sendTextMessage(String body, String formattedBody, String format) { sendMessage(Message.MSGTYPE_TEXT, body, formattedBody, format); } /** * Send a message of type msgType with a formatted body * * @param msgType the message type * @param body the unformatted text message * @param formattedBody the formatted text message (optional) * @param format the format */ private void sendMessage(String msgType, String body, String formattedBody, String format) { Message message = new Message(); message.msgtype = msgType; message.body = body; if (null != formattedBody) { // assume that the formatted body use a custom html format message.format = format; message.formatted_body = formattedBody; } send(message); } /** * Send an emote * * @param emote the emote * @param formattedEmote the formatted text message (optional) * @param format the format */ public void sendEmote(String emote, String formattedEmote, String format) { sendMessage(Message.MSGTYPE_EMOTE, emote, formattedEmote, format); } /** * The media upload fails. * * @param serverResponseCode the response code. * @param serverErrorMessage the error message. * @param messageRow the messageRow */ private void commonMediaUploadError(int serverResponseCode, final String serverErrorMessage, final MessageRow messageRow) { // warn the user that the media upload fails if (serverResponseCode == 500) { Timer relaunchTimer = new Timer(); mPendingRelaunchTimersByEventId.put(messageRow.getEvent().eventId, relaunchTimer); relaunchTimer.schedule(new TimerTask() { @Override public void run() { if (mPendingRelaunchTimersByEventId.containsKey(messageRow.getEvent().eventId)) { mPendingRelaunchTimersByEventId.remove(messageRow.getEvent().eventId); Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { resend(messageRow.getEvent()); } }); } } }, 1000); } else { messageRow.getEvent().mSentState = Event.SentState.UNDELIVERABLE; onMessageSendingFailed(messageRow.getEvent()); if (null != getActivity()) { Toast.makeText(getActivity(), (null != serverErrorMessage) ? serverErrorMessage : getString(R.string.message_failed_to_upload), Toast.LENGTH_LONG).show(); } } } /** * Upload a file content * * @param mediaUrl the media URL * @param mimeType the media mime type * @param mediaFilename the media filename */ public void uploadFileContent(final String mediaUrl, String mimeType, final String mediaFilename) { // create a tmp row final FileMessage tmpFileMessage; if ((null != mimeType) && mimeType.startsWith("audio/")) { tmpFileMessage = new AudioMessage(); } else { tmpFileMessage = new FileMessage(); } tmpFileMessage.url = mediaUrl; tmpFileMessage.body = mediaFilename; MXEncryptedAttachments.EncryptionResult encryptionResult = null; InputStream fileStream = null; try { Uri uri = Uri.parse(mediaUrl); Room.fillFileInfo(getActivity(), tmpFileMessage, uri, mimeType); String filename = uri.getPath(); fileStream = new FileInputStream(new File(filename)); if (mRoom.isEncrypted() && mSession.isCryptoEnabled() && (null != fileStream)) { encryptionResult = MXEncryptedAttachments.encryptAttachment(fileStream, mimeType); fileStream.close(); if (null != encryptionResult) { fileStream = encryptionResult.mEncryptedStream; mimeType = "application/octet-stream"; } else { displayEncryptionAlert(); return; } } if (null == tmpFileMessage.body) { tmpFileMessage.body = uri.getLastPathSegment(); } } catch (Exception e) { Log.e(LOG_TAG, "uploadFileContent failed with " + e.getLocalizedMessage()); } // remove any displayed MessageRow with this URL // to avoid duplicate final MessageRow messageRow = addMessageRow(tmpFileMessage); messageRow.getEvent().mSentState = Event.SentState.SENDING; final MXEncryptedAttachments.EncryptionResult fEncryptionResult = encryptionResult; getSession().getMediasCache().uploadContent(fileStream, tmpFileMessage.body, mimeType, mediaUrl, new MXMediaUploadListener() { @Override public void onUploadStart(String uploadId) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingSucceeded(messageRow.getEvent()); // display the pie chart. mAdapter.notifyDataSetChanged(); } }); } @Override public void onUploadCancel(String uploadId) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingFailed(messageRow.getEvent()); } }); } @Override public void onUploadError(final String uploadId, final int serverResponseCode, final String serverErrorMessage) { getUiHandler().post(new Runnable() { @Override public void run() { commonMediaUploadError(serverResponseCode, serverErrorMessage, messageRow); } }); } @Override public void onUploadComplete(final String uploadId, final String contentUri) { getUiHandler().post(new Runnable() { @Override public void run() { // Build the image message FileMessage message = tmpFileMessage.deepCopy(); // replace the thumbnail and the media contents by the computed ones getMXMediasCache().saveFileMediaForUrl(contentUri, mediaUrl, tmpFileMessage.getMimeType()); if (null != fEncryptionResult) { message.file = fEncryptionResult.mEncryptedFileInfo; message.file.url = contentUri; message.url = null; } else { message.url = contentUri; } // update the event content with the new message info messageRow.getEvent().updateContent(JsonUtils.toJson(message)); Log.d(LOG_TAG, "Uploaded to " + contentUri); send(messageRow); } }); } }); } /** * Compute the video thumbnail * * @param videoUrl the video url * @return the video thumbnail */ public String getVideoThumbnailUrl(final String videoUrl) { String thumbUrl = null; try { Uri uri = Uri.parse(videoUrl); Bitmap thumb = ThumbnailUtils.createVideoThumbnail(uri.getPath(), MediaStore.Images.Thumbnails.MINI_KIND); thumbUrl = getMXMediasCache().saveBitmap(thumb, null); } catch (Exception e) { Log.e(LOG_TAG, "getVideoThumbailUrl failed with " + e.getLocalizedMessage()); } return thumbUrl; } /** * Upload a video message * The video thumbnail will be computed * * @param videoUrl the video url * @param body the message body * @param videoMimeType the video mime type */ public void uploadVideoContent(final String videoUrl, final String body, final String videoMimeType) { uploadVideoContent(videoUrl, getVideoThumbnailUrl(videoUrl), body, videoMimeType); } /** * Upload a video message * The video thumbnail will be computed * * @param videoUrl the video url * @param thumbUrl the thumbnail Url * @param body the message body * @param videoMimeType the video mime type */ public void uploadVideoContent(final String videoUrl, final String thumbUrl, final String body, final String videoMimeType) { // if the video thumbnail cannot be retrieved // send it as a file if (null == thumbUrl) { this.uploadFileContent(videoUrl, videoMimeType, body); } else { this.uploadVideoContent(null, null, thumbUrl, "image/jpeg", videoUrl, body, videoMimeType); } } /** * Upload a video message * * @param thumbnailUrl the thumbnail Url * @param thumbnailMimeType the thumbnail mime type * @param videoUrl the video url * @param body the message body * @param videoMimeType the video mime type */ public void uploadVideoContent(final VideoMessage sourceVideoMessage, final MessageRow aVideoRow, final String thumbnailUrl, final String thumbnailMimeType, final String videoUrl, final String body, final String videoMimeType) { // create a tmp row VideoMessage tmpVideoMessage = sourceVideoMessage; Uri uri = null; Uri thumbUri = null; try { uri = Uri.parse(videoUrl); thumbUri = Uri.parse(thumbnailUrl); } catch (Exception e) { Log.e(LOG_TAG, "uploadVideoContent failed with " + e.getLocalizedMessage()); } // the video message is not defined if (null == tmpVideoMessage) { tmpVideoMessage = new VideoMessage(); tmpVideoMessage.url = videoUrl; tmpVideoMessage.body = body; try { Room.fillVideoInfo(getActivity(), tmpVideoMessage, uri, videoMimeType, thumbUri, thumbnailMimeType); if (null == tmpVideoMessage.body) { tmpVideoMessage.body = uri.getLastPathSegment(); } } catch (Exception e) { Log.e(LOG_TAG, "uploadVideoContent : fillVideoInfo failed " + e.getLocalizedMessage()); } } // remove any displayed MessageRow with this URL // to avoid duplicate final MessageRow videoRow = (null == aVideoRow) ? addMessageRow(tmpVideoMessage) : aVideoRow; videoRow.getEvent().mSentState = Event.SentState.SENDING; InputStream imageStream = null; String filename = ""; String uploadId = ""; String mimeType = ""; MXEncryptedAttachments.EncryptionResult encryptionResult = null; try { // the thumbnail has been uploaded ? if (tmpVideoMessage.isThumbnailLocalContent()) { uploadId = thumbnailUrl; imageStream = new FileInputStream(new File(thumbUri.getPath())); mimeType = thumbnailMimeType; if (mRoom.isEncrypted() && mSession.isCryptoEnabled() && (null != imageStream)) { encryptionResult = MXEncryptedAttachments.encryptAttachment(imageStream, thumbnailMimeType); imageStream.close(); if (null != encryptionResult) { imageStream = encryptionResult.mEncryptedStream; mimeType = "application/octet-stream"; } else { displayEncryptionAlert(); return; } } } else { uploadId = videoUrl; imageStream = new FileInputStream(new File(uri.getPath())); filename = tmpVideoMessage.body; mimeType = videoMimeType; if (mRoom.isEncrypted() && mSession.isCryptoEnabled() && (null != imageStream)) { encryptionResult = MXEncryptedAttachments.encryptAttachment(imageStream, thumbnailMimeType); imageStream.close(); if (null != encryptionResult) { imageStream = encryptionResult.mEncryptedStream; mimeType = "application/octet-stream"; } else { displayEncryptionAlert(); return; } } } } catch (Exception e) { Log.e(LOG_TAG, "uploadVideoContent : media parsing failed " + e.getLocalizedMessage()); } final boolean isContentUpload = TextUtils.equals(uploadId, videoUrl); final VideoMessage fVideoMessage = tmpVideoMessage; final MXEncryptedAttachments.EncryptionResult fEncryptionResult = encryptionResult; getSession().getMediasCache().uploadContent(imageStream, filename, mimeType, uploadId, new MXMediaUploadListener() { @Override public void onUploadStart(String uploadId) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingSucceeded(videoRow.getEvent()); mAdapter.notifyDataSetChanged(); } }); } @Override public void onUploadCancel(String uploadId) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingFailed(videoRow.getEvent()); } }); } @Override public void onUploadError(final String uploadId, final int serverResponseCode, final String serverErrorMessage) { getUiHandler().post(new Runnable() { @Override public void run() { commonMediaUploadError(serverResponseCode, serverErrorMessage, videoRow); } }); } @Override public void onUploadComplete(final String uploadId, final String contentUri) { getUiHandler().post(new Runnable() { @Override public void run() { // the video content has been uploaded if (isContentUpload) { // replace the thumbnail and the media contents by the computed ones getMXMediasCache().saveFileMediaForUrl(contentUri, videoUrl, videoMimeType); if (null == fEncryptionResult) { fVideoMessage.url = contentUri; } else { fEncryptionResult.mEncryptedFileInfo.url = contentUri; fVideoMessage.file = fEncryptionResult.mEncryptedFileInfo; fVideoMessage.url = null; } // update the event content with the new message info videoRow.getEvent().updateContent(JsonUtils.toJson(fVideoMessage)); Log.d(LOG_TAG, "Uploaded to " + contentUri); send(videoRow); } else { if (null == fEncryptionResult) { fVideoMessage.info.thumbnail_url = contentUri; getMXMediasCache().saveFileMediaForUrl(contentUri, thumbnailUrl, mAdapter.getMaxThumbnailWith(), mAdapter.getMaxThumbnailHeight(), thumbnailMimeType, true); } else { fEncryptionResult.mEncryptedFileInfo.url = contentUri; fVideoMessage.info.thumbnail_file = fEncryptionResult.mEncryptedFileInfo; fVideoMessage.info.thumbnail_url = null; getMXMediasCache().saveFileMediaForUrl(contentUri, thumbnailUrl, -1, -1, thumbnailMimeType, true); } // update the event content with the new message info videoRow.getEvent().updateContent(JsonUtils.toJson(fVideoMessage)); // upload the video uploadVideoContent(fVideoMessage, videoRow, thumbnailUrl, thumbnailMimeType, videoUrl, fVideoMessage.body, videoMimeType); } } }); } }); } /** * Display an encyption alert */ private void displayEncryptionAlert() { if (null != getActivity()) { new AlertDialog.Builder(getActivity()) .setMessage("Fail to encrypt?") .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // continue with delete } }) .setIcon(android.R.drawable.ic_dialog_alert) .show(); } } /** * upload an image content. * It might be triggered from a media selection : imageUri is used to compute thumbnails. * Or, it could have been called to resend an image. * * @param thumbnailUrl the thumbnail Url * @param imageUrl the image Uri * @param mediaFilename the mediaFilename * @param imageMimeType the image mine type */ public void uploadImageContent(ImageMessage imageMessage, final MessageRow aImageRow, final String thumbnailUrl, final String anImageUrl, final String mediaFilename, final String imageMimeType) { if (null == imageMessage) { imageMessage = new ImageMessage(); imageMessage.url = anImageUrl; imageMessage.thumbnailUrl = thumbnailUrl; imageMessage.body = mediaFilename; } String mimeType = null; MXEncryptedAttachments.EncryptionResult encryptionResult = null; InputStream imageStream = null; String url = null; try { Uri imageUri = Uri.parse(anImageUrl); if (null == imageMessage.info) { Room.fillImageInfo(getActivity(), imageMessage, imageUri, imageMimeType); } if ((null != thumbnailUrl) && (null == imageMessage.thumbnailInfo)) { Uri thumbUri = Uri.parse(thumbnailUrl); Room.fillThumbnailInfo(getActivity(), imageMessage, thumbUri, "image/jpeg"); } String filename; if (imageMessage.isThumbnailLocalContent()) { url = thumbnailUrl; mimeType = "image/jpeg"; filename = Uri.parse(thumbnailUrl).getPath(); } else { url = anImageUrl; mimeType = imageMimeType; filename = imageUri.getPath(); } imageStream = new FileInputStream(new File(filename)); if (mRoom.isEncrypted() && mSession.isCryptoEnabled() && (null != imageStream)) { encryptionResult = MXEncryptedAttachments.encryptAttachment(imageStream, mimeType); imageStream.close(); if (null != encryptionResult) { imageStream = encryptionResult.mEncryptedStream; mimeType = "application/octet-stream"; } else { displayEncryptionAlert(); return; } } imageMessage.body = imageUri.getLastPathSegment(); } catch (Exception e) { Log.e(LOG_TAG, "uploadImageContent failed with " + e.getMessage()); } if (TextUtils.isEmpty(imageMessage.body)) { imageMessage.body = "Image"; } // remove any displayed MessageRow with this URL // to avoid duplicate final String fMimeType = mimeType; final MessageRow imageRow = (null == aImageRow) ? addMessageRow(imageMessage) : aImageRow; final ImageMessage fImageMessage = imageMessage; imageRow.getEvent().mSentState = Event.SentState.SENDING; final MXEncryptedAttachments.EncryptionResult fEncryptionResult = encryptionResult; getSession().getMediasCache().uploadContent(imageStream, imageMessage.isThumbnailLocalContent() ? ("thumb" + imageMessage.body) : imageMessage.body, mimeType, url, new MXMediaUploadListener() { @Override public void onUploadStart(String uploadId) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingSucceeded(imageRow.getEvent()); mAdapter.notifyDataSetChanged(); } }); } @Override public void onUploadCancel(String uploadId) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingFailed(imageRow.getEvent()); } }); } @Override public void onUploadError(final String uploadId, final int serverResponseCode, final String serverErrorMessage) { getUiHandler().post(new Runnable() { @Override public void run() { commonMediaUploadError(serverResponseCode, serverErrorMessage, imageRow); } }); } @Override public void onUploadComplete(final String uploadId, final String contentUri) { getUiHandler().post(new Runnable() { @Override public void run() { if (fImageMessage.isThumbnailLocalContent()) { if (null != fEncryptionResult) { fImageMessage.info.thumbnail_file = fEncryptionResult.mEncryptedFileInfo; fImageMessage.info.thumbnail_file.url = contentUri; fImageMessage.thumbnailUrl = null; getMXMediasCache().saveFileMediaForUrl(contentUri, thumbnailUrl, -1, -1, "image/jpeg"); } else { fImageMessage.thumbnailUrl = contentUri; getMXMediasCache().saveFileMediaForUrl(contentUri, thumbnailUrl, mAdapter.getMaxThumbnailWith(), mAdapter.getMaxThumbnailHeight(), "image/jpeg"); } // update the event content with the new message info imageRow.getEvent().updateContent(JsonUtils.toJson(fImageMessage)); // upload the high res picture uploadImageContent(fImageMessage, imageRow, contentUri, anImageUrl, mediaFilename, fMimeType); } else { // replace the thumbnail and the media contents by the computed one getMXMediasCache().saveFileMediaForUrl(contentUri, anImageUrl, fImageMessage.getMimeType()); if (null != fEncryptionResult) { fImageMessage.file = fEncryptionResult.mEncryptedFileInfo; fImageMessage.file.url = contentUri; fImageMessage.url = null; } else { fImageMessage.url = contentUri; } // update the event content with the new message info imageRow.getEvent().updateContent(JsonUtils.toJson(fImageMessage)); Log.d(LOG_TAG, "Uploaded to " + contentUri); send(imageRow); } } }); } }); } /** * upload an image content. * It might be triggered from a media selection : imageUri is used to compute thumbnails. * Or, it could have been called to resend an image. * * @param thumbnailUrl the thumbnail Url * @param thumbnailMimeType the thumbnail mimetype * @param geo_uri the geo_uri * @param body the message body */ public void uploadLocationContent(final String thumbnailUrl, final String thumbnailMimeType, final String geo_uri, final String body) { // create a tmp row final LocationMessage tmpLocationMessage = new LocationMessage(); tmpLocationMessage.thumbnail_url = thumbnailUrl; tmpLocationMessage.body = body; tmpLocationMessage.geo_uri = geo_uri; FileInputStream imageStream = null; try { Uri uri = Uri.parse(thumbnailUrl); Room.fillLocationInfo(getActivity(), tmpLocationMessage, uri, thumbnailMimeType); String filename = uri.getPath(); imageStream = new FileInputStream(new File(filename)); if (TextUtils.isEmpty(tmpLocationMessage.body)) { tmpLocationMessage.body = "Location"; } } catch (Exception e) { Log.e(LOG_TAG, "uploadLocationContent failed with " + e.getLocalizedMessage()); } // remove any displayed MessageRow with this URL // to avoid duplicate final MessageRow locationRow = addMessageRow(tmpLocationMessage); locationRow.getEvent().mSentState = Event.SentState.SENDING; getSession().getMediasCache().uploadContent(imageStream, tmpLocationMessage.body, thumbnailMimeType, thumbnailUrl, new MXMediaUploadListener() { @Override public void onUploadStart(String uploadId) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingSucceeded(locationRow.getEvent()); mAdapter.notifyDataSetChanged(); } }); } @Override public void onUploadCancel(String uploadId) { getUiHandler().post(new Runnable() { @Override public void run() { onMessageSendingFailed(locationRow.getEvent()); } }); } @Override public void onUploadError(String uploadId, final int serverResponseCode, final String serverErrorMessage) { getUiHandler().post(new Runnable() { @Override public void run() { commonMediaUploadError(serverResponseCode, serverErrorMessage, locationRow); } }); } @Override public void onUploadComplete(final String uploadId, final String contentUri) { getUiHandler().post(new Runnable() { @Override public void run() { // Build the location message LocationMessage message = tmpLocationMessage.deepCopy(); // replace the thumbnail and the media contents by the computed ones getMXMediasCache().saveFileMediaForUrl(contentUri, thumbnailUrl, mAdapter.getMaxThumbnailWith(), mAdapter.getMaxThumbnailHeight(), "image/jpeg"); message.thumbnail_url = contentUri; // update the event content with the new message info locationRow.getEvent().updateContent(JsonUtils.toJson(message)); Log.d(LOG_TAG, "Uploaded to " + contentUri); send(locationRow); } }); } }); } //============================================================================================================== // Unsent messages management //============================================================================================================== /** * Provides the unsent messages list. * * @return the unsent messages list */ private List<Event> getUnsentMessages() { List<Event> unsent = new ArrayList<>(); List<Event> undeliverableEvents = mSession.getDataHandler().getStore().getUndeliverableEvents(mRoom.getRoomId()); List<Event> unknownDeviceEvents = mSession.getDataHandler().getStore().getUnknownDeviceEvents(mRoom.getRoomId()); if (null != undeliverableEvents) { unsent.addAll(undeliverableEvents); } if (null != unknownDeviceEvents) { unsent.addAll(unknownDeviceEvents); } return unsent; } /** * Delete the unsent (undeliverable messages). */ public void deleteUnsentMessages() { List<Event> unsent = getUnsentMessages(); if (unsent.size() > 0) { IMXStore store = mSession.getDataHandler().getStore(); // reset the timestamp for (Event event : unsent) { mAdapter.removeEventById(event.eventId); store.deleteEvent(event); } // update the summary Event latestEvent = store.getLatestEvent(mRoom.getRoomId()); // if there is an oldest event, use it to set a summary if (latestEvent != null) { if (RoomSummary.isSupportedEvent(latestEvent)) { store.storeSummary(latestEvent.roomId, latestEvent, mRoom.getState(), mSession.getMyUserId()); } } store.commit(); mAdapter.notifyDataSetChanged(); } } /** * Resend the unsent messages */ public void resendUnsentMessages() { // check if the call is done in the right thread if (Looper.getMainLooper().getThread() != Thread.currentThread()) { Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { resendUnsentMessages(); } }); return; } List<Event> unsent = getUnsentMessages(); if (unsent.size() > 0) { mResendingEventsList = new ArrayList<>(unsent); // reset the timestamp for (Event event : mResendingEventsList) { event.mSentState = Event.SentState.UNSENT; } resend(mResendingEventsList.get(0)); mResendingEventsList.remove(0); } } /** * Resend an event. * * @param event the event to resend. */ protected void resend(final Event event) { // sanity check // should never happen but got it in a GA issue if (null == event.eventId) { Log.e(LOG_TAG, "resend : got an event with a null eventId"); return; } // check if the call is done in the right thread if (Looper.getMainLooper().getThread() != Thread.currentThread()) { Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { resend(event); } }); return; } // update the timestamp event.originServerTs = System.currentTimeMillis(); // remove the event getSession().getDataHandler().deleteRoomEvent(event); mAdapter.removeEventById(event.eventId); mPendingRelaunchTimersByEventId.remove(event.eventId); // send it again final Message message = JsonUtils.toMessage(event.getContent()); // resend an image ? if (message instanceof ImageMessage) { ImageMessage imageMessage = (ImageMessage) message; // media has not been uploaded if (imageMessage.isLocalContent() || imageMessage.isThumbnailLocalContent()) { uploadImageContent(imageMessage, null, imageMessage.thumbnailUrl, imageMessage.url, imageMessage.body, imageMessage.getMimeType()); return; } } else if (message instanceof FileMessage) { FileMessage fileMessage = (FileMessage) message; // media has not been uploaded if (fileMessage.isLocalContent()) { uploadFileContent(fileMessage.url, fileMessage.getMimeType(), fileMessage.body); return; } } else if (message instanceof VideoMessage) { VideoMessage videoMessage = (VideoMessage) message; // media has not been uploaded if (videoMessage.isLocalContent() || videoMessage.isThumbnailLocalContent()) { String thumbnailUrl = null; String thumbnailMimeType = null; if (null != videoMessage.info) { thumbnailUrl = videoMessage.info.thumbnail_url; if (null != videoMessage.info.thumbnail_info) { thumbnailMimeType = videoMessage.info.thumbnail_info.mimetype; } } uploadVideoContent(videoMessage, null, thumbnailUrl, thumbnailMimeType, videoMessage.url, videoMessage.body, videoMessage.getVideoMimeType()); return; } else if (message instanceof LocationMessage) { LocationMessage locationMessage = (LocationMessage) message; // media has not been uploaded if (locationMessage.isLocalThumbnailContent()) { String thumbMimeType = null; if (null != locationMessage.thumbnail_info) { thumbMimeType = locationMessage.thumbnail_info.mimetype; } uploadLocationContent(locationMessage.thumbnail_url, thumbMimeType, locationMessage.geo_uri, locationMessage.body); return; } } } send(message); } //============================================================================================================== // UI stuff //============================================================================================================== /** * Display a spinner to warn the user that a back pagination is in progress. */ public void showLoadingBackProgress() { } /** * Dismiss the back pagination progress. */ public void hideLoadingBackProgress() { } /** * Display a spinner to warn the user that a forward pagination is in progress. */ public void showLoadingForwardProgress() { } /** * Dismiss the forward pagination progress. */ public void hideLoadingForwardProgress() { } /** * Display a spinner to warn the user that the initialization is in progress. */ public void showInitLoading() { } /** * Dismiss the initialization spinner. */ public void hideInitLoading() { } /** * Refresh the messages list. */ public void refresh() { mAdapter.notifyDataSetChanged(); } //============================================================================================================== // pagination methods //============================================================================================================== /** * Manage the request history error cases. * * @param error the error object. */ private void onPaginateRequestError(final Object error) { if (null != MatrixMessageListFragment.this.getActivity()) { if (error instanceof Exception) { Log.e(LOG_TAG, "Network error: " + ((Exception) error).getMessage()); Toast.makeText(MatrixMessageListFragment.this.getActivity(), getActivity().getString(R.string.network_error), Toast.LENGTH_SHORT).show(); } else if (error instanceof MatrixError) { final MatrixError matrixError = (MatrixError) error; Log.e(LOG_TAG, "Matrix error" + " : " + matrixError.errcode + " - " + matrixError.getLocalizedMessage()); Toast.makeText(MatrixMessageListFragment.this.getActivity(), getActivity().getString(R.string.matrix_error) + " : " + matrixError.getLocalizedMessage(), Toast.LENGTH_SHORT).show(); } hideLoadingBackProgress(); hideLoadingForwardProgress(); Log.d(LOG_TAG, "requestHistory failed " + error); mIsBackPaginating = false; } } /** * Start a forward pagination */ private void forwardPaginate() { if (mLockFwdPagination) { Log.d(LOG_TAG, "The forward pagination is locked."); return; } if ((null == mEventTimeLine) || mEventTimeLine.isLiveTimeline()) { //Log.d(LOG_TAG, "The forward pagination is not supported for the live timeline."); return; } if (mIsFwdPaginating) { Log.d(LOG_TAG, "A forward pagination is in progress, please wait."); return; } showLoadingForwardProgress(); final int countBeforeUpdate = mAdapter.getCount(); mIsFwdPaginating = mEventTimeLine.forwardPaginate(new ApiCallback<Integer>() { /** * the forward pagination is ended. */ private void onEndOfPagination(String errorMessage) { if (null != errorMessage) { Log.e(LOG_TAG, "forwardPaginate fails : " + errorMessage); } mIsFwdPaginating = false; hideLoadingForwardProgress(); } @Override public void onSuccess(Integer count) { final int firstPos = mMessageListView.getFirstVisiblePosition(); mLockBackPagination = true; // retrieve if (0 != count) { mAdapter.notifyDataSetChanged(); // trick to avoid that the list jump to the latest item. mMessageListView.setAdapter(mMessageListView.getAdapter()); // keep the first position while refreshing the list mMessageListView.setSelection(firstPos); mMessageListView.post(new Runnable() { @Override public void run() { // Scroll the list down to where it was before adding rows to the top int diff = mAdapter.getCount() - countBeforeUpdate; Log.d(LOG_TAG, "forwardPaginate ends with " + diff + " new items."); onEndOfPagination(null); mLockBackPagination = false; } }); } else { Log.d(LOG_TAG, "forwardPaginate ends : nothing to add"); onEndOfPagination(null); mLockBackPagination = false; } } @Override public void onNetworkError(Exception e) { onEndOfPagination(e.getLocalizedMessage()); } @Override public void onMatrixError(MatrixError e) { onEndOfPagination(e.getLocalizedMessage()); } @Override public void onUnexpectedError(Exception e) { onEndOfPagination(e.getLocalizedMessage()); } }); if (mIsFwdPaginating) { Log.d(LOG_TAG, "forwardPaginate starts"); showLoadingForwardProgress(); } else { hideLoadingForwardProgress(); Log.d(LOG_TAG, "forwardPaginate nothing to do"); } } /** * Set the scroll listener to mMessageListView */ protected void setMessageListViewScrollListener() { // ensure that the listener is set only once // else it triggers an inifinite loop with backPaginate. if (!mIsScrollListenerSet) { mIsScrollListenerSet = true; mMessageListView.setOnScrollListener(mScrollListener); } } /** * Trigger a back pagination. * * @param fillHistory true to try to fill the listview height. */ public void backPaginate(final boolean fillHistory) { if (mIsBackPaginating) { Log.d(LOG_TAG, "backPaginate is in progress : please wait"); return; } if (mIsInitialSyncing) { Log.d(LOG_TAG, "backPaginate : an initial sync is in progress"); return; } if (mLockBackPagination) { Log.d(LOG_TAG, "backPaginate : The back pagination is locked."); return; } // search mode // The search mode uses remote requests only // i.e the eventtimeline is not used. // so the dedicated method must manage the back pagination if (!TextUtils.isEmpty(mPattern)) { Log.d(LOG_TAG, "backPaginate with pattern " + mPattern); requestSearchHistory(); return; } if (!mMatrixMessagesFragment.canBackPaginate()) { Log.d(LOG_TAG, "backPaginate : cannot back paginating again"); setMessageListViewScrollListener(); return; } final int countBeforeUpdate = mAdapter.getCount(); mIsBackPaginating = mMatrixMessagesFragment.backPaginate(new SimpleApiCallback<Integer>(getActivity()) { @Override public void onSuccess(final Integer count) { // Scroll the list down to where it was before adding rows to the top mMessageListView.post(new Runnable() { @Override public void run() { mLockFwdPagination = true; final int countDiff = mAdapter.getCount() - countBeforeUpdate; Log.d(LOG_TAG, "backPaginate : ends with " + countDiff + " new items (total : " + mAdapter.getCount() + ")"); // check if some messages have been added if (0 != countDiff) { mAdapter.notifyDataSetChanged(); // trick to avoid that the list jump to the latest item. mMessageListView.setAdapter(mMessageListView.getAdapter()); final int expectedPos = fillHistory ? (mAdapter.getCount() - 1) : (mMessageListView.getFirstVisiblePosition() + countDiff); Log.d(LOG_TAG, "backPaginate : jump to " + expectedPos); //private int mFirstVisibleRowY = INVALID_VIEW_Y_POS; if (fillHistory || (UNDEFINED_VIEW_Y_POS == mFirstVisibleRowY)) { // do not use count because some messages are not displayed // so we compute the new pos mMessageListView.setSelection(expectedPos); } else { mMessageListView.setSelectionFromTop(expectedPos, -mFirstVisibleRowY); } } // Test if a back pagination can be done. // countDiff == 0 is not relevant // because the server can return an empty chunk // but the start and the end tokens are not equal. // It seems often happening with the room visibility feature if (mMatrixMessagesFragment.canBackPaginate()) { Log.d(LOG_TAG, "backPaginate again"); mMessageListView.post(new Runnable() { @Override public void run() { mLockFwdPagination = false; mIsBackPaginating = false; mMessageListView.post(new Runnable() { @Override public void run() { // back paginate until getting something to display if (0 == countDiff) { Log.d(LOG_TAG, "backPaginate again because there was nothing in the current chunk"); backPaginate(fillHistory); } else if (fillHistory) { if ((mMessageListView.getVisibility() == View.VISIBLE) && mMessageListView.getFirstVisiblePosition() < 10) { Log.d(LOG_TAG, "backPaginate : fill history"); backPaginate(fillHistory); } else { Log.d(LOG_TAG, "backPaginate : history should be filled"); hideLoadingBackProgress(); mIsInitialSyncing = false; setMessageListViewScrollListener(); } } else { hideLoadingBackProgress(); } } }); } }); } else { Log.d(LOG_TAG, "no more backPaginate"); setMessageListViewScrollListener(); hideLoadingBackProgress(); mIsBackPaginating = false; mLockFwdPagination = false; } } }); } // the request will be auto restarted when a valid network will be found @Override public void onNetworkError(Exception e) { onPaginateRequestError(e); } @Override public void onMatrixError(MatrixError e) { onPaginateRequestError(e); } @Override public void onUnexpectedError(Exception e) { onPaginateRequestError(e); } }); if (mIsBackPaginating && (null != getActivity())) { Log.d(LOG_TAG, "backPaginate : starts"); showLoadingBackProgress(); } else { Log.d(LOG_TAG, "requestHistory : nothing to do"); } } /** * Cancel the catching requests. */ public void cancelCatchingRequests() { mPattern = null; if (null != mEventTimeLine) { mEventTimeLine.cancelPaginationRequest(); } mIsInitialSyncing = false; mIsBackPaginating = false; mIsFwdPaginating = false; mLockBackPagination = false; mLockFwdPagination = false; hideInitLoading(); hideLoadingBackProgress(); hideLoadingForwardProgress(); } //============================================================================================================== // MatrixMessagesFragment methods //============================================================================================================== @Override public void onEvent(final Event event, final EventTimeline.Direction direction, final RoomState roomState) { if (direction == EventTimeline.Direction.FORWARDS) { getUiHandler().post(new Runnable() { @Override public void run() { if (Event.EVENT_TYPE_REDACTION.equals(event.getType())) { MessageRow messageRow = mAdapter.getMessageRow(event.getRedacts()); if (null != messageRow) { Event prunedEvent = mSession.getDataHandler().getStore().getEvent(event.getRedacts(), event.roomId); if (null == prunedEvent) { mAdapter.removeEventById(event.getRedacts()); } else { messageRow.updateEvent(prunedEvent); JsonObject content = messageRow.getEvent().getContentAsJsonObject(); boolean hasToRemoved = (null == content) || (null == content.entrySet()) || (0 == content.entrySet().size()); // test if the event is displayable // GA issue : the activity can be null if (!hasToRemoved && (null != getActivity())) { EventDisplay eventDisplay = new EventDisplay(getActivity(), prunedEvent, roomState); hasToRemoved = TextUtils.isEmpty(eventDisplay.getTextualDisplay()); } // event is removed if it has no more content. if (hasToRemoved) { mAdapter.removeEventById(prunedEvent.eventId); } } mAdapter.notifyDataSetChanged(); } } else if (Event.EVENT_TYPE_TYPING.equals(event.getType())) { if (null != mRoom) { mAdapter.setTypingUsers(mRoom.getTypingUsers()); } } else { if (canAddEvent(event)) { // refresh the listView only when it is a live timeline or a search mAdapter.add(new MessageRow(event, roomState), (null == mEventTimeLine) || mEventTimeLine.isLiveTimeline()); } } } }); } else { if (canAddEvent(event)) { mAdapter.addToFront(event, roomState); } } } @Override public void onLiveEventsChunkProcessed() { // NOP } @Override public void onReceiptEvent(List<String> senderIds) { // avoid useless refresh boolean shouldRefresh = true; try { IMXStore store = mSession.getDataHandler().getStore(); int firstPos = mMessageListView.getFirstVisiblePosition(); int lastPos = mMessageListView.getLastVisiblePosition(); ArrayList<String> senders = new ArrayList<>(); ArrayList<String> eventIds = new ArrayList<>(); for (int index = firstPos; index <= lastPos; index++) { MessageRow row = mAdapter.getItem(index); senders.add(row.getEvent().getSender()); eventIds.add(row.getEvent().eventId); } shouldRefresh = false; // check if the receipt will trigger a refresh for (String sender : senderIds) { if (!TextUtils.equals(sender, mSession.getMyUserId())) { ReceiptData receipt = store.getReceipt(mRoom.getRoomId(), sender); // sanity check if (null != receipt) { // test if the event is displayed int pos = eventIds.indexOf(receipt.eventId); // if displayed if (pos >= 0) { // the sender is not displayed as a reader (makes sense...) shouldRefresh = !TextUtils.equals(senders.get(pos), sender); if (shouldRefresh) { break; } } } } } } catch (Exception e) { Log.e(LOG_TAG, "onReceiptEvent failed with " + e.getLocalizedMessage()); } if (shouldRefresh) { mAdapter.notifyDataSetChanged(); } } @Override public void onInitialMessagesLoaded() { Log.d(LOG_TAG, "onInitialMessagesLoaded"); // Jump to the bottom of the list getUiHandler().post(new Runnable() { @Override public void run() { // should never happen but reported by GA if (null == mMessageListView) { return; } hideLoadingBackProgress(); if (null == mMessageListView.getAdapter()) { mMessageListView.setAdapter(mAdapter); } if ((null == mEventTimeLine) || mEventTimeLine.isLiveTimeline()) { if (mAdapter.getCount() > 0) { // refresh the list only at the end of the sync // else the one by one message refresh gives a weird UX // The application is almost frozen during the mAdapter.notifyDataSetChanged(); if (mScrollToIndex >= 0) { mMessageListView.setSelection(mScrollToIndex); mScrollToIndex = -1; } else { mMessageListView.setSelection(mAdapter.getCount() - 1); } } // fill the page mMessageListView.post(new Runnable() { @Override public void run() { if ((mMessageListView.getVisibility() == View.VISIBLE) && mMessageListView.getFirstVisiblePosition() < 10) { Log.d(LOG_TAG, "onInitialMessagesLoaded : fill history"); backPaginate(true); } else { Log.d(LOG_TAG, "onInitialMessagesLoaded : history should be filled"); mIsInitialSyncing = false; setMessageListViewScrollListener(); } } }); } else { Log.d(LOG_TAG, "onInitialMessagesLoaded : default behaviour"); if ((0 != mAdapter.getCount()) && (mScrollToIndex > 0)) { mAdapter.notifyDataSetChanged(); mMessageListView.setSelection(mScrollToIndex); mScrollToIndex = -1; mMessageListView.post(new Runnable() { @Override public void run() { mIsInitialSyncing = false; setMessageListViewScrollListener(); } }); } else { mIsInitialSyncing = false; setMessageListViewScrollListener(); } } } }); } @Override public EventTimeline getEventTimeLine() { return mEventTimeLine; } @Override public void onTimelineInitialized() { mMessageListView.post(new Runnable() { @Override public void run() { mLockFwdPagination = false; mIsInitialSyncing = false; // search the event pos in the adapter // some events are not displayed so the added events count cannot be used. int eventPos = 0; for (; eventPos < mAdapter.getCount(); eventPos++) { if (TextUtils.equals(mAdapter.getItem(eventPos).getEvent().eventId, mEventId)) { break; } } View parentView = (View) mMessageListView.getParent(); mAdapter.notifyDataSetChanged(); mMessageListView.setAdapter(mAdapter); // center the message in the mMessageListView.setSelectionFromTop(eventPos, parentView.getHeight() / 2); } }); } @Override public RoomPreviewData getRoomPreviewData() { if (null != getActivity()) { // test if the listener has bee retrieved if (null == mRoomPreviewDataListener) { try { mRoomPreviewDataListener = (IRoomPreviewDataListener) getActivity(); } catch (ClassCastException e) { Log.e(LOG_TAG, "getRoomPreviewData failed with " + e.getLocalizedMessage()); } } if (null != mRoomPreviewDataListener) { return mRoomPreviewDataListener.getRoomPreviewData(); } } return null; } @Override public void onRoomFlush() { mAdapter.clear(); } /*** * MessageAdapter listener ***/ @Override public void onRowClick(int position) { } @Override public boolean onRowLongClick(int position) { return false; } @Override public void onContentClick(int position) { } @Override public boolean onContentLongClick(int position) { return false; } @Override public void onAvatarClick(String userId) { } @Override public boolean onAvatarLongClick(String userId) { return false; } @Override public void onSenderNameClick(String userId, String displayName) { } @Override public void onMediaDownloaded(int position) { } @Override public void onReadReceiptClick(String eventId, String userId, ReceiptData receipt) { } @Override public boolean onReadReceiptLongClick(String eventId, String userId, ReceiptData receipt) { return false; } @Override public void onMoreReadReceiptClick(String eventId) { } @Override public boolean onMoreReadReceiptLongClick(String eventId) { return false; } @Override public void onURLClick(Uri uri) { if (null != uri) { Intent intent = new Intent(Intent.ACTION_VIEW, uri); intent.putExtra(Browser.EXTRA_APPLICATION_ID, getActivity().getPackageName()); getActivity().startActivity(intent); } } @Override public boolean shouldHighlightEvent(Event event) { String eventId = event.eventId; // cache the dedicated rule because it is slow to find them out Object ruleAsVoid = mBingRulesByEventId.get(eventId); if (null != ruleAsVoid) { if (ruleAsVoid instanceof BingRule) { return ((BingRule) ruleAsVoid).shouldHighlight(); } return false; } boolean res = false; BingRule rule = mSession.getDataHandler().getBingRulesManager().fulfilledBingRule(event); if (null != rule) { res = rule.shouldHighlight(); mBingRulesByEventId.put(eventId, rule); } else { mBingRulesByEventId.put(eventId, eventId); } return res; } @Override public void onMatrixUserIdClick(String userId) { } @Override public void onRoomAliasClick(String roomAlias) { } @Override public void onRoomIdClick(String roomId) { } @Override public void onMessageIdClick(String messageId) { } private int mInvalidIndexesCount = 0; @Override public void onInvalidIndexes() { mInvalidIndexesCount++; // it should happen once // else we assume that the adapter is really corrupted // It seems better to close the linked activity to avoid infinite refresh. if (1 == mInvalidIndexesCount) { mMessageListView.post(new Runnable() { @Override public void run() { mAdapter.notifyDataSetChanged(); } }); } else { mMessageListView.post(new Runnable() { @Override public void run() { if (null != getActivity()) { getActivity().finish(); } } }); } } //============================================================================================================== // search methods //============================================================================================================== /** * Cancel the current search */ protected void cancelSearch() { mPattern = null; } /** * Search the pattern on a pagination server side. */ public void requestSearchHistory() { // there is no more server message if (TextUtils.isEmpty(mNextBatch)) { mIsBackPaginating = false; return; } mIsBackPaginating = true; final int firstPos = mMessageListView.getFirstVisiblePosition(); final String fPattern = mPattern; final int countBeforeUpdate = mAdapter.getCount(); showLoadingBackProgress(); List<String> roomIds = null; if (null != mRoom) { roomIds = Arrays.asList(mRoom.getRoomId()); } ApiCallback<SearchResponse> callback = new ApiCallback<SearchResponse>() { @Override public void onSuccess(final SearchResponse searchResponse) { // check that the pattern was not modified before the end of the search if (TextUtils.equals(mPattern, fPattern)) { List<SearchResult> searchResults = searchResponse.searchCategories.roomEvents.results; // is there any result to display if (0 != searchResults.size()) { mAdapter.setNotifyOnChange(false); for (SearchResult searchResult : searchResults) { MessageRow row = new MessageRow(searchResult.result, (null == mRoom) ? null : mRoom.getState()); mAdapter.insert(row, 0); } mNextBatch = searchResponse.searchCategories.roomEvents.nextBatch; // Scroll the list down to where it was before adding rows to the top getUiHandler().post(new Runnable() { @Override public void run() { final int expectedFirstPos = firstPos + (mAdapter.getCount() - countBeforeUpdate); mAdapter.notifyDataSetChanged(); // trick to avoid that the list jump to the latest item. mMessageListView.setAdapter(mMessageListView.getAdapter()); // do not use count because some messages are not displayed // so we compute the new pos mMessageListView.setSelection(expectedFirstPos); mMessageListView.post(new Runnable() { @Override public void run() { mIsBackPaginating = false; // fill the history if (mMessageListView.getFirstVisiblePosition() <= 2) { requestSearchHistory(); } } }); } }); } else { mIsBackPaginating = false; } hideLoadingBackProgress(); } } private void onError() { mIsBackPaginating = false; hideLoadingBackProgress(); } // the request will be auto restarted when a valid network will be found @Override public void onNetworkError(Exception e) { Log.e(LOG_TAG, "Network error: " + e.getMessage()); onError(); } @Override public void onMatrixError(MatrixError e) { Log.e(LOG_TAG, "Matrix error" + " : " + e.errcode + " - " + e.getLocalizedMessage()); onError(); } @Override public void onUnexpectedError(Exception e) { Log.e(LOG_TAG, "onUnexpectedError error" + e.getMessage()); onError(); } }; if (mIsMediaSearch) { mSession.searchMediasByName(mPattern, roomIds, mNextBatch, callback); } else { mSession.searchMessagesByText(mPattern, roomIds, mNextBatch, callback); } } /** * Manage the search response. * * @param searchResponse the search response * @param onSearchResultListener the search result listener */ protected void onSearchResponse(final SearchResponse searchResponse, final OnSearchResultListener onSearchResultListener) { List<SearchResult> searchResults = searchResponse.searchCategories.roomEvents.results; ArrayList<MessageRow> messageRows = new ArrayList<>(searchResults.size()); for (SearchResult searchResult : searchResults) { RoomState roomState = null; if (null != mRoom) { roomState = mRoom.getState(); } if (null == roomState) { Room room = mSession.getDataHandler().getStore().getRoom(searchResult.result.roomId); if (null != room) { roomState = room.getState(); } } boolean isValidMessage = false; if ((null != searchResult.result) && (null != searchResult.result.getContent())) { JsonObject object = searchResult.result.getContentAsJsonObject(); if (null != object) { isValidMessage = (0 != object.entrySet().size()); } } if (isValidMessage) { messageRows.add(new MessageRow(searchResult.result, roomState)); } } Collections.reverse(messageRows); mAdapter.clear(); mAdapter.addAll(messageRows); mNextBatch = searchResponse.searchCategories.roomEvents.nextBatch; if (null != onSearchResultListener) { try { onSearchResultListener.onSearchSucceed(messageRows.size()); } catch (Exception e) { Log.e(LOG_TAG, "onSearchResponse failed with " + e.getLocalizedMessage()); } } } /** * Search a pattern in the messages. * * @param pattern the pattern to search * @param onSearchResultListener the search callback */ public void searchPattern(final String pattern, final OnSearchResultListener onSearchResultListener) { searchPattern(pattern, false, onSearchResultListener); } /** * Search a pattern in the messages. * * @param pattern the pattern to search (filename for a media message) * @param isMediaSearch true if is it is a media search. * @param onSearchResultListener the search callback */ public void searchPattern(final String pattern, boolean isMediaSearch, final OnSearchResultListener onSearchResultListener) { if (!TextUtils.equals(mPattern, pattern)) { mPattern = pattern; mIsMediaSearch = isMediaSearch; mAdapter.setSearchPattern(mPattern); // something to search if (!TextUtils.isEmpty(mPattern)) { List<String> roomIds = null; // sanity checks if (null != mRoom) { roomIds = Arrays.asList(mRoom.getRoomId()); } // ApiCallback<SearchResponse> searchCallback = new ApiCallback<SearchResponse>() { @Override public void onSuccess(final SearchResponse searchResponse) { getUiHandler().post(new Runnable() { @Override public void run() { // check that the pattern was not modified before the end of the search if (TextUtils.equals(mPattern, pattern)) { onSearchResponse(searchResponse, onSearchResultListener); } } }); } private void onError() { getUiHandler().post(new Runnable() { @Override public void run() { if (null != onSearchResultListener) { try { onSearchResultListener.onSearchFailed(); } catch (Exception e) { Log.e(LOG_TAG, "onSearchResultListener failed with " + e.getLocalizedMessage()); } } } }); } // the request will be auto restarted when a valid network will be found @Override public void onNetworkError(Exception e) { Log.e(LOG_TAG, "Network error: " + e.getMessage()); onError(); } @Override public void onMatrixError(MatrixError e) { Log.e(LOG_TAG, "Matrix error" + " : " + e.errcode + " - " + e.getLocalizedMessage()); onError(); } @Override public void onUnexpectedError(Exception e) { Log.e(LOG_TAG, "onUnexpectedError error" + e.getMessage()); onError(); } }; if (isMediaSearch) { mSession.searchMediasByName(mPattern, roomIds, null, searchCallback); } else { mSession.searchMessagesByText(mPattern, roomIds, null, searchCallback); } } } } }