BayernMessenger/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/legacy/data/store/MXFileStore.java

2607 lines
92 KiB
Java

/*
* Copyright 2015 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector 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 im.vector.matrix.android.internal.legacy.data.store;
import android.content.Context;
import android.os.HandlerThread;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import im.vector.matrix.android.internal.auth.data.Credentials;
import im.vector.matrix.android.internal.legacy.data.Room;
import im.vector.matrix.android.internal.legacy.data.RoomAccountData;
import im.vector.matrix.android.internal.legacy.data.RoomState;
import im.vector.matrix.android.internal.legacy.data.RoomSummary;
import im.vector.matrix.android.internal.legacy.data.timeline.EventTimeline;
import im.vector.matrix.android.internal.legacy.rest.callback.ApiCallback;
import im.vector.matrix.android.internal.legacy.rest.model.Event;
import im.vector.matrix.android.internal.legacy.rest.model.ReceiptData;
import im.vector.matrix.android.internal.legacy.rest.model.RoomMember;
import im.vector.matrix.android.internal.legacy.rest.model.TokensChunkEvents;
import im.vector.matrix.android.internal.legacy.rest.model.User;
import im.vector.matrix.android.internal.legacy.rest.model.group.Group;
import im.vector.matrix.android.internal.legacy.rest.model.pid.ThirdPartyIdentifier;
import im.vector.matrix.android.internal.legacy.util.CompatUtil;
import im.vector.matrix.android.internal.legacy.util.ContentUtils;
import im.vector.matrix.android.internal.legacy.util.Log;
import im.vector.matrix.android.internal.legacy.util.MXOsHandler;
/**
* An in-file IMXStore.
*/
public class MXFileStore extends MXMemoryStore {
private static final String LOG_TAG = MXFileStore.class.getSimpleName();
// some constant values
private static final int MXFILE_VERSION = 22;
// ensure that there is enough messages to fill a tablet screen
private static final int MAX_STORED_MESSAGES_COUNT = 50;
private static final String MXFILE_STORE_FOLDER = "MXFileStore";
private static final String MXFILE_STORE_METADATA_FILE_NAME = "MXFileStore";
private static final String MXFILE_STORE_GZ_ROOMS_MESSAGES_FOLDER = "messages_gz";
private static final String MXFILE_STORE_ROOMS_TOKENS_FOLDER = "tokens";
private static final String MXFILE_STORE_GZ_ROOMS_STATE_FOLDER = "state_gz";
private static final String MXFILE_STORE_GZ_ROOMS_STATE_EVENTS_FOLDER = "state_rooms_events";
private static final String MXFILE_STORE_ROOMS_SUMMARY_FOLDER = "summary";
private static final String MXFILE_STORE_ROOMS_RECEIPT_FOLDER = "receipts";
private static final String MXFILE_STORE_ROOMS_ACCOUNT_DATA_FOLDER = "accountData";
private static final String MXFILE_STORE_USER_FOLDER = "users";
private static final String MXFILE_STORE_GROUPS_FOLDER = "groups";
// the data is read from the file system
private boolean mIsReady = false;
// tell if the post processing has been done
private boolean mIsPostProcessingDone = false;
// the read receipts are ready
private boolean mAreReceiptsReady = false;
// the store is currently opening
private boolean mIsOpening = false;
// List of rooms to save on [MXStore commit]
// filled with roomId
private Set<String> mRoomsToCommitForMessages;
private Set<String> mRoomsToCommitForStates;
//private Set<String> mRoomsToCommitForStatesEvents;
private Set<String> mRoomsToCommitForSummaries;
private Set<String> mRoomsToCommitForAccountData;
private Set<String> mRoomsToCommitForReceipts;
private Set<String> mUserIdsToCommit;
private Set<String> mGroupsToCommit;
// Flag to indicate metaData needs to be store
private boolean mMetaDataHasChanged = false;
// The path of the MXFileStore folders
private File mStoreFolderFile = null;
private File mGzStoreRoomsMessagesFolderFile = null;
private File mStoreRoomsTokensFolderFile = null;
private File mGzStoreRoomsStateFolderFile = null;
private File mGzStoreRoomsStateEventsFolderFile = null;
private File mStoreRoomsSummaryFolderFile = null;
private File mStoreRoomsMessagesReceiptsFolderFile = null;
private File mStoreRoomsAccountDataFolderFile = null;
private File mStoreUserFolderFile = null;
private File mStoreGroupsFolderFile = null;
// the background thread
private HandlerThread mHandlerThread = null;
private MXOsHandler mFileStoreHandler = null;
private boolean mIsKilled = false;
private boolean mIsNewStorage = false;
private boolean mAreUsersLoaded = false;
private long mPreloadTime = 0;
// the read receipts are asynchronously loaded
// keep a list of the remaining receipts to load
private final List<String> mRoomReceiptsToLoad = new ArrayList<>();
// store some stats
private final Map<String, Long> mStoreStats = new HashMap<>();
// True if file encryption is enabled
private final boolean mEnableFileEncryption;
/**
* Create the file store dirtrees
*/
private void createDirTree(String userId) {
// data path
// MXFileStore/userID/
// MXFileStore/userID/MXFileStore
// MXFileStore/userID/MXFileStore/Messages/
// MXFileStore/userID/MXFileStore/Tokens/
// MXFileStore/userID/MXFileStore/States/
// MXFileStore/userID/MXFileStore/Summaries/
// MXFileStore/userID/MXFileStore/receipt/<room Id>/receipts
// MXFileStore/userID/MXFileStore/accountData/
// MXFileStore/userID/MXFileStore/users/
// MXFileStore/userID/MXFileStore/groups/
// create the dirtree
mStoreFolderFile = new File(new File(mContext.getApplicationContext().getFilesDir(), MXFILE_STORE_FOLDER), userId);
if (!mStoreFolderFile.exists()) {
mStoreFolderFile.mkdirs();
}
mGzStoreRoomsMessagesFolderFile = new File(mStoreFolderFile, MXFILE_STORE_GZ_ROOMS_MESSAGES_FOLDER);
if (!mGzStoreRoomsMessagesFolderFile.exists()) {
mGzStoreRoomsMessagesFolderFile.mkdirs();
}
mStoreRoomsTokensFolderFile = new File(mStoreFolderFile, MXFILE_STORE_ROOMS_TOKENS_FOLDER);
if (!mStoreRoomsTokensFolderFile.exists()) {
mStoreRoomsTokensFolderFile.mkdirs();
}
mGzStoreRoomsStateFolderFile = new File(mStoreFolderFile, MXFILE_STORE_GZ_ROOMS_STATE_FOLDER);
if (!mGzStoreRoomsStateFolderFile.exists()) {
mGzStoreRoomsStateFolderFile.mkdirs();
}
mGzStoreRoomsStateEventsFolderFile = new File(mStoreFolderFile, MXFILE_STORE_GZ_ROOMS_STATE_EVENTS_FOLDER);
if (!mGzStoreRoomsStateEventsFolderFile.exists()) {
mGzStoreRoomsStateEventsFolderFile.mkdirs();
}
mStoreRoomsSummaryFolderFile = new File(mStoreFolderFile, MXFILE_STORE_ROOMS_SUMMARY_FOLDER);
if (!mStoreRoomsSummaryFolderFile.exists()) {
mStoreRoomsSummaryFolderFile.mkdirs();
}
mStoreRoomsMessagesReceiptsFolderFile = new File(mStoreFolderFile, MXFILE_STORE_ROOMS_RECEIPT_FOLDER);
if (!mStoreRoomsMessagesReceiptsFolderFile.exists()) {
mStoreRoomsMessagesReceiptsFolderFile.mkdirs();
}
mStoreRoomsAccountDataFolderFile = new File(mStoreFolderFile, MXFILE_STORE_ROOMS_ACCOUNT_DATA_FOLDER);
if (!mStoreRoomsAccountDataFolderFile.exists()) {
mStoreRoomsAccountDataFolderFile.mkdirs();
}
mStoreUserFolderFile = new File(mStoreFolderFile, MXFILE_STORE_USER_FOLDER);
if (!mStoreUserFolderFile.exists()) {
mStoreUserFolderFile.mkdirs();
}
mStoreGroupsFolderFile = new File(mStoreFolderFile, MXFILE_STORE_GROUPS_FOLDER);
if (!mStoreGroupsFolderFile.exists()) {
mStoreGroupsFolderFile.mkdirs();
}
}
/**
* Constructor
*
* @param credentials the expected getCredentials
* @param enableFileEncryption set to true to enable file encryption.
* @param context the context.
*/
public MXFileStore(Credentials credentials, boolean enableFileEncryption, Context context) {
setContext(context);
mEnableFileEncryption = enableFileEncryption;
mIsReady = false;
mCredentials = credentials;
mHandlerThread = new HandlerThread("MXFileStoreBackgroundThread_" + mCredentials.getUserId(), Thread.MIN_PRIORITY);
createDirTree(mCredentials.getUserId());
// updated data
mRoomsToCommitForMessages = new HashSet<>();
mRoomsToCommitForStates = new HashSet<>();
//mRoomsToCommitForStatesEvents = new HashSet<>();
mRoomsToCommitForSummaries = new HashSet<>();
mRoomsToCommitForAccountData = new HashSet<>();
mRoomsToCommitForReceipts = new HashSet<>();
mUserIdsToCommit = new HashSet<>();
mGroupsToCommit = new HashSet<>();
// check if the metadata file exists and if it is valid
loadMetaData();
if (null == mMetadata) {
deleteAllData(true);
}
// create the metadata file if it does not exist
// either there is no store
// or the store was not properly initialised (the application crashed during the initialsync)
if ((null == mMetadata) || (null == mMetadata.mAccessToken)) {
mIsNewStorage = true;
mIsOpening = true;
mHandlerThread.start();
mFileStoreHandler = new MXOsHandler(mHandlerThread.getLooper());
mMetadata = new MXFileStoreMetaData();
mMetadata.mUserId = mCredentials.getUserId();
mMetadata.mAccessToken = mCredentials.getAccessToken();
mMetadata.mVersion = MXFILE_VERSION;
mMetaDataHasChanged = true;
saveMetaData();
mEventStreamToken = null;
mIsOpening = false;
// nothing to load so ready to work
mIsReady = true;
mAreReceiptsReady = true;
}
}
/**
* Killed the background thread.
*
* @param isKilled killed status
*/
private void setIsKilled(boolean isKilled) {
synchronized (this) {
mIsKilled = isKilled;
}
}
/**
* @return true if the background thread is killed.
*/
private boolean isKilled() {
boolean isKilled;
synchronized (this) {
isKilled = mIsKilled;
}
return isKilled;
}
/**
* Save changes in the store.
* If the store uses permanent storage like database or file, it is the optimised time
* to commit the last changes.
*/
@Override
public void commit() {
// Save data only if metaData exists
if ((null != mMetadata) && (null != mMetadata.mAccessToken) && !isKilled()) {
Log.d(LOG_TAG, "++ Commit");
saveUsers();
saveGroups();
saveRoomsMessages();
saveRoomStates();
saveRoomStatesEvents();
saveSummaries();
saveRoomsAccountData();
saveReceipts();
saveMetaData();
Log.d(LOG_TAG, "-- Commit");
}
}
/**
* Open the store.
*/
@Override
public void open() {
super.open();
final long fLoadTimeT0 = System.currentTimeMillis();
// avoid concurrency call.
synchronized (this) {
if (!mIsReady && !mIsOpening && (null != mMetadata) && (null != mHandlerThread)) {
mIsOpening = true;
Log.e(LOG_TAG, "Open the store.");
// creation the background handler.
if (null == mFileStoreHandler) {
// avoid already started exception
// never succeeded to reproduce but it was reported in GA.
try {
mHandlerThread.start();
} catch (IllegalThreadStateException e) {
Log.e(LOG_TAG, "mHandlerThread is already started.", e);
// already started
return;
}
mFileStoreHandler = new MXOsHandler(mHandlerThread.getLooper());
}
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
Log.e(LOG_TAG, "Open the store in the background thread.");
String errorDescription = null;
boolean succeed = (mMetadata.mVersion == MXFILE_VERSION)
&& TextUtils.equals(mMetadata.mUserId, mCredentials.getUserId())
&& TextUtils.equals(mMetadata.mAccessToken, mCredentials.getAccessToken());
if (!succeed) {
errorDescription = "Invalid store content";
Log.e(LOG_TAG, errorDescription);
}
if (succeed) {
succeed &= loadRoomsMessages();
if (!succeed) {
errorDescription = "loadRoomsMessages fails";
Log.e(LOG_TAG, errorDescription);
} else {
Log.d(LOG_TAG, "loadRoomsMessages succeeds");
}
}
if (succeed) {
succeed &= loadGroups();
if (!succeed) {
errorDescription = "loadGroups fails";
Log.e(LOG_TAG, errorDescription);
} else {
Log.d(LOG_TAG, "loadGroups succeeds");
}
}
if (succeed) {
succeed &= loadRoomsState();
if (!succeed) {
errorDescription = "loadRoomsState fails";
Log.e(LOG_TAG, errorDescription);
} else {
Log.d(LOG_TAG, "loadRoomsState succeeds");
long t0 = System.currentTimeMillis();
Log.d(LOG_TAG, "Retrieve the users from the roomstate");
Collection<Room> rooms = getRooms();
for (Room room : rooms) {
Collection<RoomMember> members = room.getState().getLoadedMembers();
for (RoomMember member : members) {
updateUserWithRoomMemberEvent(member);
}
}
long delta = System.currentTimeMillis() - t0;
Log.d(LOG_TAG, "Retrieve " + mUsers.size() + " users with the room states in " + delta + " ms");
mStoreStats.put("Retrieve users", delta);
}
}
if (succeed) {
succeed &= loadSummaries();
if (!succeed) {
errorDescription = "loadSummaries fails";
Log.e(LOG_TAG, errorDescription);
} else {
Log.d(LOG_TAG, "loadSummaries succeeds");
// Check if the room summaries match to existing rooms.
// We could have more rooms than summaries because
// some of them are hidden.
// For example, the conference calls create a dummy room to manage
// the call events.
// check also if the user is a member of the room
// https://github.com/vector-im/riot-android/issues/1302
for (String roomId : mRoomSummaries.keySet()) {
Room room = getRoom(roomId);
if (null == room) {
succeed = false;
Log.e(LOG_TAG, "loadSummaries : the room " + roomId + " does not exist");
} else if (null == room.getMember(mCredentials.getUserId())) {
//succeed = false;
Log.e(LOG_TAG, "loadSummaries) : a summary exists for the roomId "
+ roomId + " but the user is not anymore a member");
}
}
}
}
if (succeed) {
succeed &= loadRoomsAccountData();
if (!succeed) {
errorDescription = "loadRoomsAccountData fails";
Log.e(LOG_TAG, errorDescription);
} else {
Log.d(LOG_TAG, "loadRoomsAccountData succeeds");
}
}
// do not expect having empty list
// assume that something is corrupted
if (!succeed) {
Log.e(LOG_TAG, "Fail to open the store in background");
// delete all data set mMetadata to null
// backup it to restore it
// the behaviour should be the same as first login
MXFileStoreMetaData tmpMetadata = mMetadata;
deleteAllData(true);
mRoomsToCommitForMessages = new HashSet<>();
mRoomsToCommitForStates = new HashSet<>();
//mRoomsToCommitForStatesEvents = new HashSet<>();
mRoomsToCommitForSummaries = new HashSet<>();
mRoomsToCommitForReceipts = new HashSet<>();
mMetadata = tmpMetadata;
// reported by GA
// i don't see which path could have triggered this issue
// mMetadata should only be null at file store loading
if (null == mMetadata) {
mMetadata = new MXFileStoreMetaData();
mMetadata.mUserId = mCredentials.getUserId();
mMetadata.mAccessToken = mCredentials.getAccessToken();
mMetaDataHasChanged = true;
} else {
mMetadata.mEventStreamToken = null;
}
mMetadata.mVersion = MXFILE_VERSION;
// the event stream token is put to zero to ensure ta
mEventStreamToken = null;
mAreReceiptsReady = true;
} else {
Log.d(LOG_TAG, "++ store stats");
Set<String> roomIds = mRoomEvents.keySet();
for (String roomId : roomIds) {
Room room = getRoom(roomId);
if ((null != room) && (null != room.getState())) {
int membersCount = room.getState().getLoadedMembers().size();
int eventsCount = mRoomEvents.get(roomId).size();
Log.d(LOG_TAG, " room " + roomId
+ " : (lazy loaded) membersCount " + membersCount
+ " - eventsCount " + eventsCount);
}
}
Log.d(LOG_TAG, "-- store stats");
}
// post processing
Log.d(LOG_TAG, "## open() : post processing.");
dispatchPostProcess(mCredentials.getUserId());
mIsPostProcessingDone = true;
synchronized (this) {
mIsReady = true;
}
mIsOpening = false;
if (!succeed && !mIsNewStorage) {
Log.e(LOG_TAG, "The store is corrupted.");
dispatchOnStoreCorrupted(mCredentials.getUserId(), errorDescription);
} else {
// extract the room states
mRoomReceiptsToLoad.addAll(listFiles(mStoreRoomsMessagesReceiptsFolderFile.list()));
mPreloadTime = System.currentTimeMillis() - fLoadTimeT0;
if (mMetricsListener != null) {
mMetricsListener.onStorePreloaded(mPreloadTime);
}
Log.d(LOG_TAG, "The store is opened.");
dispatchOnStoreReady(mCredentials.getUserId());
// load the following items with delay
// theses items are not required to be ready
// load the receipts
loadReceipts();
// load the users
loadUsers();
}
}
});
}
};
Thread t = new Thread(r);
t.start();
} else if (mIsReady) {
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
@Override
public void run() {
// should never happen
if (!mIsPostProcessingDone && !mIsNewStorage) {
Log.e(LOG_TAG, "## open() : is ready but the post processing was not yet done : please wait....");
return;
} else {
if (!mIsPostProcessingDone) {
Log.e(LOG_TAG, "## open() : is ready but the post processing was not yet done.");
dispatchPostProcess(mCredentials.getUserId());
mIsPostProcessingDone = true;
} else {
Log.e(LOG_TAG, "## open() when ready : the post processing is already done.");
}
dispatchOnStoreReady(mCredentials.getUserId());
mPreloadTime = System.currentTimeMillis() - fLoadTimeT0;
if (mMetricsListener != null) {
mMetricsListener.onStorePreloaded(mPreloadTime);
}
}
}
});
}
};
Thread t = new Thread(r);
t.start();
}
}
}
/**
* Check if the read receipts are ready to be used.
*
* @return true if they are ready.
*/
@Override
public boolean areReceiptsReady() {
boolean res;
synchronized (this) {
res = mAreReceiptsReady;
}
return res;
}
/**
* Provides the store preload time in milliseconds.
*
* @return the store preload time in milliseconds.
*/
@Override
public long getPreloadTime() {
return mPreloadTime;
}
/**
* Provides some store stats
*
* @return the store stats
*/
public Map<String, Long> getStats() {
return mStoreStats;
}
/**
* Close the store.
* Any pending operation must be complete in this call.
*/
@Override
public void close() {
Log.d(LOG_TAG, "Close the store");
super.close();
setIsKilled(true);
if (null != mHandlerThread) {
mHandlerThread.quit();
}
mHandlerThread = null;
}
/**
* Clear the store.
* Any pending operation must be complete in this call.
*/
@Override
public void clear() {
Log.d(LOG_TAG, "Clear the store");
super.clear();
deleteAllData(false);
}
/**
* Clear the filesystem storage.
*
* @param init true to init the filesystem dirtree
*/
private void deleteAllData(boolean init) {
// delete the dedicated directories
try {
ContentUtils.deleteDirectory(mStoreFolderFile);
if (init) {
createDirTree(mCredentials.getUserId());
}
} catch (Exception e) {
Log.e(LOG_TAG, "deleteAllData failed " + e.getMessage(), e);
}
if (init) {
initCommon();
}
mMetadata = null;
mEventStreamToken = null;
mAreUsersLoaded = true;
}
/**
* Indicate if the MXStore implementation stores data permanently.
* Permanent storage allows the SDK to make less requests at the startup.
*
* @return true if permanent.
*/
@Override
public boolean isPermanent() {
return true;
}
/**
* Check if the initial load is performed.
*
* @return true if it is ready.
*/
@Override
public boolean isReady() {
synchronized (this) {
return mIsReady;
}
}
/**
* @return true if the store is corrupted.
*/
@Override
public boolean isCorrupted() {
return false;
}
/**
* Delete a directory with its content
*
* @param directory the base directory
* @return the cache file size
*/
private long directorySize(File directory) {
long directorySize = 0;
if (directory.exists()) {
File[] files = directory.listFiles();
if (null != files) {
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory()) {
directorySize += directorySize(files[i]);
} else {
directorySize += files[i].length();
}
}
}
}
return directorySize;
}
/**
* Returns to disk usage size in bytes.
*
* @return disk usage size
*/
@Override
public long diskUsage() {
return directorySize(mStoreFolderFile);
}
/**
* Set the event stream token.
*
* @param token the event stream token
*/
@Override
public void setEventStreamToken(String token) {
Log.d(LOG_TAG, "Set token to " + token);
super.setEventStreamToken(token);
mMetaDataHasChanged = true;
}
@Override
public boolean setDisplayName(String displayName, long ts) {
return mMetaDataHasChanged = super.setDisplayName(displayName, ts);
}
@Override
public boolean setAvatarURL(String avatarURL, long ts) {
return mMetaDataHasChanged = super.setAvatarURL(avatarURL, ts);
}
@Override
public void setThirdPartyIdentifiers(List<ThirdPartyIdentifier> identifiers) {
// privacy
//Log.d(LOG_TAG, "Set setThirdPartyIdentifiers to " + identifiers);
Log.d(LOG_TAG, "Set setThirdPartyIdentifiers");
mMetaDataHasChanged = true;
super.setThirdPartyIdentifiers(identifiers);
}
@Override
public void setIgnoredUserIdsList(List<String> users) {
Log.d(LOG_TAG, "## setIgnoredUsers() : " + users);
mMetaDataHasChanged = true;
super.setIgnoredUserIdsList(users);
}
@Override
public void setDirectChatRoomsDict(Map<String, List<String>> directChatRoomsDict) {
Log.d(LOG_TAG, "## setDirectChatRoomsDict()");
mMetaDataHasChanged = true;
super.setDirectChatRoomsDict(directChatRoomsDict);
}
@Override
public void storeUser(User user) {
if (!TextUtils.equals(mCredentials.getUserId(), user.user_id)) {
mUserIdsToCommit.add(user.user_id);
}
super.storeUser(user);
}
@Override
public void flushRoomEvents(String roomId) {
super.flushRoomEvents(roomId);
mRoomsToCommitForMessages.add(roomId);
if ((null != mMetadata) && (null != mMetadata.mAccessToken) && !isKilled()) {
saveRoomsMessages();
}
}
@Override
public void storeRoomEvents(String roomId, TokensChunkEvents tokensChunkEvents, EventTimeline.Direction direction) {
boolean canStore = true;
// do not flush the room messages file
// when the user reads the room history and the events list size reaches its max size.
if (direction == EventTimeline.Direction.BACKWARDS) {
LinkedHashMap<String, Event> events = mRoomEvents.get(roomId);
if (null != events) {
canStore = (events.size() < MAX_STORED_MESSAGES_COUNT);
if (!canStore) {
Log.d(LOG_TAG, "storeRoomEvents : do not flush because reaching the max size");
}
}
}
super.storeRoomEvents(roomId, tokensChunkEvents, direction);
if (canStore) {
mRoomsToCommitForMessages.add(roomId);
}
}
/**
* Store a live room event.
*
* @param event The event to be stored.
*/
@Override
public void storeLiveRoomEvent(Event event) {
super.storeLiveRoomEvent(event);
mRoomsToCommitForMessages.add(event.roomId);
}
@Override
public void deleteEvent(Event event) {
super.deleteEvent(event);
mRoomsToCommitForMessages.add(event.roomId);
}
/**
* Delete the room messages and token files.
*
* @param roomId the room id.
*/
private void deleteRoomMessagesFiles(String roomId) {
// messages list
File messagesListFile = new File(mGzStoreRoomsMessagesFolderFile, roomId);
// remove the files
if (messagesListFile.exists()) {
try {
messagesListFile.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "deleteRoomMessagesFiles - messagesListFile failed " + e.getMessage(), e);
}
}
File tokenFile = new File(mStoreRoomsTokensFolderFile, roomId);
if (tokenFile.exists()) {
try {
tokenFile.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "deleteRoomMessagesFiles - tokenFile failed " + e.getMessage(), e);
}
}
}
@Override
public void deleteRoom(String roomId) {
Log.d(LOG_TAG, "deleteRoom " + roomId);
super.deleteRoom(roomId);
deleteRoomMessagesFiles(roomId);
deleteRoomStateFile(roomId);
deleteRoomSummaryFile(roomId);
deleteRoomReceiptsFile(roomId);
deleteRoomAccountDataFile(roomId);
}
@Override
public void deleteAllRoomMessages(String roomId, boolean keepUnsent) {
Log.d(LOG_TAG, "deleteAllRoomMessages " + roomId);
super.deleteAllRoomMessages(roomId, keepUnsent);
if (!keepUnsent) {
deleteRoomMessagesFiles(roomId);
}
deleteRoomSummaryFile(roomId);
mRoomsToCommitForMessages.add(roomId);
mRoomsToCommitForSummaries.add(roomId);
}
@Override
public void storeLiveStateForRoom(String roomId) {
super.storeLiveStateForRoom(roomId);
mRoomsToCommitForStates.add(roomId);
}
//================================================================================
// Summary management
//================================================================================
@Override
public void flushSummary(RoomSummary summary) {
super.flushSummary(summary);
mRoomsToCommitForSummaries.add(summary.getRoomId());
if ((null != mMetadata) && (null != mMetadata.mAccessToken) && !isKilled()) {
saveSummaries();
}
}
@Override
public void flushSummaries() {
super.flushSummaries();
// add any existing roomid to the list to save all
mRoomsToCommitForSummaries.addAll(mRoomSummaries.keySet());
if ((null != mMetadata) && (null != mMetadata.mAccessToken) && !isKilled()) {
saveSummaries();
}
}
@Override
public void storeSummary(RoomSummary summary) {
super.storeSummary(summary);
if ((null != summary) && (null != summary.getRoomId()) && !mRoomsToCommitForSummaries.contains(summary.getRoomId())) {
mRoomsToCommitForSummaries.add(summary.getRoomId());
}
}
//================================================================================
// users management
//================================================================================
/**
* Flush users list
*/
private void saveUsers() {
if (!mAreUsersLoaded) {
// please wait
return;
}
// some updated rooms ?
if ((mUserIdsToCommit.size() > 0) && (null != mFileStoreHandler)) {
// get the list
final Set<String> fUserIds = mUserIdsToCommit;
mUserIdsToCommit = new HashSet<>();
try {
final Set<User> fUsers;
synchronized (mUsers) {
fUsers = new HashSet<>(mUsers.values());
}
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!isKilled()) {
Log.d(LOG_TAG, "saveUsers " + fUserIds.size() + " users (" + fUsers.size() + " known ones)");
long start = System.currentTimeMillis();
// the users are split into groups to save time
Map<Integer, List<User>> usersGroups = new HashMap<>();
// finds the group for each updated user
for (String userId : fUserIds) {
User user;
synchronized (mUsers) {
user = mUsers.get(userId);
}
if (null != user) {
int hashCode = user.getStorageHashKey();
if (!usersGroups.containsKey(hashCode)) {
usersGroups.put(hashCode, new ArrayList<User>());
}
}
}
// gather the user to the dedicated group if they need to be updated
for (User user : fUsers) {
if (usersGroups.containsKey(user.getStorageHashKey())) {
usersGroups.get(user.getStorageHashKey()).add(user);
}
}
// save the groups
for (int hashKey : usersGroups.keySet()) {
writeObject("saveUser " + hashKey, new File(mStoreUserFolderFile, hashKey + ""), usersGroups.get(hashKey));
}
Log.d(LOG_TAG, "saveUsers done in " + (System.currentTimeMillis() - start) + " ms");
}
}
});
}
};
Thread t = new Thread(r);
t.start();
} catch (OutOfMemoryError oom) {
Log.e(LOG_TAG, "saveUser : cannot clone the users list" + oom.getMessage(), oom);
}
}
}
/**
* Load the user information from the filesystem..
*/
private void loadUsers() {
List<String> filenames = listFiles(mStoreUserFolderFile.list());
long start = System.currentTimeMillis();
List<User> users = new ArrayList<>();
// list the files
for (String filename : filenames) {
File messagesListFile = new File(mStoreUserFolderFile, filename);
Object usersAsVoid = readObject("loadUsers " + filename, messagesListFile);
if (null != usersAsVoid) {
try {
users.addAll((List<User>) usersAsVoid);
} catch (Exception e) {
Log.e(LOG_TAG, "loadUsers failed : " + e.toString(), e);
}
}
}
// update the hash map
for (User user : users) {
synchronized (mUsers) {
User currentUser = mUsers.get(user.user_id);
if ((null == currentUser) || // not defined
currentUser.isRetrievedFromRoomMember() || // tmp user until retrieved it
(currentUser.getLatestPresenceTs() < user.getLatestPresenceTs())) // newer presence
{
mUsers.put(user.user_id, user);
}
}
}
long delta = (System.currentTimeMillis() - start);
Log.e(LOG_TAG, "loadUsers (" + filenames.size() + " files) : retrieve " + mUsers.size() + " users in " + delta + "ms");
mStoreStats.put("loadUsers", delta);
mAreUsersLoaded = true;
// save any pending save
saveUsers();
}
//================================================================================
// Room messages management
//================================================================================
/**
* Computes the saved events map to reduce storage footprint.
*
* @param roomId the room id
* @return the saved eventMap
*/
private LinkedHashMap<String, Event> getSavedEventsMap(String roomId) {
LinkedHashMap<String, Event> eventsMap;
synchronized (mRoomEventsLock) {
eventsMap = mRoomEvents.get(roomId);
}
List<Event> eventsList;
synchronized (mRoomEventsLock) {
eventsList = new ArrayList<>(eventsMap.values());
}
int startIndex = 0;
// try to reduce the number of stored messages
// it does not make sense to keep the full history.
// the method consists in saving messages until finding the oldest known token.
// At initial sync, it is not saved so keep the whole history.
// if the user back paginates, the token is stored in the event.
// if some messages are received, the token is stored in the event.
if (eventsList.size() > MAX_STORED_MESSAGES_COUNT) {
// search backward the first known token
for (startIndex = eventsList.size() - MAX_STORED_MESSAGES_COUNT; !eventsList.get(startIndex).hasToken() && (startIndex > 0); startIndex--)
;
if (startIndex > 0) {
Log.d(LOG_TAG, "## getSavedEveventsMap() : " + roomId + " reduce the number of messages " + eventsList.size()
+ " -> " + (eventsList.size() - startIndex));
}
}
LinkedHashMap<String, Event> savedEvents = new LinkedHashMap<>();
for (int index = startIndex; index < eventsList.size(); index++) {
Event event = eventsList.get(index);
savedEvents.put(event.eventId, event);
}
return savedEvents;
}
private void saveRoomMessages(String roomId) {
LinkedHashMap<String, Event> eventsHash;
synchronized (mRoomEventsLock) {
eventsHash = mRoomEvents.get(roomId);
}
String token = mRoomTokens.get(roomId);
// the list exists ?
if ((null != eventsHash) && (null != token)) {
long t0 = System.currentTimeMillis();
LinkedHashMap<String, Event> savedEventsMap = getSavedEventsMap(roomId);
if (!writeObject("saveRoomsMessage " + roomId, new File(mGzStoreRoomsMessagesFolderFile, roomId), savedEventsMap)) {
return;
}
if (!writeObject("saveRoomsMessage " + roomId, new File(mStoreRoomsTokensFolderFile, roomId), token)) {
return;
}
Log.d(LOG_TAG, "saveRoomsMessage (" + roomId + ") : " + savedEventsMap.size() + " messages saved in " + (System.currentTimeMillis() - t0) + " ms");
} else {
deleteRoomMessagesFiles(roomId);
}
}
/**
* Flush updates rooms messages list files.
*/
private void saveRoomsMessages() {
// some updated rooms ?
if ((mRoomsToCommitForMessages.size() > 0) && (null != mFileStoreHandler)) {
// get the list
final Set<String> fRoomsToCommitForMessages = mRoomsToCommitForMessages;
mRoomsToCommitForMessages = new HashSet<>();
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!isKilled()) {
long start = System.currentTimeMillis();
for (String roomId : fRoomsToCommitForMessages) {
saveRoomMessages(roomId);
}
Log.d(LOG_TAG, "saveRoomsMessages : " + fRoomsToCommitForMessages.size() + " rooms in "
+ (System.currentTimeMillis() - start) + " ms");
}
}
});
}
};
Thread t = new Thread(r);
t.start();
}
}
/**
* Load room messages from the filesystem.
*
* @param roomId the room id.
* @return true if succeed.
*/
private boolean loadRoomMessages(final String roomId) {
boolean succeeded = true;
boolean shouldSave = false;
LinkedHashMap<String, Event> events = null;
File messagesListFile = new File(mGzStoreRoomsMessagesFolderFile, roomId);
if (messagesListFile.exists()) {
Object eventsAsVoid = readObject("events " + roomId, messagesListFile);
if (null != eventsAsVoid) {
try {
events = (LinkedHashMap<String, Event>) eventsAsVoid;
} catch (Exception e) {
Log.e(LOG_TAG, "loadRoomMessages " + roomId + "failed : " + e.getMessage(), e);
return false;
}
if (events.size() > (2 * MAX_STORED_MESSAGES_COUNT)) {
Log.d(LOG_TAG, "## loadRoomMessages() : the room " + roomId + " has " + events.size()
+ " stored events : we need to find a way to reduce it.");
}
// finalizes the deserialization
for (Event event : events.values()) {
// if a message was not sent, mark it as UNDELIVERED
if ((event.mSentState == Event.SentState.UNSENT)
|| (event.mSentState == Event.SentState.SENDING)
|| (event.mSentState == Event.SentState.WAITING_RETRY)
|| (event.mSentState == Event.SentState.ENCRYPTING)) {
event.mSentState = Event.SentState.UNDELIVERED;
shouldSave = true;
}
}
} else {
return false;
}
}
// succeeds to extract the message list
if (null != events) {
// create the room object
final Room room = new Room(getDataHandler(), this, roomId);
// do not wait that the live state update
room.setReadyState(true);
storeRoom(room);
mRoomEvents.put(roomId, events);
}
if (shouldSave) {
saveRoomMessages(roomId);
}
return succeeded;
}
/**
* Load the room token from the file system.
*
* @param roomId the room id.
* @return true if it succeeds.
*/
private boolean loadRoomToken(final String roomId) {
boolean succeed = true;
Room room = getRoom(roomId);
// should always be true
if (null != room) {
String token = null;
try {
File messagesListFile = new File(mStoreRoomsTokensFolderFile, roomId);
Object tokenAsVoid = readObject("loadRoomToken " + roomId, messagesListFile);
if (null == tokenAsVoid) {
succeed = false;
} else {
token = (String) tokenAsVoid;
// check if the oldest event has a token.
LinkedHashMap<String, Event> eventsHash = mRoomEvents.get(roomId);
if ((null != eventsHash) && (eventsHash.size() > 0)) {
Event event = eventsHash.values().iterator().next();
// the room history could have been reduced to save memory
// so, if the oldest messages has a token, use it instead of the stored token.
if (null != event.mToken) {
token = event.mToken;
}
}
}
} catch (Exception e) {
succeed = false;
Log.e(LOG_TAG, "loadRoomToken failed : " + e.toString(), e);
}
if (null != token) {
mRoomTokens.put(roomId, token);
} else {
deleteRoom(roomId);
}
} else {
try {
File messagesListFile = new File(mStoreRoomsTokensFolderFile, roomId);
messagesListFile.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "loadRoomToken failed with error " + e.getMessage(), e);
}
}
return succeed;
}
/**
* Load room messages from the filesystem.
*
* @return true if the operation succeeds.
*/
private boolean loadRoomsMessages() {
boolean succeed = true;
try {
// extract the messages list
List<String> filenames = listFiles(mGzStoreRoomsMessagesFolderFile.list());
long start = System.currentTimeMillis();
for (String filename : filenames) {
if (succeed) {
succeed &= loadRoomMessages(filename);
}
}
if (succeed) {
long delta = (System.currentTimeMillis() - start);
Log.d(LOG_TAG, "loadRoomMessages : " + filenames.size() + " rooms in " + delta + " ms");
mStoreStats.put("loadRoomMessages", delta);
}
// extract the tokens list
filenames = listFiles(mStoreRoomsTokensFolderFile.list());
start = System.currentTimeMillis();
for (String filename : filenames) {
if (succeed) {
succeed &= loadRoomToken(filename);
}
}
if (succeed) {
Log.d(LOG_TAG, "loadRoomToken : " + filenames.size() + " rooms in " + (System.currentTimeMillis() - start) + " ms");
}
} catch (Exception e) {
succeed = false;
Log.e(LOG_TAG, "loadRoomToken failed : " + e.getMessage(), e);
}
return succeed;
}
//================================================================================
// Room states management
//================================================================================
// waiting that the rooms state events are loaded
private Map<String, List<Event>> mPendingRoomStateEvents = new HashMap<>();
@Override
public void storeRoomStateEvent(final String roomId, final im.vector.matrix.android.api.session.events.model.Event event) {
/*boolean isAlreadyLoaded = true;
synchronized (mRoomStateEventsByRoomId) {
isAlreadyLoaded = mRoomStateEventsByRoomId.containsKey(roomId);
}
if (isAlreadyLoaded) {
super.storeRoomStateEvent(roomId, event);
mRoomsToCommitForStatesEvents.add(roomId);
return;
}
boolean isRequestPending = false;
synchronized (mPendingRoomStateEvents) {
// a loading is already in progress
if (mPendingRoomStateEvents.containsKey(roomId)) {
mPendingRoomStateEvents.get(roomId).add(event);
isRequestPending = true;
}
}
if (isRequestPending) {
return;
}
synchronized (mPendingRoomStateEvents) {
List<Event> events = new ArrayList<Event>();
events.add(event);
mPendingRoomStateEvents.put(roomId, events);
}
getRoomStateEvents(roomId, new SimpleApiCallback<List<Event>>() {
@Override
public void onSuccess(List<Event> events) {
List<Event> pendingEvents;
synchronized (mPendingRoomStateEvents) {
pendingEvents = mPendingRoomStateEvents.get(roomId);
mPendingRoomStateEvents.remove(roomId);
}
// add them by now
for (Event event : pendingEvents) {
storeRoomStateEvent(roomId, event);
}
}
});*/
}
/**
* Save the room state.
*
* @param roomId the room id.
*/
private void saveRoomStateEvents(final String roomId) {
/*Log.d(LOG_TAG, "++ saveRoomStateEvents " + roomId);
File roomStateFile = new File(mGzStoreRoomsStateEventsFolderFile, roomId);
Map<String, Event> eventsMap = mRoomStateEventsByRoomId.get(roomId);
if (null != eventsMap) {
List<Event> events = new ArrayList<>(eventsMap.values());
long start1 = System.currentTimeMillis();
writeObject("saveRoomStateEvents " + roomId, roomStateFile, events);
Log.d(LOG_TAG, "saveRoomStateEvents " + roomId + " :" + events.size() + " events : " + (System.currentTimeMillis() - start1) + " ms");
} else {
Log.d(LOG_TAG, "-- saveRoomStateEvents " + roomId + " : empty list");
}*/
}
/**
* Flush the room state events files.
*/
private void saveRoomStatesEvents() {
/*if ((mRoomsToCommitForStatesEvents.size() > 0) && (null != mFileStoreHandler)) {
// get the list
final Set<String> fRoomsToCommitForStatesEvents = new HashSet<>(mRoomsToCommitForStatesEvents);
mRoomsToCommitForStatesEvents = new HashSet<>();
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!isKilled()) {
long start = System.currentTimeMillis();
for (String roomId : fRoomsToCommitForStatesEvents) {
saveRoomStateEvents(roomId);
}
Log.d(LOG_TAG, "saveRoomStatesEvents : " + fRoomsToCommitForStatesEvents.size() + " rooms in "
+ (System.currentTimeMillis() - start) + " ms");
}
}
});
}
};
Thread t = new Thread(r);
t.start();
}*/
}
@Override
public void getRoomStateEvents(final String roomId, final ApiCallback<List<im.vector.matrix.android.api.session.events.model.Event>> callback) {
boolean isAlreadyLoaded = true;
/*synchronized (mRoomStateEventsByRoomId) {
isAlreadyLoaded = mRoomStateEventsByRoomId.containsKey(roomId);
}*/
if (isAlreadyLoaded) {
super.getRoomStateEvents(roomId, callback);
return;
}
/*Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!isKilled()) {
File statesEventsFile = new File(mGzStoreRoomsStateEventsFolderFile, roomId);
Map<String, Event> eventsMap = new HashMap<>();
List<Event> eventsList = new ArrayList<>();
long start = System.currentTimeMillis();
if ((null != statesEventsFile) && statesEventsFile.exists()) {
try {
Object eventsListAsVoid = readObject("getRoomStateEvents", statesEventsFile);
if (null != eventsListAsVoid) {
List<Event> events = (List<Event>) eventsListAsVoid;
for (Event event : events) {
eventsMap.put(event.stateKey, event);
}
}
} catch (Exception e) {
Log.e(LOG_TAG, "getRoomStateEvents failed : " + e.getMessage(), e);
}
}
synchronized (mRoomStateEventsByRoomId) {
mRoomStateEventsByRoomId.put(roomId, eventsMap);
}
Log.d(LOG_TAG, "getRoomStateEvents : retrieve " + eventsList.size() + " events in " + (System.currentTimeMillis() - start) + " ms");
callback.onSuccess(eventsList);
}
}
});
}
};
Thread t = new Thread(r);
t.start();*/
}
/**
* Delete the room state file.
*
* @param roomId the room id.
*/
private void deleteRoomStateFile(String roomId) {
// states list
File statesFile = new File(mGzStoreRoomsStateFolderFile, roomId);
if (statesFile.exists()) {
try {
statesFile.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "deleteRoomStateFile failed with error " + e.getMessage(), e);
}
}
File statesEventsFile = new File(mGzStoreRoomsStateEventsFolderFile, roomId);
if (statesEventsFile.exists()) {
try {
statesEventsFile.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "deleteRoomStateFile failed with error " + e.getMessage(), e);
}
}
}
/**
* Save the room state.
*
* @param roomId the room id.
*/
private void saveRoomState(final String roomId) {
Log.d(LOG_TAG, "++ saveRoomsState " + roomId);
File roomStateFile = new File(mGzStoreRoomsStateFolderFile, roomId);
Room room = mRooms.get(roomId);
if (null != room) {
long start1 = System.currentTimeMillis();
writeObject("saveRoomsState " + roomId, roomStateFile, room.getState());
Log.d(LOG_TAG, "saveRoomsState " + room.getNumberOfMembers() + " members : " + (System.currentTimeMillis() - start1) + " ms");
} else {
Log.d(LOG_TAG, "saveRoomsState : delete the room state");
deleteRoomStateFile(roomId);
}
Log.d(LOG_TAG, "-- saveRoomsState " + roomId);
}
/**
* Flush the room state files.
*/
private void saveRoomStates() {
if ((mRoomsToCommitForStates.size() > 0) && (null != mFileStoreHandler)) {
// get the list
final Set<String> fRoomsToCommitForStates = mRoomsToCommitForStates;
mRoomsToCommitForStates = new HashSet<>();
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!isKilled()) {
long start = System.currentTimeMillis();
for (String roomId : fRoomsToCommitForStates) {
saveRoomState(roomId);
}
Log.d(LOG_TAG, "saveRoomsState : " + fRoomsToCommitForStates.size() + " rooms in "
+ (System.currentTimeMillis() - start) + " ms");
}
}
});
}
};
Thread t = new Thread(r);
t.start();
}
}
/**
* Load a room state from the file system.
*
* @param roomId the room id.
* @return true if the operation succeeds.
*/
private boolean loadRoomState(final String roomId) {
boolean succeed = true;
Room room = getRoom(roomId);
// should always be true
if (null != room) {
RoomState liveState = null;
try {
// the room state is not zipped
File roomStateFile = new File(mGzStoreRoomsStateFolderFile, roomId);
// new format
if (roomStateFile.exists()) {
Object roomStateAsObject = readObject("loadRoomState " + roomId, roomStateFile);
if (null == roomStateAsObject) {
succeed = false;
} else {
liveState = (RoomState) roomStateAsObject;
}
}
} catch (Exception e) {
succeed = false;
Log.e(LOG_TAG, "loadRoomState failed : " + e.getMessage(), e);
}
if (null != liveState) {
room.getTimeline().setState(liveState);
} else {
deleteRoom(roomId);
}
} else {
try {
File messagesListFile = new File(mGzStoreRoomsStateFolderFile, roomId);
messagesListFile.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "loadRoomState failed to delete a file : " + e.getMessage(), e);
}
}
return succeed;
}
/**
* Load room state from the file system.
*
* @return true if the operation succeeds.
*/
private boolean loadRoomsState() {
boolean succeed = true;
try {
long start = System.currentTimeMillis();
List<String> filenames = listFiles(mGzStoreRoomsStateFolderFile.list());
for (String filename : filenames) {
if (succeed) {
succeed &= loadRoomState(filename);
}
}
long delta = (System.currentTimeMillis() - start);
Log.d(LOG_TAG, "loadRoomsState " + filenames.size() + " rooms in " + delta + " ms");
mStoreStats.put("loadRoomsState", delta);
} catch (Exception e) {
succeed = false;
Log.e(LOG_TAG, "loadRoomsState failed : " + e.getMessage(), e);
}
return succeed;
}
//================================================================================
// AccountData management
//================================================================================
/**
* Delete the room account data file.
*
* @param roomId the room id.
*/
private void deleteRoomAccountDataFile(String roomId) {
File file = new File(mStoreRoomsAccountDataFolderFile, roomId);
// remove the files
if (file.exists()) {
try {
file.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "deleteRoomAccountDataFile failed : " + e.getMessage(), e);
}
}
}
/**
* Flush the pending account data.
*/
private void saveRoomsAccountData() {
if ((mRoomsToCommitForAccountData.size() > 0) && (null != mFileStoreHandler)) {
// get the list
final Set<String> fRoomsToCommitForAccountData = mRoomsToCommitForAccountData;
mRoomsToCommitForAccountData = new HashSet<>();
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!isKilled()) {
long start = System.currentTimeMillis();
for (String roomId : fRoomsToCommitForAccountData) {
RoomAccountData accountData = mRoomAccountData.get(roomId);
if (null != accountData) {
writeObject("saveRoomsAccountData " + roomId, new File(mStoreRoomsAccountDataFolderFile, roomId), accountData);
} else {
deleteRoomAccountDataFile(roomId);
}
}
Log.d(LOG_TAG, "saveSummaries : " + fRoomsToCommitForAccountData.size() + " account data in "
+ (System.currentTimeMillis() - start) + " ms");
}
}
});
}
};
Thread t = new Thread(r);
t.start();
}
}
/***
* Load the account Data of a dedicated room.
* @param roomId the room Id
* @return true if the operation succeeds.
*/
private boolean loadRoomAccountData(final String roomId) {
boolean succeeded = true;
RoomAccountData roomAccountData = null;
try {
File accountDataFile = new File(mStoreRoomsAccountDataFolderFile, roomId);
if (accountDataFile.exists()) {
Object accountAsVoid = readObject("loadRoomAccountData " + roomId, accountDataFile);
if (null == accountAsVoid) {
Log.e(LOG_TAG, "loadRoomAccountData failed");
return false;
}
roomAccountData = (RoomAccountData) accountAsVoid;
}
} catch (Exception e) {
succeeded = false;
Log.e(LOG_TAG, "loadRoomAccountData failed : " + e.toString(), e);
}
// succeeds to extract the message list
if (null != roomAccountData) {
Room room = getRoom(roomId);
if (null != room) {
room.setAccountData(roomAccountData);
}
}
return succeeded;
}
/**
* Load room accountData from the filesystem.
*
* @return true if the operation succeeds.
*/
private boolean loadRoomsAccountData() {
boolean succeed = true;
try {
// extract the messages list
List<String> filenames = listFiles(mStoreRoomsAccountDataFolderFile.list());
long start = System.currentTimeMillis();
for (String filename : filenames) {
succeed &= loadRoomAccountData(filename);
}
if (succeed) {
Log.d(LOG_TAG, "loadRoomsAccountData : " + filenames.size() + " rooms in " + (System.currentTimeMillis() - start) + " ms");
}
} catch (Exception e) {
succeed = false;
Log.e(LOG_TAG, "loadRoomsAccountData failed : " + e.getMessage(), e);
}
return succeed;
}
@Override
public void storeAccountData(String roomId, RoomAccountData accountData) {
super.storeAccountData(roomId, accountData);
if (null != roomId) {
Room room = mRooms.get(roomId);
// sanity checks
if ((room != null) && (null != accountData)) {
mRoomsToCommitForAccountData.add(roomId);
}
}
}
//================================================================================
// Summary management
//================================================================================
/**
* Delete the room summary file.
*
* @param roomId the room id.
*/
private void deleteRoomSummaryFile(String roomId) {
// states list
File statesFile = new File(mStoreRoomsSummaryFolderFile, roomId);
// remove the files
if (statesFile.exists()) {
try {
statesFile.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "deleteRoomSummaryFile failed : " + e.getMessage(), e);
}
}
}
/**
* Flush the pending summaries.
*/
private void saveSummaries() {
if ((mRoomsToCommitForSummaries.size() > 0) && (null != mFileStoreHandler)) {
// get the list
final Set<String> fRoomsToCommitForSummaries = mRoomsToCommitForSummaries;
mRoomsToCommitForSummaries = new HashSet<>();
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!isKilled()) {
long start = System.currentTimeMillis();
for (String roomId : fRoomsToCommitForSummaries) {
try {
File roomSummaryFile = new File(mStoreRoomsSummaryFolderFile, roomId);
RoomSummary roomSummary = mRoomSummaries.get(roomId);
if (null != roomSummary) {
writeObject("saveSummaries " + roomId, roomSummaryFile, roomSummary);
} else {
deleteRoomSummaryFile(roomId);
}
} catch (OutOfMemoryError oom) {
dispatchOOM(oom);
} catch (Exception e) {
Log.e(LOG_TAG, "saveSummaries failed : " + e.getMessage(), e);
// Toast.makeText(mContext, "saveSummaries failed " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
}
Log.d(LOG_TAG, "saveSummaries : " + fRoomsToCommitForSummaries.size() + " summaries in "
+ (System.currentTimeMillis() - start) + " ms");
}
}
});
}
};
Thread t = new Thread(r);
t.start();
}
}
/**
* Load the room summary from the files system.
*
* @param roomId the room id.
* @return true if the operation succeeds;
*/
private boolean loadSummary(final String roomId) {
boolean succeed = true;
// do not check if the room exists here.
// if the user is invited to a room, the room object is not created until it is joined.
RoomSummary summary = null;
try {
File messagesListFile = new File(mStoreRoomsSummaryFolderFile, roomId);
Object summaryAsVoid = readObject("loadSummary " + roomId, messagesListFile);
if (null == summaryAsVoid) {
Log.e(LOG_TAG, "loadSummary failed");
return false;
}
summary = (RoomSummary) summaryAsVoid;
} catch (Exception e) {
succeed = false;
Log.e(LOG_TAG, "loadSummary failed : " + e.getMessage(), e);
}
if (null != summary) {
//summary.getLatestReceivedEvent().finalizeDeserialization();
Room room = getRoom(summary.getRoomId());
// the room state is not saved in the summary.
// it is restored from the room
if (null != room) {
summary.setLatestRoomState(room.getState());
}
mRoomSummaries.put(roomId, summary);
}
return succeed;
}
/**
* Load room summaries from the file system.
*
* @return true if the operation succeeds.
*/
private boolean loadSummaries() {
boolean succeed = true;
try {
// extract the room states
List<String> filenames = listFiles(mStoreRoomsSummaryFolderFile.list());
long start = System.currentTimeMillis();
for (String filename : filenames) {
succeed &= loadSummary(filename);
}
long delta = (System.currentTimeMillis() - start);
Log.d(LOG_TAG, "loadSummaries " + filenames.size() + " rooms in " + delta + " ms");
mStoreStats.put("loadSummaries", delta);
} catch (Exception e) {
succeed = false;
Log.e(LOG_TAG, "loadSummaries failed : " + e.getMessage(), e);
}
return succeed;
}
//================================================================================
// Metadata management
//================================================================================
/**
* Load the metadata info from the file system.
*/
private void loadMetaData() {
long start = System.currentTimeMillis();
// init members
mEventStreamToken = null;
mMetadata = null;
File metaDataFile = new File(mStoreFolderFile, MXFILE_STORE_METADATA_FILE_NAME);
if (metaDataFile.exists()) {
Object metadataAsVoid = readObject("loadMetaData", metaDataFile);
if (null != metadataAsVoid) {
try {
mMetadata = (MXFileStoreMetaData) metadataAsVoid;
// remove pending \n
if (null != mMetadata.mUserDisplayName) {
mMetadata.mUserDisplayName.trim();
}
// extract the latest event stream token
mEventStreamToken = mMetadata.mEventStreamToken;
} catch (Exception e) {
Log.e(LOG_TAG, "## loadMetaData() : is corrupted", e);
return;
}
}
}
Log.d(LOG_TAG, "loadMetaData : " + (System.currentTimeMillis() - start) + " ms");
}
/**
* flush the metadata info from the file system.
*/
private void saveMetaData() {
if ((mMetaDataHasChanged) && (null != mFileStoreHandler) && (null != mMetadata)) {
mMetaDataHasChanged = false;
final MXFileStoreMetaData fMetadata = mMetadata.deepCopy();
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!mIsKilled) {
// save the metadata only when there is a current valid stream token
// avoid saving the metadata if the store has been cleared
if (null != mMetadata.mEventStreamToken) {
long start = System.currentTimeMillis();
writeObject("saveMetaData", new File(mStoreFolderFile, MXFILE_STORE_METADATA_FILE_NAME), fMetadata);
Log.d(LOG_TAG, "saveMetaData : " + (System.currentTimeMillis() - start) + " ms");
} else {
Log.e(LOG_TAG, "## saveMetaData() : cancelled because mEventStreamToken is null");
}
}
}
});
}
};
Thread t = new Thread(r);
t.start();
}
}
//================================================================================
// Event receipts management
//================================================================================
@Override
public List<ReceiptData> getEventReceipts(String roomId, String eventId, boolean excludeSelf, boolean sort) {
synchronized (mRoomReceiptsToLoad) {
int pos = mRoomReceiptsToLoad.indexOf(roomId);
// the user requires the receipts asap
if (pos >= 2) {
mRoomReceiptsToLoad.remove(roomId);
// index 0 is the current managed one
mRoomReceiptsToLoad.add(1, roomId);
}
}
return super.getEventReceipts(roomId, eventId, excludeSelf, sort);
}
/**
* Store the receipt for an user in a room
*
* @param receipt The event
* @param roomId The roomId
* @return true if the receipt has been stored
*/
@Override
public boolean storeReceipt(ReceiptData receipt, String roomId) {
boolean res = super.storeReceipt(receipt, roomId);
if (res) {
synchronized (this) {
mRoomsToCommitForReceipts.add(roomId);
}
}
return res;
}
/***
* Load the events receipts.
* @param roomId the room Id
* @return true if the operation succeeds.
*/
private boolean loadReceipts(String roomId) {
Map<String, ReceiptData> receiptsMap = null;
File file = new File(mStoreRoomsMessagesReceiptsFolderFile, roomId);
if (file.exists()) {
Object receiptsAsVoid = readObject("loadReceipts " + roomId, file);
if (null != receiptsAsVoid) {
try {
List<ReceiptData> receipts = (List<ReceiptData>) receiptsAsVoid;
receiptsMap = new HashMap<>();
for (ReceiptData r : receipts) {
receiptsMap.put(r.userId, r);
}
} catch (Exception e) {
Log.e(LOG_TAG, "loadReceipts failed : " + e.getMessage(), e);
return false;
}
} else {
return false;
}
}
if (null != receiptsMap) {
Map<String, ReceiptData> currentReceiptMap;
synchronized (mReceiptsByRoomIdLock) {
currentReceiptMap = mReceiptsByRoomId.get(roomId);
mReceiptsByRoomId.put(roomId, receiptsMap);
}
// merge the current read receipts
if (null != currentReceiptMap) {
Collection<ReceiptData> receipts = currentReceiptMap.values();
for (ReceiptData receipt : receipts) {
storeReceipt(receipt, roomId);
}
}
dispatchOnReadReceiptsLoaded(roomId);
}
return true;
}
/**
* Load event receipts from the file system.
*
* @return true if the operation succeeds.
*/
private boolean loadReceipts() {
boolean succeed = true;
try {
int count = mRoomReceiptsToLoad.size();
long start = System.currentTimeMillis();
while (mRoomReceiptsToLoad.size() > 0) {
String roomId;
synchronized (mRoomReceiptsToLoad) {
roomId = mRoomReceiptsToLoad.get(0);
}
loadReceipts(roomId);
synchronized (mRoomReceiptsToLoad) {
mRoomReceiptsToLoad.remove(0);
}
}
saveReceipts();
long delta = (System.currentTimeMillis() - start);
Log.d(LOG_TAG, "loadReceipts " + count + " rooms in " + delta + " ms");
mStoreStats.put("loadReceipts", delta);
} catch (Exception e) {
succeed = false;
//Toast.makeText(mContext, "loadReceipts failed" + e, Toast.LENGTH_LONG).show();
Log.e(LOG_TAG, "loadReceipts failed : " + e.getMessage(), e);
}
synchronized (this) {
mAreReceiptsReady = true;
}
return succeed;
}
/**
* Flush the events receipts
*
* @param roomId the roomId.
*/
private void saveReceipts(final String roomId) {
synchronized (mRoomReceiptsToLoad) {
// please wait
if (mRoomReceiptsToLoad.contains(roomId)) {
return;
}
}
final List<ReceiptData> receipts;
synchronized (mReceiptsByRoomIdLock) {
if (mReceiptsByRoomId.containsKey(roomId)) {
receipts = new ArrayList<>(mReceiptsByRoomId.get(roomId).values());
} else {
receipts = null;
}
}
// sanity check
if (null == receipts) {
return;
}
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!mIsKilled) {
long start = System.currentTimeMillis();
writeObject("saveReceipts " + roomId, new File(mStoreRoomsMessagesReceiptsFolderFile, roomId), receipts);
Log.d(LOG_TAG, "saveReceipts : roomId " + roomId + " eventId : " + (System.currentTimeMillis() - start) + " ms");
}
}
});
}
};
Thread t = new Thread(r);
t.start();
}
/**
* Save the events receipts.
*/
private void saveReceipts() {
synchronized (this) {
Set<String> roomsToCommit = mRoomsToCommitForReceipts;
for (String roomId : roomsToCommit) {
saveReceipts(roomId);
}
mRoomsToCommitForReceipts.clear();
}
}
/**
* Delete the room receipts
*
* @param roomId the room id.
*/
private void deleteRoomReceiptsFile(String roomId) {
File receiptsFile = new File(mStoreRoomsMessagesReceiptsFolderFile, roomId);
// remove the files
if (receiptsFile.exists()) {
try {
receiptsFile.delete();
} catch (Exception e) {
Log.e(LOG_TAG, "deleteReceiptsFile - failed " + e.getMessage(), e);
}
}
}
//================================================================================
// read/write methods
//================================================================================
/**
* Write an object in a dedicated file.
*
* @param description the operation description
* @param file the file
* @param object the object to save
* @return true if the operation succeeds
*/
private boolean writeObject(String description, File file, Object object) {
String parent = file.getParent();
String name = file.getName();
File tmpFile = new File(parent, name + ".tmp");
if (tmpFile.exists()) {
tmpFile.delete();
}
if (file.exists()) {
file.renameTo(tmpFile);
}
boolean succeed = false;
try {
FileOutputStream fos = new FileOutputStream(file);
OutputStream cos;
if (mEnableFileEncryption) {
cos = CompatUtil.createCipherOutputStream(fos, mContext);
} else {
cos = fos;
}
GZIPOutputStream gz = CompatUtil.createGzipOutputStream(cos);
ObjectOutputStream out = new ObjectOutputStream(gz);
out.writeObject(object);
out.flush();
out.close();
succeed = true;
} catch (OutOfMemoryError oom) {
dispatchOOM(oom);
} catch (Exception e) {
Log.e(LOG_TAG, "## writeObject() " + description + " : failed " + e.getMessage(), e);
}
if (succeed) {
tmpFile.delete();
} else {
tmpFile.renameTo(file);
}
return succeed;
}
/**
* Read an object from a dedicated file
*
* @param description the operation description
* @param file the file
* @return the read object if it can be retrieved
*/
private Object readObject(String description, File file) {
String parent = file.getParent();
String name = file.getName();
File tmpFile = new File(parent, name + ".tmp");
if (tmpFile.exists()) {
Log.e(LOG_TAG, "## readObject : rescue from a tmp file " + tmpFile.getName());
file = tmpFile;
}
Object object = null;
try {
FileInputStream fis = new FileInputStream(file);
InputStream cis;
if (mEnableFileEncryption) {
cis = CompatUtil.createCipherInputStream(fis, mContext);
if (cis == null) {
// fallback to unencrypted stream for backward compatibility
Log.i(LOG_TAG, "## readObject() : failed to read encrypted, fallback to unencrypted read");
fis.close();
cis = new FileInputStream(file);
}
} else {
cis = fis;
}
GZIPInputStream gz = new GZIPInputStream(cis);
ObjectInputStream ois = new ObjectInputStream(gz);
object = ois.readObject();
ois.close();
} catch (OutOfMemoryError oom) {
dispatchOOM(oom);
} catch (Exception e) {
Log.e(LOG_TAG, "## readObject() " + description + " : failed " + e.getMessage(), e);
}
return object;
}
/**
* Remove the tmp files from a filename list
*
* @param names the names list
* @return the filtered list
*/
private static List<String> listFiles(String[] names) {
List<String> filteredFilenames = new ArrayList<>();
List<String> tmpFilenames = new ArrayList<>();
// sanity checks
// it has been reported by GA
if (null != names) {
for (int i = 0; i < names.length; i++) {
String name = names[i];
if (!name.endsWith(".tmp")) {
filteredFilenames.add(name);
} else {
tmpFilenames.add(name.substring(0, name.length() - ".tmp".length()));
}
}
// check if the tmp file is not alone i.e the matched file was not saved (app crash...)
for (String tmpFileName : tmpFilenames) {
if (!filteredFilenames.contains(tmpFileName)) {
Log.e(LOG_TAG, "## listFiles() : " + tmpFileName + " does not exist but a tmp file has been retrieved");
filteredFilenames.add(tmpFileName);
}
}
}
return filteredFilenames;
}
/**
* Start a runnable from the store thread
*
* @param runnable the runnable to call
*/
public void post(Runnable runnable) {
if (null != mFileStoreHandler) {
mFileStoreHandler.post(runnable);
} else {
super.post(runnable);
}
}
//================================================================================
// groups management
//================================================================================
/**
* Store a group
*
* @param group the group to store
*/
@Override
public void storeGroup(Group group) {
super.storeGroup(group);
if ((null != group) && !TextUtils.isEmpty(group.getGroupId())) {
mGroupsToCommit.add(group.getGroupId());
}
}
/**
* Flush a group
*
* @param group the group to store
*/
@Override
public void flushGroup(Group group) {
super.flushGroup(group);
if ((null != group) && !TextUtils.isEmpty(group.getGroupId())) {
mGroupsToCommit.add(group.getGroupId());
saveGroups();
}
}
/**
* Delete a group
*
* @param groupId the groupId to delete
*/
@Override
public void deleteGroup(String groupId) {
super.deleteGroup(groupId);
if (!TextUtils.isEmpty(groupId)) {
mGroupsToCommit.add(groupId);
}
}
/**
* Flush groups list
*/
private void saveGroups() {
// some updated rooms ?
if ((mGroupsToCommit.size() > 0) && (null != mFileStoreHandler)) {
// get the list
final Set<String> fGroupIds = mGroupsToCommit;
mGroupsToCommit = new HashSet<>();
try {
Runnable r = new Runnable() {
@Override
public void run() {
mFileStoreHandler.post(new Runnable() {
public void run() {
if (!isKilled()) {
Log.d(LOG_TAG, "saveGroups " + fGroupIds.size() + " groups");
long start = System.currentTimeMillis();
for (String groupId : fGroupIds) {
Group group;
synchronized (mGroups) {
group = mGroups.get(groupId);
}
if (null != group) {
writeObject("saveGroup " + groupId, new File(mStoreGroupsFolderFile, groupId), group);
} else {
File tokenFile = new File(mStoreGroupsFolderFile, groupId);
if (tokenFile.exists()) {
tokenFile.delete();
}
}
}
Log.d(LOG_TAG, "saveGroups done in " + (System.currentTimeMillis() - start) + " ms");
}
}
});
}
};
Thread t = new Thread(r);
t.start();
} catch (OutOfMemoryError oom) {
Log.e(LOG_TAG, "saveGroups : failed" + oom.getMessage(), oom);
}
}
}
/**
* Load groups from the filesystem.
*
* @return true if the operation succeeds.
*/
private boolean loadGroups() {
boolean succeed = true;
try {
// extract the messages list
List<String> filenames = listFiles(mStoreGroupsFolderFile.list());
long start = System.currentTimeMillis();
for (String filename : filenames) {
File groupFile = new File(mStoreGroupsFolderFile, filename);
if (groupFile.exists()) {
Object groupAsVoid = readObject("loadGroups " + filename, groupFile);
if ((null != groupAsVoid) && (groupAsVoid instanceof Group)) {
Group group = (Group) groupAsVoid;
mGroups.put(group.getGroupId(), group);
} else {
succeed = false;
break;
}
}
}
if (succeed) {
long delta = (System.currentTimeMillis() - start);
Log.d(LOG_TAG, "loadGroups : " + filenames.size() + " groups in " + delta + " ms");
mStoreStats.put("loadGroups", delta);
}
} catch (Exception e) {
succeed = false;
Log.e(LOG_TAG, "loadGroups failed : " + e.getMessage(), e);
}
return succeed;
}
@Override
public void setURLPreviewEnabled(boolean value) {
super.setURLPreviewEnabled(value);
mMetaDataHasChanged = true;
}
@Override
public void setRoomsWithoutURLPreview(Set<String> roomIds) {
super.setRoomsWithoutURLPreview(roomIds);
mMetaDataHasChanged = true;
}
@Override
public void setUserWidgets(Map<String, Object> contentDict) {
super.setUserWidgets(contentDict);
mMetaDataHasChanged = true;
}
@Override
public void addFilter(String jsonFilter, String filterId) {
super.addFilter(jsonFilter, filterId);
mMetaDataHasChanged = true;
}
@Override
public void setAntivirusServerPublicKey(@Nullable String key) {
super.setAntivirusServerPublicKey(key);
mMetaDataHasChanged = true;
}
}