/*
* 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.content.Context;
import android.os.Bundle;
import android.os.Looper;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.text.TextUtils;
import org.matrix.androidsdk.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.matrix.androidsdk.MXSession;
import org.matrix.androidsdk.data.EventTimeline;
import org.matrix.androidsdk.data.Room;
import org.matrix.androidsdk.data.RoomPreviewData;
import org.matrix.androidsdk.data.RoomState;
import org.matrix.androidsdk.listeners.IMXEventListener;
import org.matrix.androidsdk.listeners.MXEventListener;
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.MatrixError;
import org.matrix.androidsdk.rest.model.RoomMember;
import org.matrix.androidsdk.rest.model.RoomResponse;
import org.matrix.androidsdk.rest.model.Sync.RoomSync;
import org.matrix.androidsdk.rest.model.Sync.RoomSyncState;
import org.matrix.androidsdk.rest.model.Sync.RoomSyncTimeline;
import java.util.List;
/**
* A non-UI fragment containing logic for extracting messages from a room, including handling
* pagination. For a UI implementation of this, see {@link MatrixMessageListFragment}.
*/
public class MatrixMessagesFragment extends Fragment {
private static final String LOG_TAG = "MatrixMessagesFragment";
/**
* The room ID to get messages for.
* Fragment argument: String.
*/
public static final String ARG_ROOM_ID = "org.matrix.androidsdk.fragments.MatrixMessageFragment.ARG_ROOM_ID";
public static MatrixMessagesFragment newInstance(MXSession session, String roomId, MatrixMessagesListener listener) {
MatrixMessagesFragment fragment = new MatrixMessagesFragment();
Bundle args = new Bundle();
if (null == listener) {
throw new RuntimeException("Must define a listener.");
}
if (null == session) {
throw new RuntimeException("Must define a session.");
}
if (null != roomId) {
args.putString(ARG_ROOM_ID, roomId);
}
fragment.setArguments(args);
fragment.setMatrixMessagesListener(listener);
fragment.setMXSession(session);
return fragment;
}
public interface MatrixMessagesListener {
void onEvent(Event event, EventTimeline.Direction direction, RoomState roomState);
void onLiveEventsChunkProcessed();
void onReceiptEvent(List<String> senderIds);
void onRoomFlush();
EventTimeline getEventTimeLine();
void onTimelineInitialized();
/**
* Called when the first batch of messages is loaded.
*/
void onInitialMessagesLoaded();
// UI events
void showInitLoading();
void hideInitLoading();
// get the room preview data
RoomPreviewData getRoomPreviewData();
}
// The listener to send messages back
private MatrixMessagesListener mMatrixMessagesListener;
// The adapted listener to register to the SDK
private final IMXEventListener mEventListener = new MXEventListener() {
@Override
public void onLiveEventsChunkProcessed(String fromToken, String toToken) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.onLiveEventsChunkProcessed();
}
}
@Override
public void onReceiptEvent(String roomId, List<String> senderIds) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.onReceiptEvent(senderIds);
}
}
@Override
public void onRoomFlush(String roomId) {
if (null != mMatrixMessagesListener) {
if (mEventTimeline.isLiveTimeline()) {
// clear the history
mMatrixMessagesListener.onRoomFlush();
// init the timeline
mEventTimeline.initHistory();
// fill the screen
requestInitialHistory();
}
}
}
};
private final EventTimeline.EventTimelineListener mEventTimelineListener = new EventTimeline.EventTimelineListener() {
@Override
public void onEvent(Event event, EventTimeline.Direction direction, RoomState roomState) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.onEvent(event, direction, roomState);
}
}
};
private Context mContext;
private MXSession mSession;
private Room mRoom;
public boolean mKeepRoomHistory;
private EventTimeline mEventTimeline;
@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 v = super.onCreateView(inflater, container, savedInstanceState);
// the requests are done in onCreateView instead of onActivityCreated to speed up in the events request
// it saves only few ms but it reduces the white screen flash.
mContext = getActivity().getApplicationContext();
String roomId = getArguments().getString(ARG_ROOM_ID);
// this code should never be called
// but we've got some crashes when the session was null
// so try to find it from the fragments call stack.
if (null == mSession) {
List<Fragment> fragments = null;
FragmentManager fm = getActivity().getSupportFragmentManager();
if (null != fm) {
fragments = fm.getFragments();
}
if (null != fragments) {
for (Fragment fragment : fragments) {
if (fragment instanceof MatrixMessageListFragment) {
mMatrixMessagesListener = (MatrixMessageListFragment) fragment;
mSession = ((MatrixMessageListFragment) fragment).getSession();
}
}
}
}
if (mSession == null) {
throw new RuntimeException("Must have valid default MXSession.");
}
// get the timelime
if (null == mEventTimeline) {
mEventTimeline = mMatrixMessagesListener.getEventTimeLine();
} else {
mEventTimeline.addEventTimelineListener(mEventTimelineListener);
// the room has already been initialized
sendInitialMessagesLoaded();
return v;
}
if (null != mEventTimeline) {
mEventTimeline.addEventTimelineListener(mEventTimelineListener);
mRoom = mEventTimeline.getRoom();
}
// retrieve the room.
if (null == mRoom) {
// check if this room has been joined, if not, join it then get messages.
mRoom = mSession.getDataHandler().getRoom(roomId);
}
// GA reported some weird room content
// so ensure that the room fields are properly initialized
mSession.getDataHandler().checkRoom(mRoom);
// display the message history around a dedicated message
if ((null != mEventTimeline) && !mEventTimeline.isLiveTimeline() && (null != mEventTimeline.getInitialEventId())) {
initializeTimeline();
} else {
boolean joinedRoom = false;
// does the room already exist ?
if ((mRoom != null) && (null != mEventTimeline)) {
// init the history
mEventTimeline.initHistory();
// check if some required fields are initialized
// else, the joining could have been half broken (network error)
if (null != mRoom.getState().creator) {
RoomMember self = mRoom.getMember(mSession.getCredentials().userId);
if (self != null && RoomMember.MEMBERSHIP_JOIN.equals(self.membership)) {
joinedRoom = true;
}
}
mRoom.addEventListener(mEventListener);
// room preview mode
// i.e display the room messages without joining the room
if (!mEventTimeline.isLiveTimeline()) {
previewRoom();
}
// join the room is not yet joined
else if (!joinedRoom) {
Log.d(LOG_TAG, "Joining room >> " + roomId);
joinRoom();
} else {
// the room is already joined
// fill the messages list
requestInitialHistory();
}
} else {
sendInitialMessagesLoaded();
}
}
return v;
}
@Override
public void onDestroy() {
super.onDestroy();
if ((null != mRoom) && (null != mEventTimeline)) {
if (mEventTimeline.isLiveTimeline()) {
mRoom.removeEventListener(mEventListener);
}
mEventTimeline.removeEventTimelineListener(mEventTimelineListener);
}
}
/**
* Warn the listener that this fragment is ready.
*/
private void sendInitialMessagesLoaded() {
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
// add a delay to avoid calling MatrixListFragment before it is fully initialized
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.onInitialMessagesLoaded();
}
}
}, 100);
}
/**
* Trigger a room preview i.e trigger an initial sync before filling the message list.
*/
private void previewRoom() {
Log.d(LOG_TAG, "Make a room preview of " + mRoom.getRoomId());
if (null != mMatrixMessagesListener) {
RoomPreviewData roomPreviewData = mMatrixMessagesListener.getRoomPreviewData();
if (null != roomPreviewData) {
if (null != roomPreviewData.getRoomResponse()) {
Log.d(LOG_TAG, "A preview data is provided with sync response");
RoomResponse roomResponse = roomPreviewData.getRoomResponse();
// initialize the timeline with the initial sync response
RoomSync roomSync = new RoomSync();
roomSync.state = new RoomSyncState();
roomSync.state.events = roomResponse.state;
roomSync.timeline = new RoomSyncTimeline();
roomSync.timeline.events = roomResponse.messages.chunk;
roomSync.timeline.limited = true;
roomSync.timeline.prevBatch = roomResponse.messages.end;
mEventTimeline.handleJoinedRoomSync(roomSync, true);
Log.d(LOG_TAG, "The room preview is done -> fill the room history");
requestInitialHistory();
} else {
Log.d(LOG_TAG, "A preview data is provided with no sync response : assume that it is not possible to get a room preview");
if (null != getActivity()) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.hideInitLoading();
}
}
}
return;
}
}
mSession.getRoomsApiClient().initialSync(mRoom.getRoomId(), new ApiCallback<RoomResponse>() {
@Override
public void onSuccess(RoomResponse roomResponse) {
// initialize the timeline with the initial sync response
RoomSync roomSync = new RoomSync();
roomSync.state = new RoomSyncState();
roomSync.state.events = roomResponse.state;
roomSync.timeline = new RoomSyncTimeline();
roomSync.timeline.events = roomResponse.messages.chunk;
mEventTimeline.handleJoinedRoomSync(roomSync, true);
Log.d(LOG_TAG, "The room preview is done -> fill the room history");
requestInitialHistory();
}
private void onError(String errorMessage) {
Log.e(LOG_TAG, "The room preview of " + mRoom.getRoomId() + "failed " + errorMessage);
if (null != getActivity()) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.hideInitLoading();
}
}
}
@Override
public void onNetworkError(Exception e) {
onError(e.getLocalizedMessage());
}
@Override
public void onMatrixError(MatrixError e) {
onError(e.getLocalizedMessage());
}
@Override
public void onUnexpectedError(Exception e) {
onError(e.getLocalizedMessage());
}
});
}
/**
* Display the InitializeTimeline error.
*
* @param error the error
*/
protected void displayInitializeTimelineError(Object error) {
String errorMessage = "";
if (error instanceof MatrixError) {
errorMessage = ((MatrixError) error).getLocalizedMessage();
} else if (error instanceof Exception) {
errorMessage = ((Exception) error).getLocalizedMessage();
}
if (!TextUtils.isEmpty(errorMessage)) {
Log.d(LOG_TAG, "displayInitializeTimelineError : " + errorMessage);
Toast.makeText(mContext, errorMessage, Toast.LENGTH_SHORT).show();
}
}
/**
* Initialize the timeline to fill the screen
*/
private void initializeTimeline() {
Log.d(LOG_TAG, "initializeTimeline");
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.showInitLoading();
}
mEventTimeline.resetPaginationAroundInitialEvent(30 * 2, new ApiCallback<Void>() {
@Override
public void onSuccess(Void info) {
Log.d(LOG_TAG, "initializeTimeline is done");
if (null != getActivity()) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.hideInitLoading();
mMatrixMessagesListener.onTimelineInitialized();
}
sendInitialMessagesLoaded();
}
}
private void onError() {
Log.d(LOG_TAG, "initializeTimeline fails");
if (null != getActivity()) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.hideInitLoading();
mMatrixMessagesListener.onTimelineInitialized();
}
}
}
@Override
public void onNetworkError(Exception e) {
displayInitializeTimelineError(e);
onError();
}
@Override
public void onMatrixError(MatrixError e) {
displayInitializeTimelineError(e);
onError();
}
@Override
public void onUnexpectedError(Exception e) {
displayInitializeTimelineError(e);
onError();
}
});
}
/**
* Request messages in this room upon entering.
*/
protected void requestInitialHistory() {
Log.d(LOG_TAG, "requestInitialHistory " + mRoom.getRoomId());
// the initial sync will be retrieved when a network connection will be found
boolean start = backPaginate(new SimpleApiCallback<Integer>(getActivity()) {
@Override
public void onSuccess(Integer info) {
Log.d(LOG_TAG, "requestInitialHistory onSuccess");
if (null != getActivity()) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.hideInitLoading();
mMatrixMessagesListener.onTimelineInitialized();
mMatrixMessagesListener.onInitialMessagesLoaded();
}
}
}
private void onError(String errorMessage) {
Log.e(LOG_TAG, "requestInitialHistory failed" + errorMessage);
if (null != getActivity()) {
Toast.makeText(mContext, errorMessage, Toast.LENGTH_LONG).show();
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.hideInitLoading();
}
}
}
@Override
public void onNetworkError(Exception e) {
onError(e.getLocalizedMessage());
}
@Override
public void onMatrixError(MatrixError e) {
onError(e.getLocalizedMessage());
}
@Override
public void onUnexpectedError(Exception e) {
onError(e.getLocalizedMessage());
}
});
if (start) {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.showInitLoading();
}
}
}
//==============================================================================================================
// Setters / getters
//==============================================================================================================
/**
* Set the listener which will be informed of matrix messages. This setter is provided so either
* a Fragment or an Activity can directly receive callbacks.
*
* @param listener the listener for this fragment
*/
public void setMatrixMessagesListener(MatrixMessagesListener listener) {
mMatrixMessagesListener = listener;
}
/**
* Set the MX session
*
* @param session the session.
*/
public void setMXSession(MXSession session) {
mSession = session;
}
//==============================================================================================================
// Room / timeline actions
//==============================================================================================================
/**
* Tells if a back pagination can be done.
*
* @return true if a back pagination can be done.
*/
public boolean canBackPaginate() {
if (null != mEventTimeline) {
return mEventTimeline.canBackPaginate();
} else {
return false;
}
}
/**
* Request earlier messages in this room.
*
* @param callback the callback
* @return true if the request is really started
*/
public boolean backPaginate(ApiCallback<Integer> callback) {
if (null != mEventTimeline) {
return mEventTimeline.backPaginate(callback);
} else {
return false;
}
}
/**
* Request the next events in the timeline.
*
* @param callback the callback
* @return true if the request is really started
*/
public boolean forwardPaginate(ApiCallback<Integer> callback) {
if ((null != mEventTimeline) && mEventTimeline.isLiveTimeline()) {
return mEventTimeline.forwardPaginate(callback);
} else {
return false;
}
}
/**
* Send an event in a room
*
* @param event the event
* @param callback the callback
*/
public void sendEvent(Event event, ApiCallback<Void> callback) {
if (null != mRoom) {
mRoom.sendEvent(event, callback);
}
}
/**
* Redact an event.
*
* @param eventId the event Id
* @param callback the callback.
*/
public void redact(String eventId, ApiCallback<Event> callback) {
if (null != mRoom) {
mRoom.redact(eventId, callback);
}
}
/**
* Join the room.
*/
private void joinRoom() {
if (null != mMatrixMessagesListener) {
mMatrixMessagesListener.showInitLoading();
}
Log.d(LOG_TAG, "joinRoom " + mRoom.getRoomId());
mRoom.join(new SimpleApiCallback<Void>(getActivity()) {
@Override
public void onSuccess(Void info) {
Log.d(LOG_TAG, "joinRoom succeeds");
if (null != getActivity()) {
if (null != mMatrixMessagesListener) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
mMatrixMessagesListener.hideInitLoading();
mMatrixMessagesListener.onInitialMessagesLoaded();
} catch (Exception e) {
Log.e(LOG_TAG, "joinRoom callback fails " + e.getLocalizedMessage());
}
}
});
}
}
}
private void onError(String errorMessage) {
Log.e(LOG_TAG, "joinRoom error: " + errorMessage);
if (null != getActivity()) {
Toast.makeText(mContext, errorMessage, Toast.LENGTH_SHORT).show();
if (null != mMatrixMessagesListener) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mMatrixMessagesListener.hideInitLoading();
}
});
}
}
}
// the request will be automatically restarted when a valid network will be found
@Override
public void onNetworkError(Exception e) {
Log.e(LOG_TAG, "joinRoom Network error: " + e.getLocalizedMessage());
onError(e.getLocalizedMessage());
}
@Override
public void onMatrixError(MatrixError e) {
Log.e(LOG_TAG, "joinRoom onMatrixError : " + e.getLocalizedMessage());
onError(e.getLocalizedMessage());
}
@Override
public void onUnexpectedError(Exception e) {
Log.e(LOG_TAG, "joinRoom Override : " + e.getLocalizedMessage());
onError(e.getLocalizedMessage());
}
});
}
}