/*
* Copyright 2014 OpenMarket 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.data;
import android.os.Looper;
import android.text.TextUtils;
import org.matrix.androidsdk.data.store.IMXStore;
import org.matrix.androidsdk.rest.callback.ApiCallback;
import org.matrix.androidsdk.rest.callback.SimpleApiCallback;
import org.matrix.androidsdk.rest.client.RoomsRestClient;
import org.matrix.androidsdk.rest.model.Event;
import org.matrix.androidsdk.rest.model.MatrixError;
import org.matrix.androidsdk.rest.model.TokensChunkResponse;
import org.matrix.androidsdk.util.Log;
import java.util.Collection;
import java.util.HashMap;
/**
* Layer for retrieving data either from the storage implementation, or from the server if the information is not available.
*/
public class DataRetriever {
private static final String LOG_TAG = "DataRetriever";
private RoomsRestClient mRestClient;
private HashMap<String, String> mPendingFordwardRequestTokenByRoomId = new HashMap<>();
private HashMap<String, String> mPendingBackwardRequestTokenByRoomId = new HashMap<>();
private HashMap<String, String> mPendingRemoteRequestTokenByRoomId = new HashMap<>();
public RoomsRestClient getRoomsRestClient() {
return mRestClient;
}
public void setRoomsRestClient(RoomsRestClient client) {
mRestClient = client;
}
/**
* Provides the cached messages for a dedicated roomId
* @param store the store.
* @param roomId the roomId
* @return the events list, null if the room does not exist
*/
public Collection<Event> getCachedRoomMessages(IMXStore store, final String roomId) {
return store.getRoomMessages(roomId);
}
/**
* Cancel any history requests for a dedicated room
* @param roomId the room id.
*/
public void cancelHistoryRequest(String roomId) {
Log.d(LOG_TAG, "## cancelHistoryRequest() : roomId " + roomId);
clearPendingToken(mPendingFordwardRequestTokenByRoomId, roomId);
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
}
/**
* Cancel any request history requests for a dedicated room
* @param roomId the room id.
*/
public void cancelRemoteHistoryRequest(String roomId) {
Log.d(LOG_TAG, "## cancelRemoteHistoryRequest() : roomId " + roomId);
clearPendingToken(mPendingRemoteRequestTokenByRoomId, roomId);
}
/**
* Trigger a back pagination for a dedicated room from Token.
* @param roomId the room Id
* @param token the start token.
* @param callback the callback
*/
private void backPaginate(final IMXStore store, final String roomId, final String token, final ApiCallback<TokensChunkResponse<Event>> callback) {
// reach the marker end
if (TextUtils.equals(token, Event.PAGINATE_BACK_TOKEN_END)) {
// nothing more to provide
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
// call the callback with a delay
// to reproduce the same behaviour as a network request.
// except for the initial request.
Runnable r = new Runnable() {
@Override
public void run() {
handler.postDelayed(new Runnable() {
public void run() {
callback.onSuccess(new TokensChunkResponse<Event>());
}
}, 0);
}
};
handler.post(r);
return;
}
Log.d(LOG_TAG, "## backPaginate() : starts for roomId " + roomId);
TokensChunkResponse<Event> storageResponse = store.getEarlierMessages(roomId, token, RoomsRestClient.DEFAULT_MESSAGES_PAGINATION_LIMIT);
putPendingToken(mPendingBackwardRequestTokenByRoomId, roomId, token);
if (storageResponse != null) {
final android.os.Handler handler = new android.os.Handler(Looper.getMainLooper());
final TokensChunkResponse<Event> fStorageResponse = storageResponse;
Log.d(LOG_TAG, "## backPaginate() : some data has been retrieved into the local storage (" + fStorageResponse.chunk.size() + " events)");
// call the callback with a delay
// to reproduce the same behaviour as a network request.
// except for the initial request.
Runnable r = new Runnable() {
@Override
public void run() {
handler.postDelayed(new Runnable() {
public void run() {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
Log.d(LOG_TAG, "## backPaginate() : local store roomId " + roomId + " token " + token + " vs " + expectedToken);
if (TextUtils.equals(expectedToken, token)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
callback.onSuccess(fStorageResponse);
}
}
}, 0);
}
};
Thread t = new Thread(r);
t.start();
}
else {
Log.d(LOG_TAG, "## backPaginate() : trigger a remote request");
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.BACKWARDS, RoomsRestClient.DEFAULT_MESSAGES_PAGINATION_LIMIT, new SimpleApiCallback<TokensChunkResponse<Event>>(callback) {
@Override
public void onSuccess(TokensChunkResponse<Event> events) {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
Log.d(LOG_TAG, "## backPaginate() succeeds : roomId " + roomId + " token " + token + " vs " + expectedToken);
if (TextUtils.equals(expectedToken, token)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
// Watch for the one event overlap
Event oldestEvent = store.getOldestEvent(roomId);
if (events.chunk.size() != 0) {
events.chunk.get(0).mToken = events.start;
// there is no more data on server side
if (null == events.end) {
events.end = Event.PAGINATE_BACK_TOKEN_END;
}
events.chunk.get(events.chunk.size() - 1).mToken = events.end;
Event firstReturnedEvent = events.chunk.get(0);
if ((oldestEvent != null) && (firstReturnedEvent != null)
&& TextUtils.equals(oldestEvent.eventId, firstReturnedEvent.eventId)) {
events.chunk.remove(0);
}
store.storeRoomEvents(roomId, events, EventTimeline.Direction.BACKWARDS);
}
Log.d(LOG_TAG, "## backPaginate() succeed : roomId " + roomId + " token " + token + " got " + events.chunk.size());
callback.onSuccess(events);
}
}
private void logErrorMessage(String expectedToken , String errorMessage) {
Log.e(LOG_TAG, "## backPaginate() failed : roomId " + roomId + " token " + token + " expected " + expectedToken + " with " + errorMessage);
}
@Override
public void onNetworkError(Exception e) {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
logErrorMessage(expectedToken, e.getMessage());
// dispatch only if it is expected
if (TextUtils.equals(token, expectedToken)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
callback.onNetworkError(e);
}
}
@Override
public void onMatrixError(MatrixError e) {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
logErrorMessage(expectedToken, e.getMessage());
// dispatch only if it is expected
if (TextUtils.equals(token, expectedToken)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
callback.onMatrixError(e);
}
}
@Override
public void onUnexpectedError(Exception e) {
String expectedToken = getPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
logErrorMessage(expectedToken, e.getMessage());
// dispatch only if it is expected
if (TextUtils.equals(token, expectedToken)) {
clearPendingToken(mPendingBackwardRequestTokenByRoomId, roomId);
callback.onUnexpectedError(e);
}
}
});
}
}
/**
* Trigger a forward pagination for a dedicated room from Token.
* @param roomId the room Id
* @param token the start token.
* @param callback the callback
*/
private void forwardPaginate(final IMXStore store, final String roomId, final String token, final ApiCallback<TokensChunkResponse<Event>> callback) {
putPendingToken(mPendingFordwardRequestTokenByRoomId, roomId, token);
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.FORWARDS, RoomsRestClient.DEFAULT_MESSAGES_PAGINATION_LIMIT, new SimpleApiCallback<TokensChunkResponse<Event>>(callback) {
@Override
public void onSuccess(TokensChunkResponse<Event> events) {
if (TextUtils.equals(getPendingToken(mPendingFordwardRequestTokenByRoomId, roomId), token)) {
clearPendingToken(mPendingFordwardRequestTokenByRoomId, roomId);
store.storeRoomEvents(roomId, events, EventTimeline.Direction.FORWARDS);
callback.onSuccess(events);
}
}
});
}
/**
* Request messages than the given token. These will come from storage if available, from the server otherwise.
* @param roomId the room id
* @param token the token to go back from. Null to start from live.
* @param direction the pagination direction
* @param callback the onComplete callback
*/
public void paginate(final IMXStore store, final String roomId, final String token, final EventTimeline.Direction direction, final ApiCallback<TokensChunkResponse<Event>> callback) {
if (direction == EventTimeline.Direction.BACKWARDS) {
backPaginate(store, roomId, token, callback);
} else {
forwardPaginate(store, roomId, token, callback);
}
}
/**
* Request events to the server. The local cache is not used.
* The events will not be saved in the local storage.
* @param roomId the room id
* @param token the token to go back from.
* @param paginationCount the number of events to retrieve.
* @param callback the onComplete callback
*/
public void requestServerRoomHistory(final String roomId, final String token, final int paginationCount, final ApiCallback<TokensChunkResponse<Event>> callback) {
putPendingToken(mPendingRemoteRequestTokenByRoomId, roomId, token);
mRestClient.getRoomMessagesFrom(roomId, token, EventTimeline.Direction.BACKWARDS, paginationCount, new SimpleApiCallback<TokensChunkResponse<Event>>(callback) {
@Override
public void onSuccess(TokensChunkResponse<Event> info) {
if (TextUtils.equals(getPendingToken(mPendingRemoteRequestTokenByRoomId, roomId), token)){
if (info.chunk.size() != 0) {
info.chunk.get(0).mToken = info.start;
info.chunk.get(info.chunk.size() - 1).mToken = info.end;
}
clearPendingToken(mPendingRemoteRequestTokenByRoomId, roomId);
callback.onSuccess(info);
}
}
});
}
//==============================================================================================================
// Pending token management
//==============================================================================================================
/**
* Clear token for a dedicated room
* @param dict the token cache
* @param roomId the room id
*/
private void clearPendingToken(HashMap<String, String> dict, String roomId) {
Log.d(LOG_TAG, "## clearPendingToken() : roomId " + roomId);
if (null != roomId) {
synchronized (dict) {
dict.remove(roomId);
}
}
}
/**
* Get the pending token for a dedicated room
* @param dict the token cache
* @param roomId the room Id
* @return the token
*/
private String getPendingToken(HashMap<String, String> dict, String roomId) {
String expectedToken = "Not a valid token";
synchronized (dict) {
// token == null is a valid value
if(dict.containsKey(roomId)) {
expectedToken = dict.get(roomId);
if (TextUtils.isEmpty(expectedToken)) {
expectedToken = null;
}
}
}
Log.d(LOG_TAG, "## getPendingToken() : roomId " + roomId + " token " + expectedToken);
return expectedToken;
}
/**
* Store a token for a dedicated room
* @param dict the token cache
* @param roomId the room id
* @param token the token
*/
private void putPendingToken(HashMap<String, String> dict, String roomId, String token) {
Log.d(LOG_TAG, "## putPendingToken() : roomId " + roomId + " token " + token);
synchronized (dict) {
// null is allowed for a request
if (null == token) {
dict.put(roomId, "");
} else {
dict.put(roomId, token);
}
}
}
}