Compare commits

..

49 Commits

Author SHA1 Message Date
akwizgran
fdcacde915 WIP: Integration test for contact list worker 2022-08-02 13:19:04 +01:00
akwizgran
e74021f571 Rename MailboxIntegrationTest, factor out API creation. 2022-08-02 13:17:44 +01:00
Torsten Grote
6b6880c1ff Merge branch 'tolerate-missing-folders' into 'master'
Tolerate 404 responses due to missing folders

See merge request briar/briar!1692
2022-08-01 13:40:33 +00:00
akwizgran
5defd500ae Tolerate 404 responses due to missing folders.
This prevents our own mailbox's download worker from getting stuck trying to list a folder that has been removed. Instead, the worker will move on to the next folder.
2022-07-27 16:36:32 +01:00
Torsten Grote
37ff06d192 Merge branch '2290-client-for-own-mailbox' into 'master'
Add mailbox client for our own mailbox

Closes #2290

See merge request briar/briar!1691
2022-07-18 14:27:19 +00:00
akwizgran
85aa21ebf6 Address review feedback. 2022-07-18 11:25:27 +01:00
akwizgran
e448699895 Merge branch 'briar-about-update' into 'master'
Briar about update

See merge request briar/briar!1690
2022-07-18 10:08:41 +00:00
Torsten Grote
200f83bcfe Merge branch '2293-own-mailbox-download-worker' into 'master'
Mailbox download worker for our own mailbox

Closes #2293

See merge request briar/briar!1689
2022-07-15 19:40:48 +00:00
FlyingP1g FlyingP1g
89cce89650 About menu: Added url method. 2022-07-15 20:25:43 +03:00
akwizgran
8982964fbf Add mailbox client for our own mailbox. 2022-07-15 18:00:13 +01:00
FlyingP1g FlyingP1g
f3a3fa0ea8 Merge branch 'master' into briar-about-update-bad
# Conflicts:
#	briar-android/src/main/res/layout/fragment_about.xml
#	briar-android/src/main/res/values/strings.xml
2022-07-15 19:18:21 +03:00
akwizgran
0865a06ac8 Refactor duplicated test code into superclass. 2022-07-15 16:24:22 +01:00
akwizgran
f2738c8bc4 Add some javadocs. 2022-07-15 16:07:26 +01:00
akwizgran
1321f8775e Refactor duplicated code into superclass. 2022-07-15 15:45:55 +01:00
akwizgran
9764aba47d Add download worker for own mailbox. 2022-07-15 15:19:44 +01:00
akwizgran
913e5da2f5 Refactor test expectations, add test for nothing to download. 2022-07-15 15:19:11 +01:00
akwizgran
f2ce7a386b Merge branch 'briar-info-translators-string' into 'master'
Added "translated by" to about menu.

See merge request briar/briar!1687
2022-07-15 13:26:02 +00:00
FlyingP1g FlyingP1g
7607b65e82 Added "translated by" to about menu. 2022-07-15 13:26:02 +00:00
FlyingP1g FlyingP1g
c13c2d62f5 About menu: Added tor version and small wording fixes 2022-07-12 22:26:58 +03:00
FlyingP1g FlyingP1g
8ea7204cf6 About menu: Added changelog and fixed wording 2022-07-12 21:10:29 +03:00
FlyingP1g FlyingP1g
6ec382cfc4 About menu: Thanks to translators redesign 2022-07-12 20:35:53 +03:00
FlyingP1g FlyingP1g
ad0b28a684 Better wording. 2022-07-12 19:18:29 +03:00
FlyingP1g FlyingP1g
0ae94e9579 String fix 2022-07-12 17:30:48 +03:00
FlyingP1g FlyingP1g
57bd5789d4 About menu contribution fixes. 2022-07-12 17:07:28 +03:00
FlyingP1g FlyingP1g
f7dde1250c Added "translated by" to about menu. 2022-07-12 00:54:15 +03:00
akwizgran
13d96651b4 Merge branch '2329-translations-for-trust-indicator-view-trust-levels' into 'master'
feat: [2329] adding initial translations for contact trust levels

Closes #2329

See merge request briar/briar!1664
2022-07-11 12:49:09 +00:00
johndoe4221
65029982ce feat: [2329] changing translation of trustlevel to 'verifified contact'/'unverified contact' 2022-07-11 14:31:54 +02:00
johndoe4221
380921ce25 Merge branch 'master' of https://code.briarproject.org/johndoe4221/briar into 2329-translations-for-trust-indicator-view-trust-levels 2022-07-11 14:28:28 +02:00
Torsten Grote
87ee8cd653 Merge branch 'jtorctl-0.5' into 'master'
Crash as soon as Tor closes the control connection

See merge request briar/briar!1686
2022-07-11 12:04:25 +00:00
akwizgran
d4810a6f71 Merge branch 'mr/contributing-md' into 'master'
Create CONTRIBUTING.md (describe folder names)

See merge request briar/briar!1685
2022-07-11 11:40:01 +00:00
Thomas
aa56aba1a5 Create CONTRIBUTING.md (describe folder names) 2022-07-11 11:40:01 +00:00
akwizgran
35438dbac1 Merge branch 'briar-info' into 'master'
Added about menu.

See merge request briar/briar!1683
2022-07-11 11:17:13 +00:00
johndoe4221
543b1178a1 feat: [2329] use term peer-trust-level 2022-07-07 19:10:27 +02:00
johndoe4221
7f1071f5cd feat: [2329] adding initial translations for contact trust levels 2022-07-07 19:06:45 +02:00
johndoe4221
e8c694fe00 feat: [2329] changing terminology from contact-trust-level to author-trust-level 2022-07-07 18:06:41 +02:00
johndoe4221
b58b0c74a9 feat: [2329][1630] remove translation for anonymous trust level 2022-07-07 18:06:41 +02:00
johndoe4221
9e5029917e feat: [2329] adding initial translations for contact trust levels 2022-07-07 18:06:40 +02:00
FlyingP1g FlyingP1g
12ca74f86a Minor about menu improvements. 2022-07-07 16:26:12 +03:00
akwizgran
622683f45e Crash as soon as Tor closes the control connection. 2022-07-06 15:38:23 +01:00
johndoe4221
e66f92f27e Merge branch '2329-translations-for-trust-indicator-view-trust-levels' of https://code.briarproject.org/johndoe4221/briar into 2329-translations-for-trust-indicator-view-trust-levels 2022-07-02 09:55:42 +02:00
johndoe4221
44acda2045 feat: [2329] changing terminology from contact-trust-level to author-trust-level 2022-07-02 09:43:53 +02:00
johndoe4221
afd92dd916 feat: [2329][1630] remove translation for anonymous trust level 2022-07-02 09:38:35 +02:00
johndoe4221
2a969f8e0b feat: [2329] adding initial translations for contact trust levels 2022-07-02 09:38:35 +02:00
FlyingP1g FlyingP1g
ddc6606ccf About menu improvements. 2022-07-01 17:25:14 +03:00
FlyingP1g FlyingP1g
a19a4f36c6 Update strings.xml 2022-06-30 16:14:10 +00:00
FlyingP1g FlyingP1g
6765de992d Added about menu. 2022-06-30 00:05:29 +03:00
johndoe4221
b24a18b231 feat: [2329][1630] remove translation for anonymous trust level 2022-06-16 18:10:21 +02:00
johndoe4221
8e83743dd7 Merge remote-tracking branch 'origin/master' into 2329-translations-for-trust-indicator-view-trust-levels 2022-06-15 12:19:24 +02:00
johndoe4221
6a91d18003 feat: [2329] adding initial translations for contact trust levels 2022-06-09 21:47:50 +02:00
31 changed files with 1634 additions and 431 deletions

10
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,10 @@
Folder-Description:
===================
* `briar-*`: Specifically for the Briar app (Phone/Desktop/Headless)
* `bramble-*`: The protocol stack - not necessarily Briar-dependent
---
* `*-api`: public stuff that can be referenced from other packages and modules - mostly interfaces + a few utility classes
* `*-core`: implementations of api that are portable across Android/Desktop/Headless
* `*-java`: implementations of api that are specific to Desktop & Headless

View File

