/*
* Copyright (c) 2013, Psiphon Inc.
* All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package ca.psiphon.ploggy;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import android.content.Context;
/**
* Data persistence for self, friends, and status.
*
* On disk, data is represented as JSON stored in individual files. In memory, data is represented
* as immutable POJOs which are thread-safe and easily serializable. Self and friend metadata, including
* identity, and recent status data are kept in-memory. Large data such as map tiles will be left on
* disk with perhaps an in-memory cache.
*
* Simple consistency is provided: data changes are first written to a commit file, then the commit
* file replaces the data file. In memory structures are replaced only after the file write succeeds.
*
* If local security is added to the scope of Ploggy, here's where we'd interface with SQLCipher and/or
* KeyChain, etc.
*
* ==== PROTOTYPE NOTE ====
* This module is performant for the prototype only. Missing are:
* - incremental synchronization
* - synchronization based on logical timestamp
* - efficient data storage and viewing
* ========================
*
*/
public class Data {
private static final String LOG_TAG = "Data";
public static class Self {
public final Identity.PublicIdentity mPublicIdentity;
public final Identity.PrivateIdentity mPrivateIdentity;
public final Date mCreatedTimestamp;
public Self(
Identity.PublicIdentity publicIdentity,
Identity.PrivateIdentity privateIdentity,
Date createdTimestamp) {
mPublicIdentity = publicIdentity;
mPrivateIdentity = privateIdentity;
mCreatedTimestamp = createdTimestamp;
}
}
public static class Friend {
public final String mId;
public final Identity.PublicIdentity mPublicIdentity;
public final Date mAddedTimestamp;
public final Date mLastSentStatusTimestamp;
public final Date mLastReceivedStatusTimestamp;
public Friend(
Identity.PublicIdentity publicIdentity,
Date addedTimestamp) throws Utils.ApplicationError {
this(publicIdentity, addedTimestamp, null, null);
}
public Friend(
Identity.PublicIdentity publicIdentity,
Date addedTimestamp,
Date lastSentStatusTimestamp,
Date lastReceivedStatusTimestamp) throws Utils.ApplicationError {
mId = Utils.formatFingerprint(publicIdentity.getFingerprint());
mPublicIdentity = publicIdentity;
mAddedTimestamp = addedTimestamp;
mLastSentStatusTimestamp = lastSentStatusTimestamp;
mLastReceivedStatusTimestamp = lastReceivedStatusTimestamp;
}
}
public class FriendComparator implements Comparator<Friend> {
@Override
public int compare(Friend a, Friend b) {
return a.mPublicIdentity.mNickname.compareToIgnoreCase(b.mPublicIdentity.mNickname);
}
}
public static class Message {
public final Date mTimestamp;
public final String mContent;
public final List<Resource> mAttachments;
public Message(
Date timestamp,
String content,
List<Resource> attachments) {
mTimestamp = timestamp;
mContent = content;
mAttachments = attachments;
}
}
public static class AnnotatedMessage {
public final Identity.PublicIdentity mPublicIdentity;
public final String mFriendId;
public final Data.Message mMessage;
public AnnotatedMessage(
Identity.PublicIdentity publicIdentity,
String friendId,
Data.Message message) {
mPublicIdentity = publicIdentity;
mFriendId = friendId;
mMessage = message;
}
}
public class AnnotatedMessageComparator implements Comparator<AnnotatedMessage> {
@Override
public int compare(AnnotatedMessage a, AnnotatedMessage b) {
// Descending time order
int result = b.mMessage.mTimestamp.compareTo(a.mMessage.mTimestamp);
if (result == 0) {
result = a.mPublicIdentity.mNickname.compareToIgnoreCase(b.mPublicIdentity.mNickname);
}
return result;
}
}
public static class Location {
public final Date mTimestamp;
public final double mLatitude;
public final double mLongitude;
public final int mPrecision;
public final String mStreetAddress;
public Location(
Date timestamp,
double latitude,
double longitude,
int precision,
String streetAddress) {
mTimestamp = timestamp;
mLatitude = latitude;
mLongitude = longitude;
mPrecision = precision;
mStreetAddress = streetAddress;
}
}
public static class Status {
final List<Message> mMessages;
public final Location mLocation;
public Status(
List<Message> messages,
Location location) {
mMessages = messages;
mLocation = location;
}
}
public static class Resource {
public final String mId;
public final String mMimeType;
public final long mSize;
public Resource(
String id,
String mimeType,
long size) {
mId = id;
mMimeType = mimeType;
mSize = size;
}
}
public static class LocalResource {
public enum Type {PICTURE, RAW}
public final Type mType;
public final String mResourceId;
public final String mMimeType;
public final String mFilePath;
public final String mTempFilePath;
public LocalResource(
Type type,
String resourceId,
String mimeType,
String filePath,
String tempFilePath) {
mType = type;
mResourceId = resourceId;
mMimeType = mimeType;
mFilePath = filePath;
mTempFilePath = tempFilePath;
}
}
public static class Download {
public final String mFriendId;
public final String mResourceId;
public final String mMimeType;
public final long mSize;
public enum State {IN_PROGRESS, CANCELLED, COMPLETE}
public final State mState;
public Download(
String friendId,
String resourceId,
String mimeType,
long size,
State state) {
mFriendId = friendId;
mResourceId = resourceId;
mMimeType = mimeType;
mSize = size;
mState = state;
}
}
// TODO: fix -- having these errors as subclasses of Utils.ApplicationError with
// no log can result in silent failures when functions only handle the base class
public static class DataNotFoundError extends Utils.ApplicationError {
private static final long serialVersionUID = -8736069103392081076L;
public DataNotFoundError() {
// No log for this expected condition
super(null, "");
}
}
public static class DataAlreadyExistsError extends Utils.ApplicationError {
private static final long serialVersionUID = 6287628326991088141L;
public DataAlreadyExistsError() {
// No log for this expected condition
super(null, "");
}
}
// ---- Singleton ----
private static Data instance = null;
public static synchronized Data getInstance() {
if(instance == null) {
instance = new Data();
}
return instance;
}
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
// -------------------
// TODO: SQLCipher/IOCipher storage? key/value store?
// TODO: use http://nelenkov.blogspot.ca/2011/11/using-ics-keychain-api.html?
// ...consistency: write file, then update in-memory; 2pc; only for short lists of friends
// ...eventually use file system for map tiles etc.
private static final String DATA_DIRECTORY = "ploggyData";
private static final String SELF_FILENAME = "self.json";
private static final String SELF_STATUS_FILENAME = "selfStatus.json";
private static final String FRIENDS_FILENAME = "friends.json";
private static final String FRIEND_STATUS_FILENAME_FORMAT_STRING = "%s-friendStatus.json";
private static final String LOCAL_RESOURCES_FILENAME = "localResources.json";
private static final String DOWNLOADS_FILENAME = "downloads.json";
private static final String COMMIT_FILENAME_SUFFIX = ".commit";
Self mSelf;
Status mSelfStatus;
Location mPrivateSelfLocation;
List<Friend> mFriends;
HashMap<String, Status> mFriendStatuses;
List<AnnotatedMessage> mNewMessages;
List<AnnotatedMessage> mAllMessages;
List<LocalResource> mLocalResources;
List<Download> mDownloads;
public synchronized void reset() throws Utils.ApplicationError {
// Warning: deletes all files in DATA_DIRECTORY (not recursively)
File directory = Utils.getApplicationContext().getDir(DATA_DIRECTORY, Context.MODE_PRIVATE);
directory.mkdirs();
boolean deleteFailed = false;
for (String child : directory.list()) {
File file = new File(directory, child);
if (file.isFile()) {
if (!file.delete()) {
deleteFailed = true;
// Keep attempting to delete remaining files...
}
}
}
if (deleteFailed) {
throw new Utils.ApplicationError(LOG_TAG, "delete data file failed");
}
}
public synchronized Self getSelf() throws Utils.ApplicationError, DataNotFoundError {
if (mSelf == null) {
mSelf = Json.fromJson(readFile(SELF_FILENAME), Self.class);
}
return mSelf;
}
public synchronized void updateSelf(Self self) throws Utils.ApplicationError {
// When creating a new identity, remove status from previous identity
deleteFile(String.format(SELF_STATUS_FILENAME));
writeFile(SELF_FILENAME, Json.toJson(self));
mSelf = self;
Log.addEntry(LOG_TAG, "updated your identity");
Events.post(new Events.UpdatedSelf());
}
public synchronized Status getSelfStatus() throws Utils.ApplicationError {
if (mSelfStatus == null) {
try {
mSelfStatus = Json.fromJson(readFile(SELF_STATUS_FILENAME), Status.class);
} catch (DataNotFoundError e) {
// If there's no previous status, return a blank one
return new Status(new ArrayList<Message>(), new Location(null, 0, 0, 0, null));
}
}
return mSelfStatus;
}
public synchronized Location getCurrentSelfLocation() throws Utils.ApplicationError {
// If location sharing was off when updateSelfStatusLocation was last called, then
// mPrivateSelfLocation is the more up-to-date than mSelfStatus.
if (mPrivateSelfLocation == null) {
return getSelfStatus().mLocation;
}
return mPrivateSelfLocation;
}
public synchronized void addSelfStatusMessage(Message message, List<LocalResource> attachmentLocalResources) throws Utils.ApplicationError, DataNotFoundError {
// Hack: initMessages before committing new message to avoid duplicate adds in addSelfMessageHelper
initMessages();
initLocalResources();
List<LocalResource> newLocalResources = null;
if (attachmentLocalResources != null) {
newLocalResources = new ArrayList<LocalResource>(mLocalResources);
newLocalResources.addAll(attachmentLocalResources);
}
Status currentStatus = getSelfStatus();
List<Message> messages = new ArrayList<Message>(currentStatus.mMessages);
messages.add(0, message);
while (messages.size() > Protocol.MAX_MESSAGE_COUNT) {
messages.remove(messages.size() - 1);
}
Status newStatus = new Status(messages, currentStatus.mLocation);
if (newLocalResources != null) {
writeFile(LOCAL_RESOURCES_FILENAME, Json.toJson(newLocalResources));
}
writeFile(SELF_STATUS_FILENAME, Json.toJson(newStatus));
if (newLocalResources != null) {
mLocalResources.addAll(attachmentLocalResources);
}
mSelfStatus = newStatus;
Log.addEntry(LOG_TAG, "added your message");
Events.post(new Events.UpdatedSelfStatus());
addSelfMessageHelper(getSelf(), message);
}
public synchronized void updateSelfStatusLocation(Location location, boolean shared) throws Utils.ApplicationError {
if (shared) {
Status currentStatus = getSelfStatus();
Status newStatus = new Status(currentStatus.mMessages, location);
writeFile(SELF_STATUS_FILENAME, Json.toJson(newStatus));
mSelfStatus = newStatus;
mPrivateSelfLocation = location;
} else {
mPrivateSelfLocation = location;
}
Log.addEntry(LOG_TAG, "updated your location");
Events.post(new Events.UpdatedSelfStatus());
}
private void initFriends() throws Utils.ApplicationError {
if (mFriends == null) {
try {
mFriends = new ArrayList<Friend>(Arrays.asList(Json.fromJson(readFile(FRIENDS_FILENAME), Friend[].class)));
} catch (DataNotFoundError e) {
mFriends = new ArrayList<Friend>();
}
}
}
public synchronized List<Friend> getFriends() throws Utils.ApplicationError {
initFriends();
List<Friend> friends = new ArrayList<Friend>(mFriends);
Collections.sort(friends, new FriendComparator());
return friends;
}
public synchronized Friend getFriendById(String id) throws Utils.ApplicationError, DataNotFoundError {
initFriends();
for (Friend friend : mFriends) {
if (friend.mId.equals(id)) {
return friend;
}
}
throw new DataNotFoundError();
}
public synchronized Friend getFriendByNickname(String nickname) throws Utils.ApplicationError, DataNotFoundError {
initFriends();
for (Friend friend : mFriends) {
if (friend.mPublicIdentity.mNickname.equals(nickname)) {
return friend;
}
}
throw new DataNotFoundError();
}
public synchronized Friend getFriendByCertificate(String certificate) throws Utils.ApplicationError, DataNotFoundError {
initFriends();
for (Friend friend : mFriends) {
if (friend.mPublicIdentity.mX509Certificate.equals(certificate)) {
return friend;
}
}
throw new DataNotFoundError();
}
public synchronized void addFriend(Friend friend) throws Utils.ApplicationError {
initFriends();
boolean friendWithIdExists = true;
boolean friendWithNicknameExists = true;
try {
getFriendById(friend.mId);
} catch (DataNotFoundError e) {
friendWithIdExists = false;
}
try {
getFriendByNickname(friend.mPublicIdentity.mNickname);
} catch (DataNotFoundError e) {
friendWithNicknameExists = false;
}
// TODO: report which conflict occurred
if (friendWithIdExists || friendWithNicknameExists) {
throw new DataAlreadyExistsError();
}
List<Friend> newFriends = new ArrayList<Friend>(mFriends);
newFriends.add(friend);
writeFile(FRIENDS_FILENAME, Json.toJson(newFriends));
mFriends.add(friend);
Log.addEntry(LOG_TAG, "added friend: " + friend.mPublicIdentity.mNickname);
Events.post(new Events.AddedFriend(friend.mId));
}
private void updateFriendHelper(List<Friend> list, Friend friend) throws DataNotFoundError {
boolean found = false;
for (int i = 0; i < list.size(); i++) {
if (list.get(i).mId.equals(friend.mId)) {
list.set(i, friend);
found = true;
break;
}
}
if (!found) {
throw new DataNotFoundError();
}
}
public synchronized void updateFriend(Friend friend) throws Utils.ApplicationError {
initFriends();
List<Friend> newFriends = new ArrayList<Friend>(mFriends);
updateFriendHelper(newFriends, friend);
writeFile(FRIENDS_FILENAME, Json.toJson(newFriends));
updateFriendHelper(mFriends, friend);
Log.addEntry(LOG_TAG, "updated friend: " + friend.mPublicIdentity.mNickname);
Events.post(new Events.UpdatedFriend(friend.mId));
}
public synchronized Date getFriendLastSentStatusTimestamp(String friendId) throws Utils.ApplicationError {
Friend friend = getFriendById(friendId);
return friend.mLastSentStatusTimestamp;
}
public synchronized void updateFriendLastSentStatusTimestamp(String friendId) throws Utils.ApplicationError {
// TODO: don't write an entire file for each timestamp update!
Friend friend = getFriendById(friendId);
updateFriend(
new Friend(
friend.mPublicIdentity,
friend.mAddedTimestamp,
new Date(),
friend.mLastReceivedStatusTimestamp));
}
public synchronized Date getFriendLastReceivedStatusTimestamp(String friendId) throws Utils.ApplicationError {
Friend friend = getFriendById(friendId);
return friend.mLastReceivedStatusTimestamp;
}
public synchronized void updateFriendLastReceivedStatusTimestamp(String friendId) throws Utils.ApplicationError {
// TODO: don't write an entire file for each timestamp update!
Friend friend = getFriendById(friendId);
updateFriend(
new Friend(
friend.mPublicIdentity,
friend.mAddedTimestamp,
friend.mLastSentStatusTimestamp,
new Date()));
}
private void removeFriendHelper(String id, List<Friend> list) throws DataNotFoundError {
boolean found = false;
for (int i = 0; i < list.size(); i++) {
if (list.get(i).mId.equals(id)) {
list.remove(i);
found = true;
break;
}
}
if (!found) {
throw new DataNotFoundError();
}
}
public synchronized void removeFriend(String id) throws Utils.ApplicationError, DataNotFoundError {
initFriends();
Friend friend = getFriendById(id);
deleteFile(String.format(FRIEND_STATUS_FILENAME_FORMAT_STRING, id));
List<Friend> newFriends = new ArrayList<Friend>(mFriends);
removeFriendHelper(id, newFriends);
writeFile(FRIENDS_FILENAME, Json.toJson(newFriends));
removeFriendHelper(id, mFriends);
Log.addEntry(LOG_TAG, "removed friend: " + friend.mPublicIdentity.mNickname);
Events.post(new Events.RemovedFriend(id));
// Reset all-messages to remove messages from deleted friend
// TODO: reset new-messages
mAllMessages = null;
initMessages();
}
public synchronized Status getFriendStatus(String id) throws Utils.ApplicationError, DataNotFoundError {
String filename = String.format(FRIEND_STATUS_FILENAME_FORMAT_STRING, id);
return Json.fromJson(readFile(filename), Status.class);
}
public synchronized void updateFriendStatus(String id, Status status) throws Utils.ApplicationError {
// Hack: initMessages before committing new status to avoid duplicate adds in addFriendMessagesHelper
initMessages();
Friend friend = getFriendById(id);
Status previousStatus = null;
try {
previousStatus = getFriendStatus(id);
// Mitigate push/pull race condition where older status overwrites newer status
// Only checks messages, not location
// TODO: more robust protocol... don't rely on clocks
if (previousStatus.mMessages.size() > status.mMessages.size()) {
Log.addEntry(LOG_TAG, "discarded friend status (fewer messages): " + friend.mPublicIdentity.mNickname);
return;
} else if (previousStatus.mMessages.size() == status.mMessages.size() && previousStatus.mMessages.size() > 0) {
if (previousStatus.mMessages.get(0).mTimestamp == null || status.mMessages.get(0).mTimestamp == null) {
Log.addEntry(LOG_TAG, "discarded friend status (timestamp unexpectedly null): " + friend.mPublicIdentity.mNickname);
return;
} else if (previousStatus.mMessages.get(0).mTimestamp.after(status.mMessages.get(0).mTimestamp)) {
Log.addEntry(LOG_TAG, "discarded friend status (older timestamp): " + friend.mPublicIdentity.mNickname);
return;
}
}
} catch (DataNotFoundError e) {
}
String filename = String.format(FRIEND_STATUS_FILENAME_FORMAT_STRING, id);
writeFile(filename, Json.toJson(status));
Log.addEntry(LOG_TAG, "updated friend status: " + friend.mPublicIdentity.mNickname);
Events.post(new Events.UpdatedFriendStatus(friend.mId));
addFriendMessagesHelper(friend, status, previousStatus);
}
private void initMessages() throws Utils.ApplicationError {
// TODO: this implementation is only intended for the prototype, which isn't sending incremental updates
// TODO: persistent (on disk) new-message state?
// Note: new-messages is not cleared in start() or stop(), so its state is retained when the Engine restarts
if (mNewMessages == null) {
mNewMessages = new ArrayList<AnnotatedMessage>();
}
if (mAllMessages == null) {
mAllMessages = new ArrayList<AnnotatedMessage>();
Self self = getSelf();
try {
for (Message message : getSelfStatus().mMessages) {
mAllMessages.add(new AnnotatedMessage(self.mPublicIdentity, null, message));
}
} catch (DataNotFoundError e) {
// Skip
}
for (Friend friend : getFriends()) {
// Hack to continue supporting self-as-friend, for now
if (!self.mPublicIdentity.mX509Certificate.equals(friend.mPublicIdentity.mX509Certificate)) {
try {
for (Message message : getFriendStatus(friend.mId).mMessages) {
mAllMessages.add(new AnnotatedMessage(friend.mPublicIdentity, friend.mId, message));
}
} catch (DataNotFoundError e) {
// Skip
}
}
}
Collections.sort(mAllMessages, new AnnotatedMessageComparator());
Events.post(new Events.UpdatedAllMessages());
}
}
private void addFriendMessagesHelper(Friend friend, Status status, Status previousStatus) throws Utils.ApplicationError {
// TODO: this implementation is only intended for the prototype, which isn't sending incremental updates
initMessages();
Data.Message lastMessage = null;
if (previousStatus != null &&
previousStatus.mMessages.size() > 0) {
lastMessage = previousStatus.mMessages.get(0);
}
List<AnnotatedMessage> newMessages = new ArrayList<AnnotatedMessage>();
for (Data.Message message : status.mMessages) {
if (lastMessage == null ||
!message.mTimestamp.equals(lastMessage.mTimestamp) ||
!message.mContent.equals(lastMessage.mContent)) {
newMessages.add(new AnnotatedMessage(friend.mPublicIdentity, friend.mId, message));
// Automatically enqueue new message attachments for download
for (Resource resource : message.mAttachments) {
try {
addDownload(friend.mId, resource);
} catch (DataAlreadyExistsError e) {
// Ignore
}
}
} else {
break;
}
}
if (newMessages.size() > 0) {
mNewMessages.addAll(0, newMessages);
Events.post(new Events.UpdatedNewMessages());
// Hack to continue supporting self-as-friend, for now
if (!getSelf().mPublicIdentity.mX509Certificate.equals(friend.mPublicIdentity.mX509Certificate)) {
mAllMessages.addAll(0, newMessages);
Collections.sort(mAllMessages, new AnnotatedMessageComparator());
Events.post(new Events.UpdatedAllMessages());
}
}
}
private void addSelfMessageHelper(Self self, Message message) throws Utils.ApplicationError {
initMessages();
mAllMessages.add(0, new AnnotatedMessage(self.mPublicIdentity, null, message));
Events.post(new Events.UpdatedAllMessages());
}
public synchronized List<AnnotatedMessage> getNewMessages() throws Utils.ApplicationError {
initMessages();
return new ArrayList<AnnotatedMessage>(mNewMessages);
}
public synchronized void resetNewMessages() throws Utils.ApplicationError {
initMessages();
boolean updatedNewMessages = (mNewMessages.size() > 0);
mNewMessages.clear();
if (updatedNewMessages) {
Events.post(new Events.UpdatedNewMessages());
}
}
public synchronized List<AnnotatedMessage> getAllMessages() throws Utils.ApplicationError {
initMessages();
return new ArrayList<AnnotatedMessage>(mAllMessages);
}
private void initLocalResources() throws Utils.ApplicationError {
if (mLocalResources == null) {
try {
mLocalResources = new ArrayList<LocalResource>(Arrays.asList(Json.fromJson(readFile(LOCAL_RESOURCES_FILENAME), LocalResource[].class)));
} catch (DataNotFoundError e) {
mLocalResources = new ArrayList<LocalResource>();
}
}
}
public synchronized LocalResource getLocalResource(String resourceId) throws Utils.ApplicationError, DataNotFoundError {
initLocalResources();
for (LocalResource localResource : mLocalResources) {
if (localResource.mResourceId.equals(resourceId)) {
return localResource;
}
}
throw new DataNotFoundError();
}
private void initDownloads() throws Utils.ApplicationError {
if (mDownloads == null) {
try {
mDownloads = new ArrayList<Download>(Arrays.asList(Json.fromJson(readFile(DOWNLOADS_FILENAME), Download[].class)));
} catch (DataNotFoundError e) {
mDownloads = new ArrayList<Download>();
}
}
}
public synchronized Download getDownload(String friendId, String resourceId) throws Utils.ApplicationError, DataNotFoundError {
initDownloads();
for (Download download : mDownloads) {
if (download.mFriendId.equals(friendId) && download.mResourceId.equals(resourceId)) {
return download;
}
}
throw new DataNotFoundError();
}
public synchronized Download getNextInProgressDownload(String friendId) throws Utils.ApplicationError, DataNotFoundError {
initDownloads();
for (Download download : mDownloads) {
if (download.mFriendId.equals(friendId) && download.mState == Download.State.IN_PROGRESS) {
return download;
}
}
throw new DataNotFoundError();
}
public synchronized void addDownload(String friendId, Resource resource) throws Utils.ApplicationError, DataAlreadyExistsError {
initDownloads();
try {
getDownload(friendId, resource.mId);
throw new DataAlreadyExistsError();
} catch (DataNotFoundError e) {
}
Friend friend = getFriendById(friendId);
// TODO: double check resource ID is from valid resource in friend message?
Download download = new Download(friendId, resource.mId, resource.mMimeType, resource.mSize, Download.State.IN_PROGRESS);
List<Download> newDownloads = new ArrayList<Download>(mDownloads);
newDownloads.add(download);
writeFile(DOWNLOADS_FILENAME, Json.toJson(newDownloads));
mDownloads.add(download);
Log.addEntry(LOG_TAG, "added download from friend: " + friend.mPublicIdentity.mNickname);
Events.post(new Events.AddedDownload(friendId, resource.mId));
}
private void updateDownloadHelper(List<Download> list, Download download) throws DataNotFoundError {
boolean found = false;
for (int i = 0; i < list.size(); i++) {
if (list.get(i).mFriendId.equals(download.mFriendId) && list.get(i).mResourceId.equals(download.mResourceId)) {
list.set(i, download);
found = true;
break;
}
}
if (!found) {
throw new DataNotFoundError();
}
}
public synchronized void updateDownloadState(String friendId, String resourceId, Download.State state) throws Utils.ApplicationError, DataNotFoundError {
initDownloads();
Friend friend = getFriendById(friendId);
Download download = getDownload(friendId, resourceId);
Download newDownload = new Download(download.mFriendId, download.mResourceId, download.mMimeType, download.mSize, state);
List<Download> newDownloads = new ArrayList<Download>(mDownloads);
updateDownloadHelper(newDownloads, newDownload);
writeFile(DOWNLOADS_FILENAME, Json.toJson(newDownloads));
updateDownloadHelper(mDownloads, newDownload);
if (state == Download.State.IN_PROGRESS) {
Log.addEntry(LOG_TAG, "resumed download from friend: " + friend.mPublicIdentity.mNickname);
} else if (state == Download.State.CANCELLED) {
Log.addEntry(LOG_TAG, "cancelled download from friend: " + friend.mPublicIdentity.mNickname);
} else if (state == Download.State.COMPLETE) {
Log.addEntry(LOG_TAG, "completed download from friend: " + friend.mPublicIdentity.mNickname);
}
// *** TODO: engine stop downloading on cancel
// *** TODO: delete download file on cancel
//Events.post(new Events.UpdatedDownloadState());
}
private static String readFile(String filename) throws Utils.ApplicationError, DataNotFoundError {
FileInputStream inputStream = null;
try {
File directory = Utils.getApplicationContext().getDir(DATA_DIRECTORY, Context.MODE_PRIVATE);
String commitFilename = filename + COMMIT_FILENAME_SUFFIX;
File commitFile = new File(directory, commitFilename);
File file = new File(directory, filename);
replaceFileIfExists(commitFile, file);
inputStream = new FileInputStream(file);
return Utils.readInputStreamToString(inputStream);
} catch (FileNotFoundException e) {
throw new DataNotFoundError();
} catch (IOException e) {
throw new Utils.ApplicationError(LOG_TAG, e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
}
}
}
}
private static void writeFile(String filename, String value) throws Utils.ApplicationError {
FileOutputStream outputStream = null;
try {
File directory = Utils.getApplicationContext().getDir(DATA_DIRECTORY, Context.MODE_PRIVATE);
String commitFilename = filename + COMMIT_FILENAME_SUFFIX;
File commitFile = new File(directory, commitFilename);
File file = new File(directory, filename);
outputStream = new FileOutputStream(commitFile);
outputStream.write(value.getBytes());
outputStream.close();
replaceFileIfExists(commitFile, file);
} catch (IOException e) {
throw new Utils.ApplicationError(LOG_TAG, e);
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
}
}
}
}
private static void replaceFileIfExists(File commitFile, File file) throws IOException {
if (commitFile.exists()) {
file.delete();
commitFile.renameTo(file);
}
}
private static void deleteFile(String filename) throws Utils.ApplicationError {
File directory = Utils.getApplicationContext().getDir(DATA_DIRECTORY, Context.MODE_PRIVATE);
File file = new File(directory, filename);
if (!file.delete()) {
if (file.exists()) {
throw new Utils.ApplicationError(LOG_TAG, "failed to delete file");
}
}
}
}