@@ -16,7 +16,7 @@ dependencies {
implementation 'org.bitlet:weupnp:0.1.4'
implementation 'net.i2p.crypto:eddsa:0.2.0'
implementation 'org.whispersystems:curve25519-java:0.5.0'
implementation 'org.briarproject:jtorctl:0.4'
implementation 'org.briarproject:jtorctl:0.5'
//noinspection GradleDependency
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"

View File

@@ -10,7 +10,6 @@ import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import javax.inject.Inject;
import static java.util.logging.Logger.getLogger;
@@ -30,7 +29,6 @@ class ContactMailboxClient implements MailboxClient {
@Nullable
private MailboxWorker uploadWorker = null, downloadWorker = null;
@Inject
ContactMailboxClient(MailboxWorkerFactory workerFactory,
ConnectivityChecker connectivityChecker,
TorReachabilityMonitor reachabilityMonitor) {

View File

@@ -1,73 +1,25 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Cancellable;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import org.briarproject.bramble.mailbox.TorReachabilityMonitor.TorReachabilityObserver;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.util.LogUtils.logException;
@ThreadSafe
@NotNullByDefault
class ContactMailboxDownloadWorker implements MailboxWorker,
ConnectivityObserver, TorReachabilityObserver {
/**
* When the worker is started it waits for a connectivity check, then
* starts its first download cycle: checking the inbox, downloading and
* deleting any files, and checking again until the inbox is empty.
* <p>
* The worker then waits for our Tor hidden service to be reachable before
* starting its second download cycle. This ensures that if a contact
* tried and failed to connect to our hidden service before it was
* reachable, and therefore uploaded a file to the mailbox instead, we'll
* find the file in the second download cycle.
*/
private enum State {
CREATED,
CONNECTIVITY_CHECK,
DOWNLOAD_CYCLE_1,
WAITING_FOR_TOR,
DOWNLOAD_CYCLE_2,
FINISHED,
DESTROYED
}
private static final Logger LOG =
getLogger(ContactMailboxDownloadWorker.class.getName());
private final ConnectivityChecker connectivityChecker;
private final TorReachabilityMonitor torReachabilityMonitor;
private final MailboxApiCaller mailboxApiCaller;
private final MailboxApi mailboxApi;
private final MailboxFileManager mailboxFileManager;
private final MailboxProperties mailboxProperties;
private final Object lock = new Object();
@GuardedBy("lock")
private State state = State.CREATED;
@GuardedBy("lock")
@Nullable
private Cancellable apiCall = null;
class ContactMailboxDownloadWorker extends MailboxDownloadWorker {
ContactMailboxDownloadWorker(
ConnectivityChecker connectivityChecker,
@@ -76,57 +28,14 @@ class ContactMailboxDownloadWorker implements MailboxWorker,
MailboxApi mailboxApi,
MailboxFileManager mailboxFileManager,
MailboxProperties mailboxProperties) {
super(connectivityChecker, torReachabilityMonitor, mailboxApiCaller,
mailboxApi, mailboxFileManager, mailboxProperties);
if (mailboxProperties.isOwner()) throw new IllegalArgumentException();
this.connectivityChecker = connectivityChecker;
this.torReachabilityMonitor = torReachabilityMonitor;
this.mailboxApiCaller = mailboxApiCaller;
this.mailboxApi = mailboxApi;
this.mailboxFileManager = mailboxFileManager;
this.mailboxProperties = mailboxProperties;
}
@Override
public void start() {
LOG.info("Started");
synchronized (lock) {
// Don't allow the worker to be reused
if (state != State.CREATED) return;
state = State.CONNECTIVITY_CHECK;
}
// Avoid leaking observer in case destroy() is called concurrently
// before observer is added
connectivityChecker.checkConnectivity(mailboxProperties, this);
boolean destroyed;
synchronized (lock) {
destroyed = state == State.DESTROYED;
}
if (destroyed) connectivityChecker.removeObserver(this);
}
@Override
public void destroy() {
LOG.info("Destroyed");
Cancellable apiCall;
synchronized (lock) {
state = State.DESTROYED;
apiCall = this.apiCall;
this.apiCall = null;
}
if (apiCall != null) apiCall.cancel();
connectivityChecker.removeObserver(this);
torReachabilityMonitor.removeObserver(this);
}
@Override
public void onConnectivityCheckSucceeded() {
LOG.info("Connectivity check succeeded");
synchronized (lock) {
if (state != State.CONNECTIVITY_CHECK) return;
state = State.DOWNLOAD_CYCLE_1;
// Start first download cycle
apiCall = mailboxApiCaller.retryWithBackoff(
new SimpleApiCall(this::apiCallListInbox));
}
protected ApiCall createApiCallForDownloadCycle() {
return new SimpleApiCall(this::apiCallListInbox);
}
private void apiCallListInbox() throws IOException, ApiException {
@@ -134,110 +43,23 @@ class ContactMailboxDownloadWorker implements MailboxWorker,
if (state == State.DESTROYED) return;
}
LOG.info("Listing inbox");
List<MailboxFile> files = mailboxApi.getFiles(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()));
if (files.isEmpty()) onDownloadCycleFinished();
else downloadNextFile(new LinkedList<>(files));
}
private void onDownloadCycleFinished() {
boolean addObserver = false;
synchronized (lock) {
if (state == State.DOWNLOAD_CYCLE_1) {
LOG.info("First download cycle finished");
state = State.WAITING_FOR_TOR;
apiCall = null;
addObserver = true;
} else if (state == State.DOWNLOAD_CYCLE_2) {
LOG.info("Second download cycle finished");
state = State.FINISHED;
apiCall = null;
}
}
if (addObserver) {
// Avoid leaking observer in case destroy() is called concurrently
// before observer is added
torReachabilityMonitor.addOneShotObserver(this);
boolean destroyed;
synchronized (lock) {
destroyed = state == State.DESTROYED;
}
if (destroyed) torReachabilityMonitor.removeObserver(this);
}
}
private void downloadNextFile(Queue<MailboxFile> queue) {
synchronized (lock) {
if (state == State.DESTROYED) return;
MailboxFile file = queue.remove();
apiCall = mailboxApiCaller.retryWithBackoff(
new SimpleApiCall(() -> apiCallDownloadFile(file, queue)));
}
}
private void apiCallDownloadFile(MailboxFile file,
Queue<MailboxFile> queue) throws IOException, ApiException {
synchronized (lock) {
if (state == State.DESTROYED) return;
}
LOG.info("Downloading file");
File tempFile = mailboxFileManager.createTempFileForDownload();
MailboxFolderId folderId =
requireNonNull(mailboxProperties.getInboxId());
List<MailboxFile> files;
try {
mailboxApi.getFile(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()),
file.name, tempFile);
} catch (IOException | ApiException e) {
if (!tempFile.delete()) {
LOG.warning("Failed to delete temporary file");
}
throw e;
}
mailboxFileManager.handleDownloadedFile(tempFile);
deleteFile(file, queue);
}
private void deleteFile(MailboxFile file, Queue<MailboxFile> queue) {
synchronized (lock) {
if (state == State.DESTROYED) return;
apiCall = mailboxApiCaller.retryWithBackoff(
new SimpleApiCall(() -> apiCallDeleteFile(file, queue)));
}
}
private void apiCallDeleteFile(MailboxFile file, Queue<MailboxFile> queue)
throws IOException, ApiException {
synchronized (lock) {
if (state == State.DESTROYED) return;
}
try {
mailboxApi.deleteFile(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()), file.name);
files = mailboxApi.getFiles(mailboxProperties, folderId);
} catch (TolerableFailureException e) {
// Catch this so we can continue to the next file
logException(LOG, INFO, e);
LOG.warning("Inbox folder does not exist");
files = emptyList();
}
if (queue.isEmpty()) {
// List the inbox again to check for files that may have arrived
// while we were downloading
synchronized (lock) {
if (state == State.DESTROYED) return;
apiCall = mailboxApiCaller.retryWithBackoff(
new SimpleApiCall(this::apiCallListInbox));
}
if (files.isEmpty()) {
onDownloadCycleFinished();
} else {
Queue<FolderFile> queue = new LinkedList<>();
for (MailboxFile file : files) {
queue.add(new FolderFile(folderId, file.name));
}
downloadNextFile(queue);
}
}
@Override
public void onTorReachable() {
LOG.info("Our Tor hidden service is reachable");
synchronized (lock) {
if (state != State.WAITING_FOR_TOR) return;
state = State.DOWNLOAD_CYCLE_2;
// Start second download cycle
apiCall = mailboxApiCaller.retryWithBackoff(
new SimpleApiCall(this::apiCallListInbox));
}
}
}

View File

@@ -91,9 +91,13 @@ interface MailboxApi {
* Used by owner and contacts to list their files to retrieve.
* <p>
* Returns 200 OK with the list of files in JSON.
*
* @throws TolerableFailureException if response code is 404 (folder does
* not exist or client is not authorised to download from it)
*/
List<MailboxFile> getFiles(MailboxProperties properties,
MailboxFolderId folderId) throws IOException, ApiException;
MailboxFolderId folderId)
throws IOException, ApiException, TolerableFailureException;
/**
* Used by owner and contacts to retrieve a file.
@@ -102,17 +106,22 @@ interface MailboxApi {
* in the response body.
*
* @param file the empty file the response bytes will be written into.
* @throws TolerableFailureException if response code is 404 (folder does
* not exist, client is not authorised to download from folder, or file
* does not exist)
*/
void getFile(MailboxProperties properties, MailboxFolderId folderId,
MailboxFileId fileId, File file) throws IOException, ApiException;
MailboxFileId fileId, File file)
throws IOException, ApiException, TolerableFailureException;
/**
* Used by owner and contacts to delete files.
* <p>
* Returns 200 OK (no exception) if deletion was successful.
*
* @throws TolerableFailureException on 404 response,
* because file was most likely deleted already.
* @throws TolerableFailureException if response code is 404 (folder does
* not exist, client is not authorised to download from folder, or file
* does not exist)
*/
void deleteFile(MailboxProperties properties, MailboxFolderId folderId,
MailboxFileId fileId)

View File

@@ -216,9 +216,11 @@ class MailboxApiImpl implements MailboxApi {
@Override
public List<MailboxFile> getFiles(MailboxProperties properties,
MailboxFolderId folderId) throws IOException, ApiException {
MailboxFolderId folderId)
throws IOException, ApiException, TolerableFailureException {
String path = "/files/" + folderId;
Response response = sendGetRequest(properties, path);
if (response.code() == 404) throw new TolerableFailureException();
if (response.code() != 200) throw new ApiException();
ResponseBody body = response.body();
@@ -252,9 +254,11 @@ class MailboxApiImpl implements MailboxApi {
@Override
public void getFile(MailboxProperties properties, MailboxFolderId folderId,
MailboxFileId fileId, File file) throws IOException, ApiException {
MailboxFileId fileId, File file)
throws IOException, ApiException, TolerableFailureException {
String path = "/files/" + folderId + "/" + fileId;
Response response = sendGetRequest(properties, path);
if (response.code() == 404) throw new TolerableFailureException();
if (response.code() != 200) throw new ApiException();
ResponseBody body = response.body();

View File

@@ -24,6 +24,10 @@ interface MailboxClient {
/**
* Assigns a contact to the client for upload.
*
* @param properties Properties for communicating with the mailbox
* managed by this client.
* @param folderId The ID of the folder to which files will be uploaded.
*/
void assignContactForUpload(ContactId c, MailboxProperties properties,
MailboxFolderId folderId);
@@ -35,6 +39,11 @@ interface MailboxClient {
/**
* Assigns a contact to the client for download.
*
* @param properties Properties for communicating with the mailbox
* managed by this client.
* @param folderId The ID of the folder from which files will be
* downloaded.
*/
void assignContactForDownload(ContactId c, MailboxProperties properties,
MailboxFolderId folderId);

View File

@@ -0,0 +1,250 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Cancellable;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import org.briarproject.bramble.mailbox.TorReachabilityMonitor.TorReachabilityObserver;
import java.io.File;
import java.io.IOException;
import java.util.Queue;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Logger.getLogger;
@ThreadSafe
@NotNullByDefault
abstract class MailboxDownloadWorker implements MailboxWorker,
ConnectivityObserver, TorReachabilityObserver {
/**
* When the worker is started it waits for a connectivity check, then
* starts its first download cycle: checking for files to download,
* downloading and deleting the files, and checking again until all files
* have been downloaded and deleted.
* <p>
* The worker then waits for our Tor hidden service to be reachable before
* starting its second download cycle. This ensures that if a contact
* tried and failed to connect to our hidden service before it was
* reachable, and therefore uploaded a file to the mailbox instead, we'll
* find the file in the second download cycle.
*/
protected enum State {
CREATED,
CONNECTIVITY_CHECK,
DOWNLOAD_CYCLE_1,
WAITING_FOR_TOR,
DOWNLOAD_CYCLE_2,
FINISHED,
DESTROYED
}
protected static final Logger LOG =
getLogger(MailboxDownloadWorker.class.getName());
private final ConnectivityChecker connectivityChecker;
private final TorReachabilityMonitor torReachabilityMonitor;
protected final MailboxApiCaller mailboxApiCaller;
protected final MailboxApi mailboxApi;
private final MailboxFileManager mailboxFileManager;
protected final MailboxProperties mailboxProperties;
protected final Object lock = new Object();
@GuardedBy("lock")
protected State state = State.CREATED;
@GuardedBy("lock")
@Nullable
protected Cancellable apiCall = null;
/**
* Creates the API call that starts the worker's download cycle.
*/
protected abstract ApiCall createApiCallForDownloadCycle();
MailboxDownloadWorker(
ConnectivityChecker connectivityChecker,
TorReachabilityMonitor torReachabilityMonitor,
MailboxApiCaller mailboxApiCaller,
MailboxApi mailboxApi,
MailboxFileManager mailboxFileManager,
MailboxProperties mailboxProperties) {
this.connectivityChecker = connectivityChecker;
this.torReachabilityMonitor = torReachabilityMonitor;
this.mailboxApiCaller = mailboxApiCaller;
this.mailboxApi = mailboxApi;
this.mailboxFileManager = mailboxFileManager;
this.mailboxProperties = mailboxProperties;
}
@Override
public void start() {
LOG.info("Started");
synchronized (lock) {
// Don't allow the worker to be reused
if (state != State.CREATED) return;
state = State.CONNECTIVITY_CHECK;
}
// Avoid leaking observer in case destroy() is called concurrently
// before observer is added
connectivityChecker.checkConnectivity(mailboxProperties, this);
boolean destroyed;
synchronized (lock) {
destroyed = state == State.DESTROYED;
}
if (destroyed) connectivityChecker.removeObserver(this);
}
@Override
public void destroy() {
LOG.info("Destroyed");
Cancellable apiCall;
synchronized (lock) {
state = State.DESTROYED;
apiCall = this.apiCall;
this.apiCall = null;
}
if (apiCall != null) apiCall.cancel();
connectivityChecker.removeObserver(this);
torReachabilityMonitor.removeObserver(this);
}
@Override
public void onConnectivityCheckSucceeded() {
LOG.info("Connectivity check succeeded");
synchronized (lock) {
if (state != State.CONNECTIVITY_CHECK) return;
state = State.DOWNLOAD_CYCLE_1;
// Start first download cycle
apiCall = mailboxApiCaller.retryWithBackoff(
createApiCallForDownloadCycle());
}
}
void onDownloadCycleFinished() {
boolean addObserver = false;
synchronized (lock) {
if (state == State.DOWNLOAD_CYCLE_1) {
LOG.info("First download cycle finished");
state = State.WAITING_FOR_TOR;
apiCall = null;
addObserver = true;
} else if (state == State.DOWNLOAD_CYCLE_2) {
LOG.info("Second download cycle finished");
state = State.FINISHED;
apiCall = null;
}
}
if (addObserver) {
// Avoid leaking observer in case destroy() is called concurrently
// before observer is added
torReachabilityMonitor.addOneShotObserver(this);
boolean destroyed;
synchronized (lock) {
destroyed = state == State.DESTROYED;
}
if (destroyed) torReachabilityMonitor.removeObserver(this);
}
}
void downloadNextFile(Queue<FolderFile> queue) {
synchronized (lock) {
if (state == State.DESTROYED) return;
if (queue.isEmpty()) {
// Check for files again, as new files may have arrived while
// we were downloading
apiCall = mailboxApiCaller.retryWithBackoff(
createApiCallForDownloadCycle());
} else {
FolderFile file = queue.remove();
apiCall = mailboxApiCaller.retryWithBackoff(
new SimpleApiCall(() ->
apiCallDownloadFile(file, queue)));
}
}
}
private void apiCallDownloadFile(FolderFile file, Queue<FolderFile> queue)
throws IOException, ApiException {
synchronized (lock) {
if (state == State.DESTROYED) return;
}
LOG.info("Downloading file");
File tempFile = mailboxFileManager.createTempFileForDownload();
try {
mailboxApi.getFile(mailboxProperties, file.folderId, file.fileId,
tempFile);
} catch (IOException | ApiException e) {
if (!tempFile.delete()) {
LOG.warning("Failed to delete temporary file");
}
throw e;
} catch (TolerableFailureException e) {
// File not found - continue to the next file
LOG.warning("File does not exist");
if (!tempFile.delete()) {
LOG.warning("Failed to delete temporary file");
}
downloadNextFile(queue);
return;
}
mailboxFileManager.handleDownloadedFile(tempFile);
deleteFile(file, queue);
}
private void deleteFile(FolderFile file, Queue<FolderFile> queue) {
synchronized (lock) {
if (state == State.DESTROYED) return;
apiCall = mailboxApiCaller.retryWithBackoff(
new SimpleApiCall(() -> apiCallDeleteFile(file, queue)));
}
}
private void apiCallDeleteFile(FolderFile file, Queue<FolderFile> queue)
throws IOException, ApiException {
synchronized (lock) {
if (state == State.DESTROYED) return;
}
try {
mailboxApi.deleteFile(mailboxProperties, file.folderId,
file.fileId);
} catch (TolerableFailureException e) {
// File not found - continue to the next file
LOG.warning("File does not exist");
}
downloadNextFile(queue);
}
@Override
public void onTorReachable() {
LOG.info("Our Tor hidden service is reachable");
synchronized (lock) {
if (state != State.WAITING_FOR_TOR) return;
state = State.DOWNLOAD_CYCLE_2;
// Start second download cycle
apiCall = mailboxApiCaller.retryWithBackoff(
createApiCallForDownloadCycle());
}
}
// Package access for testing
static class FolderFile {
final MailboxFolderId folderId;
final MailboxFileId fileId;
FolderFile(MailboxFolderId folderId, MailboxFileId fileId) {
this.folderId = folderId;
this.fileId = fileId;
}
}
}

View File

@@ -24,4 +24,8 @@ interface MailboxWorkerFactory {
ConnectivityChecker connectivityChecker,
TorReachabilityMonitor reachabilityMonitor,
MailboxProperties properties);
MailboxWorker createContactListWorkerForOwnMailbox(
ConnectivityChecker connectivityChecker,
MailboxProperties properties);
}

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.api.system.TaskScheduler;
@@ -27,6 +28,7 @@ class MailboxWorkerFactoryImpl implements MailboxWorkerFactory {
private final MailboxApiCaller mailboxApiCaller;
private final MailboxApi mailboxApi;
private final MailboxFileManager mailboxFileManager;
private final MailboxUpdateManager mailboxUpdateManager;
@Inject
MailboxWorkerFactoryImpl(@IoExecutor Executor ioExecutor,
@@ -36,7 +38,8 @@ class MailboxWorkerFactoryImpl implements MailboxWorkerFactory {
EventBus eventBus,
MailboxApiCaller mailboxApiCaller,
MailboxApi mailboxApi,
MailboxFileManager mailboxFileManager) {
MailboxFileManager mailboxFileManager,
MailboxUpdateManager mailboxUpdateManager) {
this.ioExecutor = ioExecutor;
this.db = db;
this.clock = clock;
@@ -45,6 +48,7 @@ class MailboxWorkerFactoryImpl implements MailboxWorkerFactory {
this.mailboxApiCaller = mailboxApiCaller;
this.mailboxApi = mailboxApi;
this.mailboxFileManager = mailboxFileManager;
this.mailboxUpdateManager = mailboxUpdateManager;
}
@Override
@@ -75,7 +79,17 @@ class MailboxWorkerFactoryImpl implements MailboxWorkerFactory {
ConnectivityChecker connectivityChecker,
TorReachabilityMonitor reachabilityMonitor,
MailboxProperties properties) {
// TODO
throw new UnsupportedOperationException();
return new OwnMailboxDownloadWorker(connectivityChecker,
reachabilityMonitor, mailboxApiCaller, mailboxApi,
mailboxFileManager, properties);
}
@Override
public MailboxWorker createContactListWorkerForOwnMailbox(
ConnectivityChecker connectivityChecker,
MailboxProperties properties) {
return new OwnMailboxContactListWorker(ioExecutor, db, eventBus,
connectivityChecker, mailboxApiCaller, mailboxApi,
mailboxUpdateManager, properties);
}
}

View File

@@ -0,0 +1,154 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Logger.getLogger;
@ThreadSafe
@NotNullByDefault
class OwnMailboxClient implements MailboxClient {
private static final Logger LOG =
getLogger(OwnMailboxClient.class.getName());
private final MailboxWorkerFactory workerFactory;
private final ConnectivityChecker connectivityChecker;
private final TorReachabilityMonitor reachabilityMonitor;
private final MailboxWorker contactListWorker;
private final Object lock = new Object();
/**
* Upload workers: one worker per contact assigned for upload.
*/
@GuardedBy("lock")
private final Map<ContactId, MailboxWorker> uploadWorkers = new HashMap<>();
/**
* Download worker: shared between all contacts assigned for download.
* Null if no contacts are assigned for download.
*/
@GuardedBy("lock")
@Nullable
private MailboxWorker downloadWorker = null;
/**
* IDs of contacts assigned for download, so that we know when to
* create/destroy the download worker.
*/
@GuardedBy("lock")
private final Set<ContactId> assignedForDownload = new HashSet<>();
OwnMailboxClient(MailboxWorkerFactory workerFactory,
ConnectivityChecker connectivityChecker,
TorReachabilityMonitor reachabilityMonitor,
MailboxProperties properties) {
if (!properties.isOwner()) throw new IllegalArgumentException();
this.workerFactory = workerFactory;
this.connectivityChecker = connectivityChecker;
this.reachabilityMonitor = reachabilityMonitor;
contactListWorker = workerFactory.createContactListWorkerForOwnMailbox(
connectivityChecker, properties);
}
@Override
public void start() {
LOG.info("Started");
contactListWorker.start();
}
@Override
public void destroy() {
LOG.info("Destroyed");
List<MailboxWorker> uploadWorkers;
MailboxWorker downloadWorker;
synchronized (lock) {
uploadWorkers = new ArrayList<>(this.uploadWorkers.values());
this.uploadWorkers.clear();
downloadWorker = this.downloadWorker;
this.downloadWorker = null;
}
// Destroy the workers (with apologies to Mr Marx and Mr Engels)
for (MailboxWorker worker : uploadWorkers) worker.destroy();
if (downloadWorker != null) downloadWorker.destroy();
contactListWorker.destroy();
}
@Override
public void assignContactForUpload(ContactId contactId,
MailboxProperties properties, MailboxFolderId folderId) {
LOG.info("Contact assigned for upload");
if (!properties.isOwner()) throw new IllegalArgumentException();
MailboxWorker uploadWorker = workerFactory.createUploadWorker(
connectivityChecker, properties, folderId, contactId);
synchronized (lock) {
MailboxWorker old = uploadWorkers.put(contactId, uploadWorker);
if (old != null) throw new IllegalStateException();
}
uploadWorker.start();
}
@Override
public void deassignContactForUpload(ContactId contactId) {
LOG.info("Contact deassigned for upload");
MailboxWorker uploadWorker;
synchronized (lock) {
uploadWorker = uploadWorkers.remove(contactId);
}
if (uploadWorker != null) uploadWorker.destroy();
}
@Override
public void assignContactForDownload(ContactId contactId,
MailboxProperties properties, MailboxFolderId folderId) {
LOG.info("Contact assigned for download");
if (!properties.isOwner()) throw new IllegalArgumentException();
// Create a download worker if we don't already have one. The worker
// will use the API to discover which folders have files to download,
// so it doesn't need to track the set of assigned contacts
MailboxWorker toStart = null;
synchronized (lock) {
if (!assignedForDownload.add(contactId)) {
throw new IllegalStateException();
}
if (downloadWorker == null) {
toStart = workerFactory.createDownloadWorkerForOwnMailbox(
connectivityChecker, reachabilityMonitor, properties);
downloadWorker = toStart;
}
}
if (toStart != null) toStart.start();
}
@Override
public void deassignContactForDownload(ContactId contactId) {
LOG.info("Contact deassigned for download");
// If there are no more contacts assigned for download, destroy the
// download worker
MailboxWorker toDestroy = null;
synchronized (lock) {
if (!assignedForDownload.remove(contactId)) {
throw new IllegalStateException();
}
if (assignedForDownload.isEmpty()) {
toDestroy = downloadWorker;
downloadWorker = null;
}
}
if (toDestroy != null) toDestroy.destroy();
}
}

View File

@@ -303,7 +303,7 @@ class OwnMailboxContactListWorker
mailboxApi.deleteContact(mailboxProperties, c);
} catch (TolerableFailureException e) {
// Catch this so we can continue to the next update
logException(LOG, INFO, e);
LOG.warning("Contact does not exist");
}
updateContactList();
}

View File

@@ -0,0 +1,148 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.Collections.shuffle;
import static java.util.logging.Level.INFO;
@ThreadSafe
@NotNullByDefault
class OwnMailboxDownloadWorker extends MailboxDownloadWorker {
/**
* The maximum number of files that will be downloaded before checking
* again for folders with available files. This ensures that if a file
* arrives during a download cycle, its folder will be checked within a
* reasonable amount of time even if some other folder has a very large
* number of files.
* <p>
* Package access for testing.
*/
static final int MAX_ROUND_ROBIN_FILES = 1000;
OwnMailboxDownloadWorker(
ConnectivityChecker connectivityChecker,
TorReachabilityMonitor torReachabilityMonitor,
MailboxApiCaller mailboxApiCaller,
MailboxApi mailboxApi,
MailboxFileManager mailboxFileManager,
MailboxProperties mailboxProperties) {
super(connectivityChecker, torReachabilityMonitor, mailboxApiCaller,
mailboxApi, mailboxFileManager, mailboxProperties);
if (!mailboxProperties.isOwner()) throw new IllegalArgumentException();
}
@Override
protected ApiCall createApiCallForDownloadCycle() {
return new SimpleApiCall(this::apiCallListFolders);
}
private void apiCallListFolders() throws IOException, ApiException {
synchronized (lock) {
if (state == State.DESTROYED) return;
}
LOG.info("Listing folders with available files");
List<MailboxFolderId> folders =
mailboxApi.getFolders(mailboxProperties);
if (folders.isEmpty()) onDownloadCycleFinished();
else listNextFolder(new LinkedList<>(folders), new HashMap<>());
}
/**
* Removes the next folder from `queue` and starts a task to list the
* files in the folder and add them to `available`.
*/
private void listNextFolder(Queue<MailboxFolderId> queue,
Map<MailboxFolderId, Queue<MailboxFile>> available) {
synchronized (lock) {
if (state == State.DESTROYED) return;
MailboxFolderId folder = queue.remove();
apiCall = mailboxApiCaller.retryWithBackoff(new SimpleApiCall(() ->
apiCallListFolder(folder, queue, available)));
}
}
private void apiCallListFolder(MailboxFolderId folder,
Queue<MailboxFolderId> queue,
Map<MailboxFolderId, Queue<MailboxFile>> available)
throws IOException, ApiException {
synchronized (lock) {
if (state == State.DESTROYED) return;
}
LOG.info("Listing folder");
try {
List<MailboxFile> files =
mailboxApi.getFiles(mailboxProperties, folder);
if (!files.isEmpty()) {
available.put(folder, new LinkedList<>(files));
}
} catch (TolerableFailureException e) {
LOG.warning("Folder does not exist");
}
if (queue.isEmpty()) {
LOG.info("Finished listing folders");
if (available.isEmpty()) onDownloadCycleFinished();
else createDownloadQueue(available);
} else {
listNextFolder(queue, available);
}
}
/**
* Visits the given folders in round-robin order to create a queue of up to
* {@link #MAX_ROUND_ROBIN_FILES} to download.
*/
private void createDownloadQueue(
Map<MailboxFolderId, Queue<MailboxFile>> available) {
synchronized (lock) {
if (state == State.DESTROYED) return;
}
if (LOG.isLoggable(INFO)) {
LOG.info(available.size() + " folders have available files");
}
Queue<FolderFile> queue = createRoundRobinQueue(available);
if (LOG.isLoggable(INFO)) {
LOG.info("Downloading " + queue.size() + " files");
}
downloadNextFile(queue);
}
// Package access for testing
Queue<FolderFile> createRoundRobinQueue(
Map<MailboxFolderId, Queue<MailboxFile>> available) {
List<MailboxFolderId> roundRobin = new ArrayList<>(available.keySet());
// Shuffle the folders so we don't always favour the same folders
shuffle(roundRobin);
Queue<FolderFile> queue = new LinkedList<>();
while (queue.size() < MAX_ROUND_ROBIN_FILES && !available.isEmpty()) {
Iterator<MailboxFolderId> it = roundRobin.iterator();
while (queue.size() < MAX_ROUND_ROBIN_FILES && it.hasNext()) {
MailboxFolderId folder = it.next();
Queue<MailboxFile> files = available.get(folder);
MailboxFile file = files.remove();
queue.add(new FolderFile(folder, file.name));
if (files.isEmpty()) {
available.remove(folder);
it.remove();
}
}
}
return queue;
}
}

View File

@@ -800,6 +800,13 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
}
}
@Override
public void controlConnectionClosed() {
if (state.isTorRunning()) {
throw new RuntimeException("Control connection closed");
}
}
private String removeSeverity(String msg) {
return msg.replaceFirst("[^ ]+ ", "");
}

View File

@@ -75,7 +75,7 @@ public class ContactMailboxClientTest extends BrambleMockTestCase {
}
@Test
public void assignContactForDownloadAndDestroyClient() {
public void testAssignContactForDownloadAndDestroyClient() {
client.start();
// When the contact is assigned, the worker should be created and
@@ -89,7 +89,7 @@ public class ContactMailboxClientTest extends BrambleMockTestCase {
}
@Test
public void assignAndDeassignContactForDownload() {
public void testAssignAndDeassignContactForDownload() {
client.start();
// When the contact is assigned, the worker should be created and

View File

@@ -1,88 +1,68 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Cancellable;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.CaptureArgumentAction;
import org.jmock.Expectations;
import org.jmock.lib.action.DoAllAction;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.junit.Assert.assertFalse;
public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
public class ContactMailboxDownloadWorkerTest
extends MailboxDownloadWorkerTest<ContactMailboxDownloadWorker> {
private final ConnectivityChecker connectivityChecker =
context.mock(ConnectivityChecker.class);
private final TorReachabilityMonitor torReachabilityMonitor =
context.mock(TorReachabilityMonitor.class);
private final MailboxApiCaller mailboxApiCaller =
context.mock(MailboxApiCaller.class);
private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
private final MailboxFileManager mailboxFileManager =
context.mock(MailboxFileManager.class);
private final Cancellable apiCall = context.mock(Cancellable.class);
private final MailboxProperties mailboxProperties =
getMailboxProperties(false, CLIENT_SUPPORTS);
private final long now = System.currentTimeMillis();
private final MailboxFile file1 =
new MailboxFile(new MailboxFileId(getRandomId()), now - 1);
private final MailboxFile file2 =
new MailboxFile(new MailboxFileId(getRandomId()), now);
private final List<MailboxFile> files = asList(file1, file2);
private File testDir, tempFile;
private ContactMailboxDownloadWorker worker;
@Before
public void setUp() {
testDir = getTestDirectory();
tempFile = new File(testDir, "temp");
public ContactMailboxDownloadWorkerTest() {
mailboxProperties = getMailboxProperties(false, CLIENT_SUPPORTS);
worker = new ContactMailboxDownloadWorker(connectivityChecker,
torReachabilityMonitor, mailboxApiCaller, mailboxApi,
mailboxFileManager, mailboxProperties);
}
@After
public void tearDown() {
deleteTestDirectory(testDir);
}
@Test
public void testChecksConnectivityWhenStartedAndRemovesObserverWhenDestroyed() {
// When the worker is started it should start a connectivity check
context.checking(new Expectations() {{
oneOf(connectivityChecker).checkConnectivity(
with(mailboxProperties), with(worker));
}});
expectStartConnectivityCheck();
worker.start();
// When the worker is destroyed it should remove the connectivity
// and reachability observers
context.checking(new Expectations() {{
oneOf(connectivityChecker).removeObserver(worker);
oneOf(torReachabilityMonitor).removeObserver(worker);
}});
expectRemoveObservers();
worker.destroy();
}
@Test
public void testChecksForFilesWhenConnectivityCheckSucceeds()
throws Exception {
// When the worker is started it should start a connectivity check
expectStartConnectivityCheck();
worker.start();
// When the connectivity check succeeds, a list-inbox task should be
// started for the first download cycle
AtomicReference<ApiCall> listTask = new AtomicReference<>();
expectStartTask(listTask);
worker.onConnectivityCheckSucceeded();
// When the list-inbox tasks runs and finds no files to download,
// it should add a Tor reachability observer
expectCheckForFiles(mailboxProperties.getInboxId(), emptyList());
expectAddReachabilityObserver();
assertFalse(listTask.get().callApi());
// When the reachability observer is called, a list-inbox task should
// be started for the second download cycle
expectStartTask(listTask);
worker.onTorReachable();
// When the list-inbox tasks runs and finds no files to download,
// it should finish the second download cycle
expectCheckForFiles(mailboxProperties.getInboxId(), emptyList());
assertFalse(listTask.get().callApi());
// When the worker is destroyed it should remove the connectivity
// and reachability observers
expectRemoveObservers();
worker.destroy();
}
@@ -90,150 +70,67 @@ public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
public void testDownloadsFilesWhenConnectivityCheckSucceeds()
throws Exception {
// When the worker is started it should start a connectivity check
context.checking(new Expectations() {{
oneOf(connectivityChecker).checkConnectivity(
with(mailboxProperties), with(worker));
}});
expectStartConnectivityCheck();
worker.start();
// When the connectivity check succeeds, a list-inbox task should be
// started for the first download cycle
AtomicReference<ApiCall> listTask = new AtomicReference<>();
context.checking(new Expectations() {{
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(listTask, ApiCall.class, 0),
returnValue(apiCall)
));
}});
expectStartTask(listTask);
worker.onConnectivityCheckSucceeded();
// When the list-inbox tasks runs and finds some files to download,
// it should start a download task for the first file
AtomicReference<ApiCall> downloadTask = new AtomicReference<>();
context.checking(new Expectations() {{
oneOf(mailboxApi).getFiles(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()));
will(returnValue(files));
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(downloadTask, ApiCall.class, 0),
returnValue(apiCall)
));
}});
expectCheckForFiles(mailboxProperties.getInboxId(), files);
expectStartTask(downloadTask);
assertFalse(listTask.get().callApi());
// When the first download task runs it should download the file to the
// location provided by the file manager and start a delete task
AtomicReference<ApiCall> deleteTask = new AtomicReference<>();
context.checking(new Expectations() {{
oneOf(mailboxFileManager).createTempFileForDownload();
will(returnValue(tempFile));
oneOf(mailboxApi).getFile(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()),
file1.name, tempFile);
oneOf(mailboxFileManager).handleDownloadedFile(tempFile);
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(deleteTask, ApiCall.class, 0),
returnValue(apiCall)
));
}});
expectDownloadFile(mailboxProperties.getInboxId(), file1);
expectStartTask(deleteTask);
assertFalse(downloadTask.get().callApi());
// When the first delete task runs it should delete the file, ignore
// the tolerable failure, and start a download task for the next file
context.checking(new Expectations() {{
oneOf(mailboxApi).deleteFile(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()), file1.name);
will(throwException(new TolerableFailureException()));
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(downloadTask, ApiCall.class, 0),
returnValue(apiCall)
));
}});
expectDeleteFile(mailboxProperties.getInboxId(), file1, true);
expectStartTask(downloadTask);
assertFalse(deleteTask.get().callApi());
// When the second download task runs it should download the file to
// the location provided by the file manager and start a delete task
context.checking(new Expectations() {{
oneOf(mailboxFileManager).createTempFileForDownload();
will(returnValue(tempFile));
oneOf(mailboxApi).getFile(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()),
file2.name, tempFile);
oneOf(mailboxFileManager).handleDownloadedFile(tempFile);
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(deleteTask, ApiCall.class, 0),
returnValue(apiCall)
));
}});
expectDownloadFile(mailboxProperties.getInboxId(), file2);
expectStartTask(deleteTask);
assertFalse(downloadTask.get().callApi());
// When the second delete task runs it should delete the file and
// start a list-inbox task to check for files that may have arrived
// since the first download cycle started
context.checking(new Expectations() {{
oneOf(mailboxApi).deleteFile(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()), file2.name);
will(throwException(new TolerableFailureException()));
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(listTask, ApiCall.class, 0),
returnValue(apiCall)
));
}});
expectDeleteFile(mailboxProperties.getInboxId(), file2, false);
expectStartTask(listTask);
assertFalse(deleteTask.get().callApi());
// When the list-inbox tasks runs and finds no more files to download,
// it should add a Tor reachability observer
context.checking(new Expectations() {{
oneOf(mailboxApi).getFiles(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()));
will(returnValue(emptyList()));
oneOf(torReachabilityMonitor).addOneShotObserver(worker);
}});
expectCheckForFiles(mailboxProperties.getInboxId(), emptyList());
expectAddReachabilityObserver();
assertFalse(listTask.get().callApi());
// When the reachability observer is called, a list-inbox task should
// be started for the second download cycle
context.checking(new Expectations() {{
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(listTask, ApiCall.class, 0),
returnValue(apiCall)
));
}});
expectStartTask(listTask);
worker.onTorReachable();
// When the list-inbox tasks runs and finds no more files to download,
// it should finish the second download cycle
context.checking(new Expectations() {{
oneOf(mailboxApi).getFiles(mailboxProperties,
requireNonNull(mailboxProperties.getInboxId()));
will(returnValue(emptyList()));
}});
expectCheckForFiles(mailboxProperties.getInboxId(), emptyList());
assertFalse(listTask.get().callApi());
// When the worker is destroyed it should remove the connectivity
// and reachability observers
context.checking(new Expectations() {{
oneOf(connectivityChecker).removeObserver(worker);
oneOf(torReachabilityMonitor).removeObserver(worker);
}});
expectRemoveObservers();
worker.destroy();
}
}

View File

@@ -1,8 +1,6 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
@@ -13,7 +11,6 @@ import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
@@ -25,14 +22,11 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nonnull;
import javax.net.SocketFactory;
import okhttp3.OkHttpClient;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.SETUP_TOKEN;
import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.URL_BASE;
import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.createMailboxApi;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
@@ -40,58 +34,31 @@ import static org.briarproject.bramble.test.TestUtils.readBytes;
import static org.briarproject.bramble.test.TestUtils.writeBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
public class MailboxIntegrationTest extends BrambleTestCase {
public class MailboxApiIntegrationTest extends BrambleTestCase {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private static final String URL_BASE = "http://127.0.0.1:8000";
private static final MailboxAuthToken SETUP_TOKEN;
private static final MailboxApi api = createMailboxApi();
static {
try {
SETUP_TOKEN = MailboxAuthToken.fromString(
"54686973206973206120736574757020746f6b656e20666f722042726961722e");
} catch (InvalidMailboxIdException e) {
throw new IllegalStateException();
}
}
private static final OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(SocketFactory.getDefault())
.connectTimeout(60_000, MILLISECONDS)
.build();
private static final WeakSingletonProvider<OkHttpClient>
httpClientProvider =
new WeakSingletonProvider<OkHttpClient>() {
@Override
@Nonnull
public OkHttpClient createInstance() {
return client;
}
};
// We aren't using a real onion address, so use the given address verbatim
private static final UrlConverter urlConverter = onion -> onion;
private static final MailboxApiImpl api =
new MailboxApiImpl(httpClientProvider, urlConverter);
// needs to be static to keep values across different tests
private static MailboxProperties ownerProperties;
/**
* Called before each test to make sure the mailbox is setup once
* before starting with individual tests.
* {@link BeforeClass} needs to be static, so we can't use the API class.
*/
@Before
public void ensureSetup() throws IOException, ApiException {
@BeforeClass
public static void setUp() throws IOException, ApiException {
// Skip this test unless it's explicitly enabled in the environment
assumeTrue(isOptionalTestEnabled(MailboxIntegrationTest.class));
assumeTrue(isOptionalTestEnabled(MailboxApiIntegrationTest.class));
if (ownerProperties != null) return;
assertNull(ownerProperties);
MailboxProperties setupProperties = new MailboxProperties(
URL_BASE, SETUP_TOKEN, new ArrayList<>());
ownerProperties = api.setup(setupProperties);
@@ -100,7 +67,7 @@ public class MailboxIntegrationTest extends BrambleTestCase {
@AfterClass
// we can't test wiping as a regular test as it stops the mailbox
public static void wipe() throws IOException, ApiException {
if (!isOptionalTestEnabled(MailboxIntegrationTest.class)) return;
if (!isOptionalTestEnabled(MailboxApiIntegrationTest.class)) return;
api.wipeMailbox(ownerProperties);
@@ -186,7 +153,7 @@ public class MailboxIntegrationTest extends BrambleTestCase {
MailboxFileId fileName1 = files1.get(0).name;
// owner can't check files
assertThrows(ApiException.class, () ->
assertThrows(TolerableFailureException.class, () ->
api.getFiles(ownerProperties, contact.inboxId));
// contact downloads file
@@ -197,12 +164,13 @@ public class MailboxIntegrationTest extends BrambleTestCase {
// owner can't download file, even if knowing name
File file1forbidden = folder.newFile();
assertThrows(ApiException.class, () -> api.getFile(ownerProperties,
contact.inboxId, fileName1, file1forbidden));
assertThrows(TolerableFailureException.class, () ->
api.getFile(ownerProperties, contact.inboxId, fileName1,
file1forbidden));
assertEquals(0, file1forbidden.length());
// owner can't delete file
assertThrows(ApiException.class, () ->
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(ownerProperties, contact.inboxId, fileName1));
// contact deletes file
@@ -232,7 +200,7 @@ public class MailboxIntegrationTest extends BrambleTestCase {
MailboxFileId file3name = files2.get(1).name;
// contact can't list files in contact's outbox
assertThrows(ApiException.class, () ->
assertThrows(TolerableFailureException.class, () ->
api.getFiles(contactProperties, contact.outboxId));
// owner downloads both files from contact's outbox
@@ -252,17 +220,19 @@ public class MailboxIntegrationTest extends BrambleTestCase {
// contact can't download files again, even if knowing name
File file2forbidden = folder.newFile();
File file3forbidden = folder.newFile();
assertThrows(ApiException.class, () -> api.getFile(contactProperties,
contact.outboxId, file2name, file2forbidden));
assertThrows(ApiException.class, () -> api.getFile(contactProperties,
contact.outboxId, file3name, file3forbidden));
assertThrows(TolerableFailureException.class, () ->
api.getFile(contactProperties, contact.outboxId, file2name,
file2forbidden));
assertThrows(TolerableFailureException.class, () ->
api.getFile(contactProperties, contact.outboxId, file3name,
file3forbidden));
assertEquals(0, file1forbidden.length());
assertEquals(0, file2forbidden.length());
// contact can't delete files in outbox
assertThrows(ApiException.class, () ->
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(contactProperties, contact.outboxId, file2name));
assertThrows(ApiException.class, () ->
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(contactProperties, contact.outboxId, file3name));
// owner deletes files

View File

@@ -630,6 +630,7 @@ public class MailboxApiTest extends BrambleTestCase {
server.enqueue(new MockResponse().setBody(invalidResponse3));
server.enqueue(new MockResponse().setBody(invalidResponse4));
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse().setResponseCode(404));
server.enqueue(new MockResponse().setResponseCode(500));
server.start();
String baseUrl = getBaseUrl(server);
@@ -706,13 +707,21 @@ public class MailboxApiTest extends BrambleTestCase {
assertEquals("GET", request8.getMethod());
assertToken(request8, token);
// 500 internal server error
assertThrows(ApiException.class,
() -> api.getFiles(properties, contactInboxId));
// 404 not found
assertThrows(TolerableFailureException.class, () ->
api.getFiles(properties, contactInboxId));
RecordedRequest request9 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request9.getPath());
assertEquals("GET", request9.getMethod());
assertToken(request9, token);
// 500 internal server error
assertThrows(ApiException.class,
() -> api.getFiles(properties, contactInboxId));
RecordedRequest request10 = server.takeRequest();
assertEquals("/files/" + contactInboxId, request10.getPath());
assertEquals("GET", request10.getMethod());
assertToken(request10, token);
}
@Test

View File

@@ -0,0 +1,130 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Cancellable;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.CaptureArgumentAction;
import org.jmock.Expectations;
import org.jmock.lib.action.DoAllAction;
import org.junit.After;
import org.junit.Before;
import java.io.File;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Arrays.asList;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
abstract class MailboxDownloadWorkerTest<W extends MailboxDownloadWorker>
extends BrambleMockTestCase {
final ConnectivityChecker connectivityChecker =
context.mock(ConnectivityChecker.class);
final TorReachabilityMonitor torReachabilityMonitor =
context.mock(TorReachabilityMonitor.class);
final MailboxApiCaller mailboxApiCaller =
context.mock(MailboxApiCaller.class);
final MailboxApi mailboxApi = context.mock(MailboxApi.class);
final MailboxFileManager mailboxFileManager =
context.mock(MailboxFileManager.class);
private final Cancellable apiCall = context.mock(Cancellable.class);
private final long now = System.currentTimeMillis();
final MailboxFile file1 =
new MailboxFile(new MailboxFileId(getRandomId()), now - 1);
final MailboxFile file2 =
new MailboxFile(new MailboxFileId(getRandomId()), now);
final List<MailboxFile> files = asList(file1, file2);
private File testDir, tempFile;
MailboxProperties mailboxProperties;
W worker;
@Before
public void setUp() {
testDir = getTestDirectory();
tempFile = new File(testDir, "temp");
}
@After
public void tearDown() {
deleteTestDirectory(testDir);
}
void expectStartConnectivityCheck() {
context.checking(new Expectations() {{
oneOf(connectivityChecker).checkConnectivity(
with(mailboxProperties), with(worker));
}});
}
void expectStartTask(AtomicReference<ApiCall> task) {
context.checking(new Expectations() {{
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(task, ApiCall.class, 0),
returnValue(apiCall)
));
}});
}
void expectCheckForFoldersWithAvailableFiles(
List<MailboxFolderId> folderIds) throws Exception {
context.checking(new Expectations() {{
oneOf(mailboxApi).getFolders(mailboxProperties);
will(returnValue(folderIds));
}});
}
void expectCheckForFiles(MailboxFolderId folderId,
List<MailboxFile> files) throws Exception {
context.checking(new Expectations() {{
oneOf(mailboxApi).getFiles(mailboxProperties, folderId);
will(returnValue(files));
}});
}
void expectDownloadFile(MailboxFolderId folderId,
MailboxFile file)
throws Exception {
context.checking(new Expectations() {{
oneOf(mailboxFileManager).createTempFileForDownload();
will(returnValue(tempFile));
oneOf(mailboxApi).getFile(mailboxProperties, folderId, file.name,
tempFile);
oneOf(mailboxFileManager).handleDownloadedFile(tempFile);
}});
}
void expectDeleteFile(MailboxFolderId folderId, MailboxFile file,
boolean tolerableFailure) throws Exception {
context.checking(new Expectations() {{
oneOf(mailboxApi).deleteFile(mailboxProperties, folderId,
file.name);
if (tolerableFailure) {
will(throwException(new TolerableFailureException()));
}
}});
}
void expectAddReachabilityObserver() {
context.checking(new Expectations() {{
oneOf(torReachabilityMonitor).addOneShotObserver(worker);
}});
}
void expectRemoveObservers() {
context.checking(new Expectations() {{
oneOf(connectivityChecker).removeObserver(worker);
oneOf(torReachabilityMonitor).removeObserver(worker);
}});
}
}

View File

@@ -0,0 +1,18 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
import org.briarproject.bramble.test.BrambleIntegrationTestComponent;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {
BrambleCoreIntegrationTestModule.class,
BrambleCoreModule.class
})
interface MailboxIntegrationTestComponent extends
BrambleIntegrationTestComponent {
}

View File

@@ -0,0 +1,80 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.io.IoModule;
import javax.annotation.Nonnull;
import javax.inject.Singleton;
import javax.net.SocketFactory;
import dagger.Module;
import dagger.Provides;
import okhttp3.OkHttpClient;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
class MailboxIntegrationTestUtils {
static final String URL_BASE = "http://127.0.0.1:8000";
static final MailboxAuthToken SETUP_TOKEN;
static {
try {
SETUP_TOKEN = MailboxAuthToken.fromString(
"54686973206973206120736574757020746f6b656e20666f722042726961722e");
} catch (InvalidMailboxIdException e) {
throw new IllegalStateException();
}
}
static WeakSingletonProvider<OkHttpClient> createHttpClientProvider() {
OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(SocketFactory.getDefault())
.connectTimeout(60_000, MILLISECONDS)
.build();
return new WeakSingletonProvider<OkHttpClient>() {
@Override
@Nonnull
public OkHttpClient createInstance() {
return client;
}
};
}
static MailboxApi createMailboxApi() {
return new MailboxApiImpl(createHttpClientProvider(), onion -> onion);
}
static MailboxIntegrationTestComponent createTestComponent() {
MailboxIntegrationTestComponent component =
DaggerMailboxIntegrationTestComponent
.builder()
.ioModule(new TestIoModule())
.mailboxModule(new TestMailboxModule())
.build();
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(component);
return component;
}
@Module
static class TestIoModule extends IoModule {
@Provides
@Singleton
WeakSingletonProvider<OkHttpClient> provideOkHttpClientProvider() {
return createHttpClientProvider();
}
}
@Module
static class TestMailboxModule extends MailboxModule {
@Provides
UrlConverter provideUrlConverter() {
return onion -> onion;
}
}
}

View File

@@ -0,0 +1,208 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.junit.Test;
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
import static org.briarproject.bramble.test.TestUtils.getContactId;
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
public class OwnMailboxClientTest extends BrambleMockTestCase {
private final MailboxWorkerFactory workerFactory =
context.mock(MailboxWorkerFactory.class);
private final ConnectivityChecker connectivityChecker =
context.mock(ConnectivityChecker.class);
private final TorReachabilityMonitor reachabilityMonitor =
context.mock(TorReachabilityMonitor.class);
private final MailboxWorker contactListWorker =
context.mock(MailboxWorker.class, "contactListWorker");
private final MailboxWorker uploadWorker1 =
context.mock(MailboxWorker.class, "uploadWorker1");
private final MailboxWorker uploadWorker2 =
context.mock(MailboxWorker.class, "uploadWorker2");
private final MailboxWorker downloadWorker =
context.mock(MailboxWorker.class, "downloadWorker");
private final MailboxProperties properties =
getMailboxProperties(true, CLIENT_SUPPORTS);
private final MailboxFolderId folderId = new MailboxFolderId(getRandomId());
private final ContactId contactId1 = getContactId();
private final ContactId contactId2 = getContactId();
private final OwnMailboxClient client;
public OwnMailboxClientTest() {
expectCreateContactListWorker();
client = new OwnMailboxClient(workerFactory, connectivityChecker,
reachabilityMonitor, properties);
context.assertIsSatisfied();
}
@Test
public void testStartAndDestroyWithNoContactsAssigned() {
expectStartWorker(contactListWorker);
client.start();
expectDestroyWorker(contactListWorker);
client.destroy();
}
@Test
public void testAssignContactForUploadAndDestroyClient() {
expectStartWorker(contactListWorker);
client.start();
// When the contact is assigned, the worker should be created and
// started
expectCreateUploadWorker(contactId1, uploadWorker1);
expectStartWorker(uploadWorker1);
client.assignContactForUpload(contactId1, properties, folderId);
// When the client is destroyed, the worker should be destroyed
expectDestroyWorker(uploadWorker1);
expectDestroyWorker(contactListWorker);
client.destroy();
}
@Test
public void testAssignAndDeassignContactForUpload() {
expectStartWorker(contactListWorker);
client.start();
// When the contact is assigned, the worker should be created and
// started
expectCreateUploadWorker(contactId1, uploadWorker1);
expectStartWorker(uploadWorker1);
client.assignContactForUpload(contactId1, properties, folderId);
// When the contact is deassigned, the worker should be destroyed
expectDestroyWorker(uploadWorker1);
client.deassignContactForUpload(contactId1);
context.assertIsSatisfied();
expectDestroyWorker(contactListWorker);
client.destroy();
}
@Test
public void testAssignAndDeassignTwoContactsForUpload() {
expectStartWorker(contactListWorker);
client.start();
// When the first contact is assigned, the first worker should be
// created and started
expectCreateUploadWorker(contactId1, uploadWorker1);
expectStartWorker(uploadWorker1);
client.assignContactForUpload(contactId1, properties, folderId);
// When the second contact is assigned, the second worker should be
// created and started
expectCreateUploadWorker(contactId2, uploadWorker2);
expectStartWorker(uploadWorker2);
client.assignContactForUpload(contactId2, properties, folderId);
// When the second contact is deassigned, the second worker should be
// destroyed
expectDestroyWorker(uploadWorker2);
client.deassignContactForUpload(contactId2);
context.assertIsSatisfied();
// When the first contact is deassigned, the first worker should be
// destroyed
expectDestroyWorker(uploadWorker1);
client.deassignContactForUpload(contactId1);
context.assertIsSatisfied();
expectDestroyWorker(contactListWorker);
client.destroy();
}
@Test
public void testAssignContactForDownloadAndDestroyClient() {
expectStartWorker(contactListWorker);
client.start();
// When the contact is assigned, the worker should be created and
// started
expectCreateDownloadWorker();
expectStartWorker(downloadWorker);
client.assignContactForDownload(contactId1, properties, folderId);
// When the client is destroyed, the worker should be destroyed
expectDestroyWorker(downloadWorker);
expectDestroyWorker(contactListWorker);
client.destroy();
}
@Test
public void testAssignAndDeassignTwoContactsForDownload() {
expectStartWorker(contactListWorker);
client.start();
// When the first contact is assigned, the worker should be created and
// started
expectCreateDownloadWorker();
expectStartWorker(downloadWorker);
client.assignContactForDownload(contactId1, properties, folderId);
// When the second contact is assigned, nothing should happen to the
// worker
client.assignContactForDownload(contactId2, properties, folderId);
// When the first contact is deassigned, nothing should happen to the
// worker
client.deassignContactForDownload(contactId1);
// When the second contact is deassigned, the worker should be
// destroyed
expectDestroyWorker(downloadWorker);
client.deassignContactForDownload(contactId2);
context.assertIsSatisfied();
expectDestroyWorker(contactListWorker);
client.destroy();
}
private void expectCreateContactListWorker() {
context.checking(new Expectations() {{
oneOf(workerFactory).createContactListWorkerForOwnMailbox(
connectivityChecker, properties);
will(returnValue(contactListWorker));
}});
}
private void expectCreateUploadWorker(ContactId contactId,
MailboxWorker worker) {
context.checking(new Expectations() {{
oneOf(workerFactory).createUploadWorker(connectivityChecker,
properties, folderId, contactId);
will(returnValue(worker));
}});
}
private void expectCreateDownloadWorker() {
context.checking(new Expectations() {{
oneOf(workerFactory).createDownloadWorkerForOwnMailbox(
connectivityChecker, reachabilityMonitor, properties);
will(returnValue(downloadWorker));
}});
}
private void expectStartWorker(MailboxWorker worker) {
context.checking(new Expectations() {{
oneOf(worker).start();
}});
}
private void expectDestroyWorker(MailboxWorker worker) {
context.checking(new Expectations() {{
oneOf(worker).destroy();
}});
}
}

View File

@@ -0,0 +1,25 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.Before;
import org.junit.Test;
import static org.briarproject.bramble.mailbox.MailboxIntegrationTestUtils.createTestComponent;
import static org.junit.Assert.assertTrue;
public class OwnMailboxContactListWorkerIntegrationTest
extends BrambleTestCase {
private MailboxIntegrationTestComponent component;
@Before
public void setUp() {
component = createTestComponent();
}
// Just test that we can build the component. TODO: Write actual tests
@Test
public void testBuild() {
assertTrue(true);
}
}

View File

@@ -0,0 +1,236 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
import static org.briarproject.bramble.mailbox.MailboxDownloadWorker.FolderFile;
import static org.briarproject.bramble.mailbox.OwnMailboxDownloadWorker.MAX_ROUND_ROBIN_FILES;
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class OwnMailboxDownloadWorkerTest
extends MailboxDownloadWorkerTest<OwnMailboxDownloadWorker> {
private final MailboxFolderId folderId1 =
new MailboxFolderId(getRandomId());
private final MailboxFolderId folderId2 =
new MailboxFolderId(getRandomId());
private final List<MailboxFolderId> folderIds =
asList(folderId1, folderId2);
public OwnMailboxDownloadWorkerTest() {
mailboxProperties = getMailboxProperties(true, CLIENT_SUPPORTS);
worker = new OwnMailboxDownloadWorker(connectivityChecker,
torReachabilityMonitor, mailboxApiCaller, mailboxApi,
mailboxFileManager, mailboxProperties);
}
@Override
public void setUp() {
super.setUp();
}
@Test
public void testChecksConnectivityWhenStartedAndRemovesObserverWhenDestroyed() {
// When the worker is started it should start a connectivity check
expectStartConnectivityCheck();
worker.start();
// When the worker is destroyed it should remove the connectivity
// and reachability observers
expectRemoveObservers();
worker.destroy();
}
@Test
public void testChecksForFilesWhenConnectivityCheckSucceeds()
throws Exception {
// When the worker is started it should start a connectivity check
expectStartConnectivityCheck();
worker.start();
// When the connectivity check succeeds, a list-folders task should be
// started for the first download cycle
AtomicReference<ApiCall> listFoldersTask = new AtomicReference<>();
expectStartTask(listFoldersTask);
worker.onConnectivityCheckSucceeded();
// When the list-folders tasks runs and finds no folders with files
// to download, it should add a Tor reachability observer
expectCheckForFoldersWithAvailableFiles(emptyList());
expectAddReachabilityObserver();
assertFalse(listFoldersTask.get().callApi());
// When the reachability observer is called, a list-folders task should
// be started for the second download cycle
expectStartTask(listFoldersTask);
worker.onTorReachable();
// When the list-folders tasks runs and finds no folders with files
// to download, it should finish the second download cycle
expectCheckForFoldersWithAvailableFiles(emptyList());
assertFalse(listFoldersTask.get().callApi());
// When the worker is destroyed it should remove the connectivity
// and reachability observers
expectRemoveObservers();
worker.destroy();
}
@Test
public void testDownloadsFilesWhenConnectivityCheckSucceeds()
throws Exception {
// When the worker is started it should start a connectivity check
expectStartConnectivityCheck();
worker.start();
// When the connectivity check succeeds, a list-folders task should be
// started for the first download cycle
AtomicReference<ApiCall> listFoldersTask = new AtomicReference<>();
expectStartTask(listFoldersTask);
worker.onConnectivityCheckSucceeded();
// When the list-folders tasks runs and finds some folders with files
// to download, it should start a list-files task for the first folder
AtomicReference<ApiCall> listFilesTask = new AtomicReference<>();
expectCheckForFoldersWithAvailableFiles(folderIds);
expectStartTask(listFilesTask);
assertFalse(listFoldersTask.get().callApi());
// When the first list-files task runs and finds no files to download,
// it should start a second list-files task for the next folder
expectCheckForFiles(folderId1, emptyList());
expectStartTask(listFilesTask);
assertFalse(listFilesTask.get().callApi());
// When the second list-files task runs and finds some files to
// download, it should create the round-robin queue and start a
// download task for the first file
AtomicReference<ApiCall> downloadTask = new AtomicReference<>();
expectCheckForFiles(folderId2, files);
expectStartTask(downloadTask);
assertFalse(listFilesTask.get().callApi());
// When the first download task runs it should download the file to the
// location provided by the file manager and start a delete task
AtomicReference<ApiCall> deleteTask = new AtomicReference<>();
expectDownloadFile(folderId2, file1);
expectStartTask(deleteTask);
assertFalse(downloadTask.get().callApi());
// When the first delete task runs it should delete the file, ignore
// the tolerable failure, and start a download task for the next file
expectDeleteFile(folderId2, file1, true); // Delete fails tolerably
expectStartTask(downloadTask);
assertFalse(deleteTask.get().callApi());
// When the second download task runs it should download the file to
// the location provided by the file manager and start a delete task
expectDownloadFile(folderId2, file2);
expectStartTask(deleteTask);
assertFalse(downloadTask.get().callApi());
// When the second delete task runs it should delete the file and
// start a list-inbox task to check for files that may have arrived
// since the first download cycle started
expectDeleteFile(folderId2, file2, false); // Delete succeeds
expectStartTask(listFoldersTask);
assertFalse(deleteTask.get().callApi());
// When the list-inbox tasks runs and finds no more files to download,
// it should add a Tor reachability observer
expectCheckForFoldersWithAvailableFiles(emptyList());
expectAddReachabilityObserver();
assertFalse(listFoldersTask.get().callApi());
// When the reachability observer is called, a list-inbox task should
// be started for the second download cycle
expectStartTask(listFoldersTask);
worker.onTorReachable();
// When the list-inbox tasks runs and finds no more files to download,
// it should finish the second download cycle
expectCheckForFoldersWithAvailableFiles(emptyList());
assertFalse(listFoldersTask.get().callApi());
// When the worker is destroyed it should remove the connectivity
// and reachability observers
expectRemoveObservers();
worker.destroy();
}
@Test
public void testRoundRobinQueueVisitsAllFolders() {
// Ten folders with two files each
Map<MailboxFolderId, Queue<MailboxFile>> available =
createAvailableFiles(10, 2);
Queue<FolderFile> queue = worker.createRoundRobinQueue(available);
// Check that all files were queued
for (MailboxFolderId folderId : available.keySet()) {
assertEquals(2, countFilesWithFolderId(queue, folderId));
}
}
@Test
public void testSizeOfRoundRobinQueueIsLimited() {
// Two folders with MAX_ROUND_ROBIN_FILES each
Map<MailboxFolderId, Queue<MailboxFile>> available =
createAvailableFiles(2, MAX_ROUND_ROBIN_FILES);
Queue<FolderFile> queue = worker.createRoundRobinQueue(available);
// Check that half the files in each folder were queued
for (MailboxFolderId folderId : available.keySet()) {
assertEquals(MAX_ROUND_ROBIN_FILES / 2,
countFilesWithFolderId(queue, folderId));
}
}
private Map<MailboxFolderId, Queue<MailboxFile>> createAvailableFiles(
int numFolders, int numFiles) {
Map<MailboxFolderId, Queue<MailboxFile>> available = new HashMap<>();
List<MailboxFolderId> folderIds = createFolderIds(numFolders);
for (MailboxFolderId folderId : folderIds) {
available.put(folderId, createFiles(numFiles));
}
return available;
}
private List<MailboxFolderId> createFolderIds(int size) {
List<MailboxFolderId> folderIds = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
folderIds.add(new MailboxFolderId(getRandomId()));
}
return folderIds;
}
private Queue<MailboxFile> createFiles(int size) {
Queue<MailboxFile> files = new LinkedList<>();
for (int i = 0; i < size; i++) {
files.add(new MailboxFile(new MailboxFileId(getRandomId()), i));
}
return files;
}
private int countFilesWithFolderId(Queue<FolderFile> queue,
MailboxFolderId folderId) {
int count = 0;
for (FolderFile file : queue) {
if (file.folderId.equals(folderId)) count++;
}
return count;
}
}

View File

@@ -35,7 +35,7 @@ dependencyVerification {
'org.apache-extras.beanshell:bsh:2.0b6:bsh-2.0b6.jar:a17955976070c0573235ee662f2794a78082758b61accffce8d3f8aedcd91047',
'org.bitlet:weupnp:0.1.4:weupnp-0.1.4.jar:88df7e6504929d00bdb832863761385c68ab92af945b04f0770b126270a444fb',
'org.bouncycastle:bcprov-jdk15to18:1.70:bcprov-jdk15to18-1.70.jar:7df4c54f29ce2dd616dc3b198ca4db3dfcc79e3cb397c084a0aff97b85c0bf38',
'org.briarproject:jtorctl:0.4:jtorctl-0.4.jar:4e61f59dc9f3984438a7151c4df8d7c1f83d5fb3eb8c151acfc794a8fef85a36',
'org.briarproject:jtorctl:0.5:jtorctl-0.5.jar:43f8c7d390169772b9a2c82ab806c8414c136a2a8636c555e22754bb7260793b',
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
'org.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
'org.codehaus.mojo.signature:java16:1.1:java16-1.1.signature:53799223a2c98dba2d0add810bed76315460df285c69e4f397ae6098f87dd619',

View File

@@ -29,6 +29,7 @@ android {
versionCode 10410
versionName "1.4.10"
applicationId "org.briarproject.briar.android"
buildConfigField "String", "TorVersion", "\"$tor_version\""
vectorDrawables.useSupportLibrary = true
buildConfigField "String", "GitHash",

View File

@@ -0,0 +1,92 @@
package org.briarproject.briar.android.settings;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.R;
import java.util.logging.Logger;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class AboutFragment extends Fragment {
final static String TAG = AboutFragment.class.getName();
private static final Logger LOG = getLogger(TAG);
private TextView briarVersion;
private TextView torVersion;
private TextView briarWebsite;
private TextView briarSourceCode;
private TextView briarChangelog;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_about, container,
false);
}
@Override
public void onStart() {
super.onStart();
requireActivity().setTitle(R.string.about_title);
briarVersion = requireActivity().findViewById(R.id.BriarVersion);
briarVersion.setText(
getString(R.string.briar_version, BuildConfig.VERSION_NAME));
torVersion = requireActivity().findViewById(R.id.TorVersion);
torVersion.setText(
getString(R.string.tor_version, BuildConfig.TorVersion));
briarWebsite = requireActivity().findViewById(R.id.BriarWebsite);
briarSourceCode = requireActivity().findViewById(R.id.BriarSourceCode);
briarChangelog = requireActivity().findViewById(R.id.BriarChangelog);
briarWebsite.setOnClickListener(View -> {
String url = "https://briarproject.org/";
goToUrl(url);
});
briarSourceCode.setOnClickListener(View -> {
String url = "https://code.briarproject.org/briar/briar";
goToUrl(url);
});
briarChangelog.setOnClickListener(View -> {
String url =
"https://code.briarproject.org/briar/briar/-/wikis/changelog";
goToUrl(url);
});
}
private void goToUrl(String url) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
try {
startActivity(i);
} catch (ActivityNotFoundException e) {
logException(LOG, WARNING, e);
Toast.makeText(requireContext(),
R.string.error_start_activity, LENGTH_LONG).show();
}
}
}

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/textColorPrimary"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
tools:ignore="NewApi">
<path
android:fillColor="#FF000000"
android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z" />
</vector>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<TextView
android:id="@+id/BriarVersion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/briar_version"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/TorVersion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/tor_version"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/BriarVersion" />
<TextView
android:id="@+id/LinksTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/links"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/TorVersion" />
<TextView
android:id="@+id/BriarWebsite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/briar_website"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/LinksTitle" />
<TextView
android:id="@+id/BriarSourceCode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/briar_source_code"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/BriarWebsite" />
<TextView
android:id="@+id/BriarChangelog"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/briar_changelog"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/BriarSourceCode" />
<TextView
android:id="@+id/TranslatorThanks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/translator_thanks"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/BriarChangelog" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -314,6 +314,13 @@
<string name="different_person_button">Different Person</string>
<string name="duplicate_link_dialog_text_3">%1$s and %2$s sent you the same link.\n\nOne of them may be trying to discover who your contacts are.\n\nDon\'t tell them you received the same link from someone else.</string>
<string name="pending_contact_updated_toast">Pending contact updated</string>
<!-- Peer trust levels -->
<string name="peer_trust_level_unverified">Unverified contact</string>
<string name="peer_trust_level_verified">Verified contact</string>
<string name="peer_trust_level_ourselves">Me</string>
<string name="peer_trust_level_stranger">Stranger</string>
<!-- Introductions -->
@@ -684,6 +691,17 @@
<string name="mailbox_error_wizard_info2">Please come back to this screen when you have access to the device.</string>
<string name="mailbox_error_wizard_info3">Please unlink your mailbox using the button below.\n\nAfter unlinking your old Mailbox, you can set up a new Mailbox at any time.</string>
<!-- About -->
<string name="about_title">About</string>
<string name="briar_version">Briar version: %s</string>
<string name="tor_version">Tor version: %s</string>
<string name="links">Links</string>
<string name="briar_website">\u2022 <a href="">Website</a></string>
<string name="briar_source_code">\u2022 <a href="">Source code</a></string>
<string name="briar_changelog">\u2022 <a href="">Changelog</a></string>
<!-- Here translators can add their names or Transifex usernames(eg "Thanks to all the contributors at the Localization Lab, especially Tom, Matthew and Jerry") -->
<string name="translator_thanks">Thanks to all the contributors at the Localization Lab</string>
<!-- Conversation Settings -->
<string name="disappearing_messages_title">Disappearing messages</string>
<string name="disappearing_messages_explanation_long">Turning on this setting will make new

View File

@@ -29,6 +29,11 @@
android:title="@string/mailbox_settings_title"
app:icon="@drawable/ic_mailbox" />
<Preference
android:title="@string/about_title"
app:fragment="org.briarproject.briar.android.settings.AboutFragment"
app:icon="@drawable/ic_info_dark" />
<PreferenceCategory
android:key="pref_key_actions"
android:layout="@layout/preferences_category"