mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Compare commits
121 Commits
release-1.
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c29c4fa5b | ||
|
|
34948fce13 | ||
|
|
f604320989 | ||
|
|
ea8c78bfef | ||
|
|
37d8a34c69 | ||
|
|
45e3c56eb8 | ||
|
|
647467a51a | ||
|
|
144bd7bf1d | ||
|
|
9d1c49c6b6 | ||
|
|
f5c2bed528 | ||
|
|
b4de1424ac | ||
|
|
f184bfe9ac | ||
|
|
d3204ab3ee | ||
|
|
163565e15c | ||
|
|
ef46f8ed64 | ||
|
|
f42a9a20d8 | ||
|
|
03c55311c7 | ||
|
|
ad67a0abd6 | ||
|
|
9bc3a2c73d | ||
|
|
af55d181e9 | ||
|
|
dde94baebd | ||
|
|
64e3940f77 | ||
|
|
012ab0310f | ||
|
|
63012d0a72 | ||
|
|
ebd233d005 | ||
|
|
f3b4440105 | ||
|
|
e36f275be7 | ||
|
|
3dc3d384d3 | ||
|
|
0c0c34696a | ||
|
|
7047f7d3d6 | ||
|
|
abc8c86eaa | ||
|
|
fa14448aa7 | ||
|
|
654603cfad | ||
|
|
112dace96c | ||
|
|
35bdb8075f | ||
|
|
f9d312a632 | ||
|
|
dd93c6852e | ||
|
|
5c4d971873 | ||
|
|
1d9acc7425 | ||
|
|
6738287a83 | ||
|
|
ea4f763a55 | ||
|
|
9d027fb250 | ||
|
|
b56a9beb1d | ||
|
|
1083507752 | ||
|
|
0d2137f0f8 | ||
|
|
f9ddb3a3a4 | ||
|
|
539198026d | ||
|
|
d84fb0e761 | ||
|
|
7bf07b3b84 | ||
|
|
e4da7968e3 | ||
|
|
f0e4e3c164 | ||
|
|
fac4132289 | ||
|
|
917da9ce36 | ||
|
|
e9249a9463 | ||
|
|
ec0a59db01 | ||
|
|
7e62d2aeff | ||
|
|
3d8826cef9 | ||
|
|
6113b4ebee | ||
|
|
00e3e64495 | ||
|
|
115724a0a4 | ||
|
|
0a92f0516f | ||
|
|
cc09a6deb2 | ||
|
|
5888775300 | ||
|
|
712f0f7cd9 | ||
|
|
0d3f531545 | ||
|
|
b02629bf34 | ||
|
|
b6693071f9 | ||
|
|
ff739e1982 | ||
|
|
47fa7ccc81 | ||
|
|
1cf1e8b617 | ||
|
|
b012f0991f | ||
|
|
1b7a1de881 | ||
|
|
8510fc80c9 | ||
|
|
5ae2a37d37 | ||
|
|
b032a84902 | ||
|
|
f6414b5ca1 | ||
|
|
c5669bece5 | ||
|
|
8c76db6216 | ||
|
|
370fe7601d | ||
|
|
55de9859e0 | ||
|
|
06a8086502 | ||
|
|
5522929b9b | ||
|
|
50d4f825c8 | ||
|
|
6cc225ec22 | ||
|
|
aaa03cd809 | ||
|
|
890c9837b5 | ||
|
|
6ac54af432 | ||
|
|
0ccbd57d1d | ||
|
|
66d3e8950e | ||
|
|
d291b7796e | ||
|
|
7af863160d | ||
|
|
c74e585668 | ||
|
|
0fd56b6c38 | ||
|
|
57264b0f04 | ||
|
|
70225c5380 | ||
|
|
f0a602a579 | ||
|
|
f9ab242f43 | ||
|
|
24efc29722 | ||
|
|
e72169ecee | ||
|
|
47db28a738 | ||
|
|
30261b0dcf | ||
|
|
10f9df4dc8 | ||
|
|
60dd260a5a | ||
|
|
579a72f54b | ||
|
|
e270910399 | ||
|
|
c1483ea61a | ||
|
|
35751ecef6 | ||
|
|
37d058a766 | ||
|
|
5a6846c972 | ||
|
|
b34e6ee2a7 | ||
|
|
2ae1e9631f | ||
|
|
76f2859a45 | ||
|
|
1c25b2da82 | ||
|
|
26fe2f804f | ||
|
|
c8d1ee878c | ||
|
|
7b16e78470 | ||
|
|
7fb71a6cb9 | ||
|
|
fc076954a3 | ||
|
|
6a1d2e65ba | ||
|
|
d67cbd40bd | ||
|
|
dba85debfa |
@@ -1,52 +1,30 @@
|
|||||||
image: briar/ci-image-android:latest
|
image: briar/ci-image-android:latest
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- optional_tests
|
- optional_tests
|
||||||
- check_reproducibility
|
- check_reproducibility
|
||||||
|
|
||||||
.base-test:
|
test:
|
||||||
|
stage: test
|
||||||
before_script:
|
before_script:
|
||||||
- set -e
|
- set -e
|
||||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
- export GRADLE_USER_HOME=$PWD/.gradle
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
key: "$CI_COMMIT_REF_SLUG"
|
|
||||||
paths:
|
paths:
|
||||||
- .gradle/wrapper
|
- .gradle/wrapper
|
||||||
- .gradle/caches
|
- .gradle/caches
|
||||||
|
|
||||||
|
script:
|
||||||
|
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
|
||||||
|
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check compileOfficialDebugAndroidTestSources compileScreenshotDebugAndroidTestSources
|
||||||
|
|
||||||
after_script:
|
after_script:
|
||||||
# these file change every time and should not be cached
|
# these file change every time but should not be cached
|
||||||
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
||||||
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
||||||
|
|
||||||
test:
|
|
||||||
extends: .base-test
|
|
||||||
stage: test
|
|
||||||
script:
|
|
||||||
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom animalSnifferMain animalSnifferTest
|
|
||||||
- ./gradlew --no-daemon -Djava.security.egd=file:/dev/urandom check
|
|
||||||
|
|
||||||
android test:
|
|
||||||
extends: .base-test
|
|
||||||
stage: optional_tests
|
|
||||||
image: briar/ci-image-android-emulator:latest
|
|
||||||
script:
|
|
||||||
# start emulator first, so it can fail early
|
|
||||||
- start-emulator.sh
|
|
||||||
# run normal and screenshot tests together (exclude Large tests)
|
|
||||||
- ./gradlew -Djava.security.egd=file:/dev/urandom connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest
|
|
||||||
artifacts:
|
|
||||||
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
|
|
||||||
paths:
|
|
||||||
- kernel.log
|
|
||||||
- logcat.txt
|
|
||||||
expire_in: 3 days
|
|
||||||
when: on_failure
|
|
||||||
when: manual
|
|
||||||
except:
|
|
||||||
- tags
|
|
||||||
tags:
|
|
||||||
- kvm
|
|
||||||
|
|
||||||
test_reproducible:
|
test_reproducible:
|
||||||
stage: check_reproducibility
|
stage: check_reproducibility
|
||||||
@@ -62,7 +40,6 @@ test_reproducible:
|
|||||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
- export GRADLE_USER_HOME=$PWD/.gradle
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
key: "$CI_COMMIT_REF_SLUG"
|
|
||||||
paths:
|
paths:
|
||||||
- .gradle/wrapper
|
- .gradle/wrapper
|
||||||
- .gradle/caches
|
- .gradle/caches
|
||||||
|
|||||||
1
.idea/dictionaries/briar.xml
generated
1
.idea/dictionaries/briar.xml
generated
@@ -7,6 +7,7 @@
|
|||||||
<w>encrypter</w>
|
<w>encrypter</w>
|
||||||
<w>identicon</w>
|
<w>identicon</w>
|
||||||
<w>introducee</w>
|
<w>introducee</w>
|
||||||
|
<w>introducees</w>
|
||||||
<w>introducer</w>
|
<w>introducer</w>
|
||||||
<w>onboarding</w>
|
<w>onboarding</w>
|
||||||
</words>
|
</words>
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Instrumentation Tests (destroys DB)" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests">
|
|
||||||
<module name="briar.briar-android" />
|
|
||||||
<option name="TESTING_TYPE" value="1" />
|
|
||||||
<option name="METHOD_NAME" value="" />
|
|
||||||
<option name="CLASS_NAME" value="" />
|
|
||||||
<option name="PACKAGE_NAME" value="org.briarproject.briar.android" />
|
|
||||||
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
|
|
||||||
<option name="EXTRA_OPTIONS" value="-e notAnnotation androidx.test.filters.LargeTest" />
|
|
||||||
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
|
|
||||||
<option name="CLEAR_LOGCAT" value="false" />
|
|
||||||
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
|
|
||||||
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
|
|
||||||
<option name="FORCE_STOP_RUNNING_APP" value="true" />
|
|
||||||
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
|
|
||||||
<option name="DEBUGGER_TYPE" value="Auto" />
|
|
||||||
<Auto>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
</Auto>
|
|
||||||
<Hybrid>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
</Hybrid>
|
|
||||||
<Java />
|
|
||||||
<Native>
|
|
||||||
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
|
|
||||||
<option name="SHOW_STATIC_VARS" value="true" />
|
|
||||||
<option name="WORKING_DIR" value="" />
|
|
||||||
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
|
|
||||||
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
|
|
||||||
</Native>
|
|
||||||
<Profilers>
|
|
||||||
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="STARTUP_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
|
|
||||||
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
|
|
||||||
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
|
|
||||||
</Profilers>
|
|
||||||
<method v="2">
|
|
||||||
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
@@ -8,15 +8,11 @@ android {
|
|||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion '30.0.2'
|
buildToolsVersion '30.0.2'
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
doNotStrip '**/*.so'
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 10220
|
versionCode 10216
|
||||||
versionName "1.2.20"
|
versionName "1.2.16"
|
||||||
consumerProguardFiles 'proguard-rules.txt'
|
consumerProguardFiles 'proguard-rules.txt'
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|||||||
@@ -175,11 +175,6 @@ class AndroidBluetoothPlugin
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
IoUtils.tryToClose(s, LOG, WARNING);
|
IoUtils.tryToClose(s, LOG, WARNING);
|
||||||
throw e;
|
throw e;
|
||||||
} catch (NullPointerException e) {
|
|
||||||
// BluetoothSocket#connect() may throw an NPE under unknown
|
|
||||||
// circumstances
|
|
||||||
IoUtils.tryToClose(s, LOG, WARNING);
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,8 +114,12 @@ public class AndroidUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of supported content types for image attachments.
|
* Returns an array of supported content types for image attachments.
|
||||||
|
* GIFs can't be compressed on API < 24 so they're not supported.
|
||||||
|
* <p>
|
||||||
|
* TODO: Remove this restriction when large message support is added
|
||||||
*/
|
*/
|
||||||
public static String[] getSupportedImageContentTypes() {
|
public static String[] getSupportedImageContentTypes() {
|
||||||
return new String[] {"image/jpeg", "image/png", "image/gif"};
|
if (SDK_INT < 24) return new String[] {"image/jpeg", "image/png"};
|
||||||
|
else return new String[] {"image/jpeg", "image/png", "image/gif"};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ public interface FeatureFlags {
|
|||||||
|
|
||||||
boolean shouldEnableProfilePictures();
|
boolean shouldEnableProfilePictures();
|
||||||
|
|
||||||
|
boolean shouldEnableDisappearingMessages();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.briarproject.bramble.api.cleanup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.db.Transaction;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for registering a hook with the {@link CleanupManager}
|
||||||
|
* that will be called when a message's cleanup deadline is reached.
|
||||||
|
*/
|
||||||
|
@NotNullByDefault
|
||||||
|
public interface CleanupHook {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the cleanup deadlines of one or more messages are reached.
|
||||||
|
* <p>
|
||||||
|
* The callee is not required to delete the messages, but the hook won't be
|
||||||
|
* called again for these messages unless another cleanup timer is set (see
|
||||||
|
* {@link DatabaseComponent#setCleanupTimerDuration(Transaction, MessageId, long)}
|
||||||
|
* and {@link DatabaseComponent#startCleanupTimer(Transaction, MessageId)}).
|
||||||
|
*/
|
||||||
|
void deleteMessages(Transaction txn, GroupId g,
|
||||||
|
Collection<MessageId> messageIds) throws DbException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.briarproject.bramble.api.cleanup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
|
||||||
|
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.bramble.api.db.Transaction;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.ClientId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CleanupManager is responsible for tracking the cleanup deadlines of
|
||||||
|
* messages and passing them to their respective
|
||||||
|
* {@link CleanupHook CleanupHooks} when the deadlines are reached.
|
||||||
|
* <p>
|
||||||
|
* The CleanupManager responds to
|
||||||
|
* {@link CleanupTimerStartedEvent CleanupTimerStartedEvents} broadcast by the
|
||||||
|
* {@link DatabaseComponent}.
|
||||||
|
* <p>
|
||||||
|
* See {@link DatabaseComponent#setCleanupTimerDuration(Transaction, MessageId, long)},
|
||||||
|
* {@link DatabaseComponent#startCleanupTimer(Transaction, MessageId)},
|
||||||
|
* {@link DatabaseComponent#stopCleanupTimer(Transaction, MessageId)}.
|
||||||
|
*/
|
||||||
|
@NotNullByDefault
|
||||||
|
public interface CleanupManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When scheduling a cleanup task we overshoot the deadline by this many
|
||||||
|
* milliseconds to reduce the number of tasks that need to be scheduled
|
||||||
|
* when messages have cleanup deadlines that are close together.
|
||||||
|
*/
|
||||||
|
long BATCH_DELAY_MS = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a hook to be called when messages are due for cleanup.
|
||||||
|
* This method should be called before
|
||||||
|
* {@link LifecycleManager#startServices(SecretKey)}.
|
||||||
|
*/
|
||||||
|
void registerCleanupHook(ClientId c, int majorVersion,
|
||||||
|
CleanupHook hook);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.briarproject.bramble.api.cleanup.event;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event that is broadcast when a message's cleanup timer is started.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
|
public class CleanupTimerStartedEvent extends Event {
|
||||||
|
|
||||||
|
private final MessageId messageId;
|
||||||
|
private final long cleanupDeadline;
|
||||||
|
|
||||||
|
public CleanupTimerStartedEvent(MessageId messageId,
|
||||||
|
long cleanupDeadline) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
this.cleanupDeadline = cleanupDeadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageId getMessageId() {
|
||||||
|
return messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCleanupDeadline() {
|
||||||
|
return cleanupDeadline;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.bramble.api.client;
|
package org.briarproject.bramble.api.client;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
import org.briarproject.bramble.api.FormatException;
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.crypto.PrivateKey;
|
import org.briarproject.bramble.api.crypto.PrivateKey;
|
||||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||||
@@ -16,6 +17,7 @@ import org.briarproject.bramble.api.sync.Message;
|
|||||||
import org.briarproject.bramble.api.sync.MessageId;
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
@@ -50,9 +52,11 @@ public interface ClientHelper {
|
|||||||
BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g)
|
BdfDictionary getGroupMetadataAsDictionary(Transaction txn, GroupId g)
|
||||||
throws DbException, FormatException;
|
throws DbException, FormatException;
|
||||||
|
|
||||||
|
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
|
||||||
|
BdfDictionary query) throws DbException, FormatException;
|
||||||
|
|
||||||
BdfDictionary getMessageMetadataAsDictionary(MessageId m)
|
BdfDictionary getMessageMetadataAsDictionary(MessageId m)
|
||||||
throws DbException,
|
throws DbException, FormatException;
|
||||||
FormatException;
|
|
||||||
|
|
||||||
BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m)
|
BdfDictionary getMessageMetadataAsDictionary(Transaction txn, MessageId m)
|
||||||
throws DbException, FormatException;
|
throws DbException, FormatException;
|
||||||
@@ -119,4 +123,17 @@ public interface ClientHelper {
|
|||||||
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
|
Map<TransportId, TransportProperties> parseAndValidateTransportPropertiesMap(
|
||||||
BdfDictionary properties) throws FormatException;
|
BdfDictionary properties) throws FormatException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the contact ID from the group metadata of the given contact
|
||||||
|
* group.
|
||||||
|
*/
|
||||||
|
ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
||||||
|
throws DbException, FormatException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the given contact ID in the group metadata of the given contact
|
||||||
|
* group.
|
||||||
|
*/
|
||||||
|
void setContactId(Transaction txn, GroupId contactGroupId, ContactId c)
|
||||||
|
throws DbException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.briarproject.bramble.api.client;
|
||||||
|
|
||||||
|
public interface ContactGroupConstants {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group metadata key for associating a contact ID with a contact group.
|
||||||
|
*/
|
||||||
|
String GROUP_KEY_CONTACT_ID = "contactId";
|
||||||
|
}
|
||||||
@@ -41,6 +41,18 @@ import javax.annotation.Nullable;
|
|||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public interface DatabaseComponent extends TransactionManager {
|
public interface DatabaseComponent extends TransactionManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return value for {@link #getNextCleanupDeadline(Transaction)} if
|
||||||
|
* no messages are scheduled to be deleted.
|
||||||
|
*/
|
||||||
|
long NO_CLEANUP_DEADLINE = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return value for {@link #startCleanupTimer(Transaction, MessageId)}
|
||||||
|
* if the cleanup timer was not started.
|
||||||
|
*/
|
||||||
|
long TIMER_NOT_STARTED = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the database and returns true if the database already existed.
|
* Opens the database and returns true if the database already existed.
|
||||||
*
|
*
|
||||||
@@ -288,6 +300,16 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
Collection<MessageId> getMessageIds(Transaction txn, GroupId g)
|
Collection<MessageId> getMessageIds(Transaction txn, GroupId g)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IDs of any delivered messages in the given group with
|
||||||
|
* metadata that matches all entries in the given query. If the query is
|
||||||
|
* empty, the IDs of all delivered messages are returned.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
|
||||||
|
Metadata query) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the IDs of any messages that need to be validated.
|
* Returns the IDs of any messages that need to be validated.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -314,6 +336,15 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
Collection<MessageId> getMessagesToShare(Transaction txn)
|
Collection<MessageId> getMessagesToShare(Transaction txn)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IDs of any messages of any messages that are due for
|
||||||
|
* deletion, along with their group IDs.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
Map<GroupId, Collection<MessageId>> getMessagesToDelete(Transaction txn)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the metadata for all delivered messages in the given group.
|
* Returns the metadata for all delivered messages in the given group.
|
||||||
* <p/>
|
* <p/>
|
||||||
@@ -395,6 +426,15 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
|
MessageStatus getMessageStatus(Transaction txn, ContactId c, MessageId m)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||||
|
* message is due to be deleted, or {@link #NO_CLEANUP_DEADLINE}
|
||||||
|
* if no messages are scheduled to be deleted.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
long getNextCleanupDeadline(Transaction txn) throws DbException;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns the next time (in milliseconds since the Unix epoch) when a
|
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||||
* message is due to be sent to the given contact. The returned value may
|
* message is due to be sent to the given contact. The returned value may
|
||||||
@@ -535,6 +575,13 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
|
void removeTransportKeys(Transaction txn, TransportId t, KeySetId k)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cleanup timer duration for the given message. This does not
|
||||||
|
* start the message's cleanup timer.
|
||||||
|
*/
|
||||||
|
void setCleanupTimerDuration(Transaction txn, MessageId m, long duration)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given contact as verified.
|
* Marks the given contact as verified.
|
||||||
*/
|
*/
|
||||||
@@ -557,6 +604,12 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
*/
|
*/
|
||||||
void setMessagePermanent(Transaction txn, MessageId m) throws DbException;
|
void setMessagePermanent(Transaction txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the given message as not shared. This method is only meant for
|
||||||
|
* testing.
|
||||||
|
*/
|
||||||
|
void setMessageNotShared(Transaction txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given message as shared.
|
* Marks the given message as shared.
|
||||||
*/
|
*/
|
||||||
@@ -599,6 +652,22 @@ public interface DatabaseComponent extends TransactionManager {
|
|||||||
void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
|
void setTransportKeysActive(Transaction txn, TransportId t, KeySetId k)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the cleanup timer for the given message, if a timer duration
|
||||||
|
* has been set and the timer has not already been started.
|
||||||
|
*
|
||||||
|
* @return The cleanup deadline, or {@link #TIMER_NOT_STARTED} if no
|
||||||
|
* timer duration has been set for this message or its timer has already
|
||||||
|
* been started.
|
||||||
|
*/
|
||||||
|
long startCleanupTimer(Transaction txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the cleanup timer for the given message, if the timer has been
|
||||||
|
* started.
|
||||||
|
*/
|
||||||
|
void stopCleanupTimer(Transaction txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the given transport keys, deleting any keys they have replaced.
|
* Stores the given transport keys, deleting any keys they have replaced.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import org.briarproject.bramble.api.data.BdfList;
|
|||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class ValidationUtils {
|
public class ValidationUtils {
|
||||||
|
|
||||||
@@ -64,4 +66,9 @@ public class ValidationUtils {
|
|||||||
if (dictionary != null && dictionary.size() != size)
|
if (dictionary != null && dictionary.size() != size)
|
||||||
throw new FormatException();
|
throw new FormatException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void checkRange(@Nullable Long l, long min, long max)
|
||||||
|
throws FormatException {
|
||||||
|
if (l != null && (l < min || l > max)) throw new FormatException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.briarproject.bramble.test;
|
||||||
|
|
||||||
|
public interface TimeTravel {
|
||||||
|
|
||||||
|
void setCurrentTimeMillis(long now) throws InterruptedException;
|
||||||
|
|
||||||
|
void addCurrentTimeMillis(long add) throws InterruptedException;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.bramble;
|
package org.briarproject.bramble;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.cleanup.CleanupModule;
|
||||||
import org.briarproject.bramble.contact.ContactModule;
|
import org.briarproject.bramble.contact.ContactModule;
|
||||||
import org.briarproject.bramble.crypto.CryptoExecutorModule;
|
import org.briarproject.bramble.crypto.CryptoExecutorModule;
|
||||||
import org.briarproject.bramble.db.DatabaseExecutorModule;
|
import org.briarproject.bramble.db.DatabaseExecutorModule;
|
||||||
@@ -14,6 +15,8 @@ import org.briarproject.bramble.versioning.VersioningModule;
|
|||||||
|
|
||||||
public interface BrambleCoreEagerSingletons {
|
public interface BrambleCoreEagerSingletons {
|
||||||
|
|
||||||
|
void inject(CleanupModule.EagerSingletons init);
|
||||||
|
|
||||||
void inject(ContactModule.EagerSingletons init);
|
void inject(ContactModule.EagerSingletons init);
|
||||||
|
|
||||||
void inject(CryptoExecutorModule.EagerSingletons init);
|
void inject(CryptoExecutorModule.EagerSingletons init);
|
||||||
@@ -39,6 +42,7 @@ public interface BrambleCoreEagerSingletons {
|
|||||||
class Helper {
|
class Helper {
|
||||||
|
|
||||||
public static void injectEagerSingletons(BrambleCoreEagerSingletons c) {
|
public static void injectEagerSingletons(BrambleCoreEagerSingletons c) {
|
||||||
|
c.inject(new CleanupModule.EagerSingletons());
|
||||||
c.inject(new ContactModule.EagerSingletons());
|
c.inject(new ContactModule.EagerSingletons());
|
||||||
c.inject(new CryptoExecutorModule.EagerSingletons());
|
c.inject(new CryptoExecutorModule.EagerSingletons());
|
||||||
c.inject(new DatabaseExecutorModule.EagerSingletons());
|
c.inject(new DatabaseExecutorModule.EagerSingletons());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.bramble;
|
package org.briarproject.bramble;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.cleanup.CleanupModule;
|
||||||
import org.briarproject.bramble.client.ClientModule;
|
import org.briarproject.bramble.client.ClientModule;
|
||||||
import org.briarproject.bramble.connection.ConnectionModule;
|
import org.briarproject.bramble.connection.ConnectionModule;
|
||||||
import org.briarproject.bramble.contact.ContactModule;
|
import org.briarproject.bramble.contact.ContactModule;
|
||||||
@@ -21,15 +22,14 @@ import org.briarproject.bramble.rendezvous.RendezvousModule;
|
|||||||
import org.briarproject.bramble.settings.SettingsModule;
|
import org.briarproject.bramble.settings.SettingsModule;
|
||||||
import org.briarproject.bramble.sync.SyncModule;
|
import org.briarproject.bramble.sync.SyncModule;
|
||||||
import org.briarproject.bramble.sync.validation.ValidationModule;
|
import org.briarproject.bramble.sync.validation.ValidationModule;
|
||||||
import org.briarproject.bramble.system.ClockModule;
|
|
||||||
import org.briarproject.bramble.transport.TransportModule;
|
import org.briarproject.bramble.transport.TransportModule;
|
||||||
import org.briarproject.bramble.versioning.VersioningModule;
|
import org.briarproject.bramble.versioning.VersioningModule;
|
||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
|
|
||||||
@Module(includes = {
|
@Module(includes = {
|
||||||
|
CleanupModule.class,
|
||||||
ClientModule.class,
|
ClientModule.class,
|
||||||
ClockModule.class,
|
|
||||||
ConnectionModule.class,
|
ConnectionModule.class,
|
||||||
ContactModule.class,
|
ContactModule.class,
|
||||||
CryptoModule.class,
|
CryptoModule.class,
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package org.briarproject.bramble.cleanup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.CleanupHook;
|
||||||
|
import org.briarproject.bramble.api.cleanup.CleanupManager;
|
||||||
|
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.db.Transaction;
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.Service;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.ClientId;
|
||||||
|
import org.briarproject.bramble.api.sync.Group;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
|
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||||
|
import org.briarproject.bramble.api.versioning.ClientMajorVersion;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static java.util.logging.Level.INFO;
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
|
||||||
|
@ThreadSafe
|
||||||
|
@NotNullByDefault
|
||||||
|
class CleanupManagerImpl implements CleanupManager, Service, EventListener {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
getLogger(CleanupManagerImpl.class.getName());
|
||||||
|
|
||||||
|
private final Executor dbExecutor;
|
||||||
|
private final DatabaseComponent db;
|
||||||
|
private final TaskScheduler taskScheduler;
|
||||||
|
private final Clock clock;
|
||||||
|
private final Map<ClientMajorVersion, CleanupHook> hooks =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
@GuardedBy("lock")
|
||||||
|
private final Set<CleanupTask> pending = new HashSet<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
CleanupManagerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
|
DatabaseComponent db, TaskScheduler taskScheduler, Clock clock) {
|
||||||
|
this.dbExecutor = dbExecutor;
|
||||||
|
this.db = db;
|
||||||
|
this.taskScheduler = taskScheduler;
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerCleanupHook(ClientId c, int majorVersion,
|
||||||
|
CleanupHook hook) {
|
||||||
|
hooks.put(new ClientMajorVersion(c, majorVersion), hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startService() {
|
||||||
|
maybeScheduleTask(clock.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopService() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void eventOccurred(Event e) {
|
||||||
|
if (e instanceof CleanupTimerStartedEvent) {
|
||||||
|
CleanupTimerStartedEvent a = (CleanupTimerStartedEvent) e;
|
||||||
|
maybeScheduleTask(a.getCleanupDeadline());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybeScheduleTask(long deadline) {
|
||||||
|
synchronized (lock) {
|
||||||
|
for (CleanupTask task : pending) {
|
||||||
|
if (task.deadline <= deadline) return;
|
||||||
|
}
|
||||||
|
CleanupTask task = new CleanupTask(deadline);
|
||||||
|
pending.add(task);
|
||||||
|
scheduleTask(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleTask(CleanupTask task) {
|
||||||
|
long now = clock.currentTimeMillis();
|
||||||
|
long delay = max(0, task.deadline - now + BATCH_DELAY_MS);
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Scheduling cleanup task in " + delay + " ms");
|
||||||
|
}
|
||||||
|
taskScheduler.schedule(() -> deleteMessagesAndScheduleNextTask(task),
|
||||||
|
dbExecutor, delay, MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteMessagesAndScheduleNextTask(CleanupTask task) {
|
||||||
|
try {
|
||||||
|
synchronized (lock) {
|
||||||
|
pending.remove(task);
|
||||||
|
}
|
||||||
|
long deadline = db.transactionWithResult(false, txn -> {
|
||||||
|
deleteMessages(txn);
|
||||||
|
return db.getNextCleanupDeadline(txn);
|
||||||
|
});
|
||||||
|
if (deadline != NO_CLEANUP_DEADLINE) {
|
||||||
|
maybeScheduleTask(deadline);
|
||||||
|
}
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteMessages(Transaction txn) throws DbException {
|
||||||
|
Map<GroupId, Collection<MessageId>> ids = db.getMessagesToDelete(txn);
|
||||||
|
for (Entry<GroupId, Collection<MessageId>> e : ids.entrySet()) {
|
||||||
|
GroupId groupId = e.getKey();
|
||||||
|
Collection<MessageId> messageIds = e.getValue();
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info(messageIds.size() + " messages to delete");
|
||||||
|
}
|
||||||
|
for (MessageId m : messageIds) db.stopCleanupTimer(txn, m);
|
||||||
|
Group group = db.getGroup(txn, groupId);
|
||||||
|
ClientMajorVersion cv = new ClientMajorVersion(group.getClientId(),
|
||||||
|
group.getMajorVersion());
|
||||||
|
CleanupHook hook = hooks.get(cv);
|
||||||
|
if (hook == null) {
|
||||||
|
throw new IllegalStateException("No cleanup hook for " + cv);
|
||||||
|
}
|
||||||
|
hook.deleteMessages(txn, groupId, messageIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CleanupTask {
|
||||||
|
|
||||||
|
private final long deadline;
|
||||||
|
|
||||||
|
private CleanupTask(long deadline) {
|
||||||
|
this.deadline = deadline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.briarproject.bramble.cleanup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.CleanupManager;
|
||||||
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public class CleanupModule {
|
||||||
|
|
||||||
|
public static class EagerSingletons {
|
||||||
|
@Inject
|
||||||
|
CleanupManager cleanupManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
CleanupManager provideCleanupManager(LifecycleManager lifecycleManager,
|
||||||
|
EventBus eventBus, CleanupManagerImpl cleanupManager) {
|
||||||
|
lifecycleManager.registerService(cleanupManager);
|
||||||
|
eventBus.addListener(cleanupManager);
|
||||||
|
return cleanupManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@ package org.briarproject.bramble.client;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
import org.briarproject.bramble.api.FormatException;
|
||||||
import org.briarproject.bramble.api.client.ClientHelper;
|
import org.briarproject.bramble.api.client.ClientHelper;
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||||
import org.briarproject.bramble.api.crypto.KeyParser;
|
import org.briarproject.bramble.api.crypto.KeyParser;
|
||||||
import org.briarproject.bramble.api.crypto.PrivateKey;
|
import org.briarproject.bramble.api.crypto.PrivateKey;
|
||||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.bramble.api.data.BdfEntry;
|
||||||
import org.briarproject.bramble.api.data.BdfList;
|
import org.briarproject.bramble.api.data.BdfList;
|
||||||
import org.briarproject.bramble.api.data.BdfReader;
|
import org.briarproject.bramble.api.data.BdfReader;
|
||||||
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
import org.briarproject.bramble.api.data.BdfReaderFactory;
|
||||||
@@ -32,6 +34,7 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
@@ -39,6 +42,7 @@ import java.util.Map.Entry;
|
|||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static org.briarproject.bramble.api.client.ContactGroupConstants.GROUP_KEY_CONTACT_ID;
|
||||||
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
|
import static org.briarproject.bramble.api.identity.Author.FORMAT_VERSION;
|
||||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||||
@@ -151,6 +155,12 @@ class ClientHelperImpl implements ClientHelper {
|
|||||||
return metadataParser.parse(metadata);
|
return metadataParser.parse(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MessageId> getMessageIds(Transaction txn, GroupId g,
|
||||||
|
BdfDictionary query) throws DbException, FormatException {
|
||||||
|
return db.getMessageIds(txn, g, metadataEncoder.encode(query));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BdfDictionary getMessageMetadataAsDictionary(MessageId m)
|
public BdfDictionary getMessageMetadataAsDictionary(MessageId m)
|
||||||
throws DbException, FormatException {
|
throws DbException, FormatException {
|
||||||
@@ -389,4 +399,27 @@ class ClientHelperImpl implements ClientHelper {
|
|||||||
return tpMap;
|
return tpMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
||||||
|
throws DbException {
|
||||||
|
try {
|
||||||
|
BdfDictionary meta =
|
||||||
|
getGroupMetadataAsDictionary(txn, contactGroupId);
|
||||||
|
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e); // Invalid group metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setContactId(Transaction txn, GroupId contactGroupId,
|
||||||
|
ContactId c) throws DbException {
|
||||||
|
BdfDictionary meta = BdfDictionary.of(
|
||||||
|
new BdfEntry(GROUP_KEY_CONTACT_ID, c.getInt()));
|
||||||
|
try {
|
||||||
|
mergeGroupMetadata(txn, contactGroupId, meta);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -497,6 +497,25 @@ interface Database<T> {
|
|||||||
*/
|
*/
|
||||||
Collection<MessageId> getMessagesToShare(T txn) throws DbException;
|
Collection<MessageId> getMessagesToShare(T txn) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IDs of any messages of any messages that are due for
|
||||||
|
* deletion, along with their group IDs.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
Map<GroupId, Collection<MessageId>> getMessagesToDelete(T txn)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||||
|
* message is due to be deleted, or
|
||||||
|
* {@link DatabaseComponent#NO_CLEANUP_DEADLINE} if no messages are
|
||||||
|
* scheduled to be deleted.
|
||||||
|
* <p/>
|
||||||
|
* Read-only.
|
||||||
|
*/
|
||||||
|
long getNextCleanupDeadline(T txn) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the next time (in milliseconds since the Unix epoch) when a
|
* Returns the next time (in milliseconds since the Unix epoch) when a
|
||||||
* message is due to be sent to the given contact. The returned value may
|
* message is due to be sent to the given contact. The returned value may
|
||||||
@@ -606,8 +625,10 @@ interface Database<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks a message as having been seen by the given contact.
|
* Marks a message as having been seen by the given contact.
|
||||||
|
*
|
||||||
|
* @return True if the message was not already marked as seen.
|
||||||
*/
|
*/
|
||||||
void raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
|
boolean raiseSeenFlag(T txn, ContactId c, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a contact from the database.
|
* Removes a contact from the database.
|
||||||
@@ -671,6 +692,13 @@ interface Database<T> {
|
|||||||
*/
|
*/
|
||||||
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
|
void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cleanup timer duration for the given message. This does not
|
||||||
|
* start the message's cleanup timer.
|
||||||
|
*/
|
||||||
|
void setCleanupTimerDuration(T txn, MessageId m, long duration)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given contact as verified.
|
* Marks the given contact as verified.
|
||||||
*/
|
*/
|
||||||
@@ -701,9 +729,10 @@ interface Database<T> {
|
|||||||
void setMessagePermanent(T txn, MessageId m) throws DbException;
|
void setMessagePermanent(T txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the given message as shared.
|
* Marks the given message as shared or not.
|
||||||
*/
|
*/
|
||||||
void setMessageShared(T txn, MessageId m) throws DbException;
|
void setMessageShared(T txn, MessageId m, boolean shared)
|
||||||
|
throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the validation and delivery state of the given message.
|
* Sets the validation and delivery state of the given message.
|
||||||
@@ -730,6 +759,22 @@ interface Database<T> {
|
|||||||
void setTransportKeysActive(T txn, TransportId t, KeySetId k)
|
void setTransportKeysActive(T txn, TransportId t, KeySetId k)
|
||||||
throws DbException;
|
throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the cleanup timer for the given message, if a timer duration
|
||||||
|
* has been set and the timer has not already been started.
|
||||||
|
*
|
||||||
|
* @return The cleanup deadline, or
|
||||||
|
* {@link DatabaseComponent#TIMER_NOT_STARTED} if no timer duration has
|
||||||
|
* been set for this message or its timer has already been started.
|
||||||
|
*/
|
||||||
|
long startCleanupTimer(T txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the cleanup timer for the given message, if the timer has been
|
||||||
|
* started.
|
||||||
|
*/
|
||||||
|
void stopCleanupTimer(T txn, MessageId m) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the transmission count, expiry time and estimated time of arrival
|
* Updates the transmission count, expiry time and estimated time of arrival
|
||||||
* of the given message with respect to the given contact, using the latency
|
* of the given message with respect to the given contact, using the latency
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.bramble.db;
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.contact.PendingContact;
|
import org.briarproject.bramble.api.contact.PendingContact;
|
||||||
@@ -576,6 +577,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
return db.getMessageIds(txn, g);
|
return db.getMessageIds(txn, g);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<MessageId> getMessageIds(Transaction transaction,
|
||||||
|
GroupId g, Metadata query) throws DbException {
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsGroup(txn, g))
|
||||||
|
throw new NoSuchGroupException();
|
||||||
|
return db.getMessageIds(txn, g, query);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
|
public Collection<MessageId> getMessagesToValidate(Transaction transaction)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -597,6 +607,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
return db.getMessagesToShare(txn);
|
return db.getMessagesToShare(txn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
|
||||||
|
Transaction transaction) throws DbException {
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
return db.getMessagesToDelete(txn);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction,
|
public Map<MessageId, Metadata> getMessageMetadata(Transaction transaction,
|
||||||
GroupId g) throws DbException {
|
GroupId g) throws DbException {
|
||||||
@@ -692,6 +709,13 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
return db.getMessageDependents(txn, m);
|
return db.getMessageDependents(txn, m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getNextCleanupDeadline(Transaction transaction)
|
||||||
|
throws DbException {
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
return db.getNextCleanupDeadline(txn);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getNextSendTime(Transaction transaction, ContactId c)
|
public long getNextSendTime(Transaction transaction, ContactId c)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -795,8 +819,17 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
Collection<MessageId> acked = new ArrayList<>();
|
Collection<MessageId> acked = new ArrayList<>();
|
||||||
for (MessageId m : a.getMessageIds()) {
|
for (MessageId m : a.getMessageIds()) {
|
||||||
if (db.containsVisibleMessage(txn, c, m)) {
|
if (db.containsVisibleMessage(txn, c, m)) {
|
||||||
db.raiseSeenFlag(txn, c, m);
|
if (db.raiseSeenFlag(txn, c, m)) {
|
||||||
acked.add(m);
|
// This is the first time the message has been acked by
|
||||||
|
// this contact. Start the cleanup timer (a no-op unless
|
||||||
|
// a cleanup deadline has been set for this message)
|
||||||
|
long deadline = db.startCleanupTimer(txn, m);
|
||||||
|
if (deadline != TIMER_NOT_STARTED) {
|
||||||
|
transaction.attach(new CleanupTimerStartedEvent(m,
|
||||||
|
deadline));
|
||||||
|
}
|
||||||
|
acked.add(m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (acked.size() > 0) {
|
if (acked.size() > 0) {
|
||||||
@@ -952,6 +985,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
db.removeTransportKeys(txn, t, k);
|
db.removeTransportKeys(txn, t, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCleanupTimerDuration(Transaction transaction, MessageId m,
|
||||||
|
long duration) throws DbException {
|
||||||
|
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsMessage(txn, m))
|
||||||
|
throw new NoSuchMessageException();
|
||||||
|
db.setCleanupTimerDuration(txn, m, duration);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setContactVerified(Transaction transaction, ContactId c)
|
public void setContactVerified(Transaction transaction, ContactId c)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -1001,6 +1044,16 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
db.setMessagePermanent(txn, m);
|
db.setMessagePermanent(txn, m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMessageNotShared(Transaction transaction, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsMessage(txn, m))
|
||||||
|
throw new NoSuchMessageException();
|
||||||
|
db.setMessageShared(txn, m, false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMessageShared(Transaction transaction, MessageId m)
|
public void setMessageShared(Transaction transaction, MessageId m)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -1010,7 +1063,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
throw new NoSuchMessageException();
|
throw new NoSuchMessageException();
|
||||||
if (db.getMessageState(txn, m) != DELIVERED)
|
if (db.getMessageState(txn, m) != DELIVERED)
|
||||||
throw new IllegalArgumentException("Shared undelivered message");
|
throw new IllegalArgumentException("Shared undelivered message");
|
||||||
db.setMessageShared(txn, m);
|
db.setMessageShared(txn, m, true);
|
||||||
transaction.attach(new MessageSharedEvent(m));
|
transaction.attach(new MessageSharedEvent(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1082,6 +1135,30 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
|
|||||||
db.setTransportKeysActive(txn, t, k);
|
db.setTransportKeysActive(txn, t, k);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long startCleanupTimer(Transaction transaction, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsMessage(txn, m))
|
||||||
|
throw new NoSuchMessageException();
|
||||||
|
long deadline = db.startCleanupTimer(txn, m);
|
||||||
|
if (deadline != TIMER_NOT_STARTED) {
|
||||||
|
transaction.attach(new CleanupTimerStartedEvent(m, deadline));
|
||||||
|
}
|
||||||
|
return deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopCleanupTimer(Transaction transaction, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
if (transaction.isReadOnly()) throw new IllegalArgumentException();
|
||||||
|
T txn = unbox(transaction);
|
||||||
|
if (!db.containsMessage(txn, m))
|
||||||
|
throw new NoSuchMessageException();
|
||||||
|
db.stopCleanupTimer(txn, m);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateTransportKeys(Transaction transaction,
|
public void updateTransportKeys(Transaction transaction,
|
||||||
Collection<TransportKeySet> keys) throws DbException {
|
Collection<TransportKeySet> keys) throws DbException {
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ import static java.util.Arrays.asList;
|
|||||||
import static java.util.logging.Level.INFO;
|
import static java.util.logging.Level.INFO;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
|
||||||
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||||
@@ -98,7 +100,7 @@ import static org.briarproject.bramble.util.LogUtils.now;
|
|||||||
abstract class JdbcDatabase implements Database<Connection> {
|
abstract class JdbcDatabase implements Database<Connection> {
|
||||||
|
|
||||||
// Package access for testing
|
// Package access for testing
|
||||||
static final int CODE_SCHEMA_VERSION = 47;
|
static final int CODE_SCHEMA_VERSION = 48;
|
||||||
|
|
||||||
// Time period offsets for incoming transport keys
|
// Time period offsets for incoming transport keys
|
||||||
private static final int OFFSET_PREV = -1;
|
private static final int OFFSET_PREV = -1;
|
||||||
@@ -180,6 +182,11 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
+ " state INT NOT NULL,"
|
+ " state INT NOT NULL,"
|
||||||
+ " shared BOOLEAN NOT NULL,"
|
+ " shared BOOLEAN NOT NULL,"
|
||||||
+ " temporary BOOLEAN NOT NULL,"
|
+ " temporary BOOLEAN NOT NULL,"
|
||||||
|
// Null if no timer duration has been set
|
||||||
|
+ " cleanupTimerDuration BIGINT,"
|
||||||
|
// Null if no timer duration has been set or the timer
|
||||||
|
// hasn't started
|
||||||
|
+ " cleanupDeadline BIGINT,"
|
||||||
+ " length INT NOT NULL,"
|
+ " length INT NOT NULL,"
|
||||||
+ " raw BLOB," // Null if message has been deleted
|
+ " raw BLOB," // Null if message has been deleted
|
||||||
+ " PRIMARY KEY (messageId),"
|
+ " PRIMARY KEY (messageId),"
|
||||||
@@ -336,6 +343,10 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
|
"CREATE INDEX IF NOT EXISTS statusesByContactIdTimestamp"
|
||||||
+ " ON statuses (contactId, timestamp)";
|
+ " ON statuses (contactId, timestamp)";
|
||||||
|
|
||||||
|
private static final String INDEX_MESSAGES_BY_CLEANUP_DEADLINE =
|
||||||
|
"CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
|
||||||
|
+ " ON messages (cleanupDeadline)";
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(JdbcDatabase.class.getName());
|
getLogger(JdbcDatabase.class.getName());
|
||||||
|
|
||||||
@@ -463,7 +474,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
new Migration43_44(dbTypes),
|
new Migration43_44(dbTypes),
|
||||||
new Migration44_45(),
|
new Migration44_45(),
|
||||||
new Migration45_46(),
|
new Migration45_46(),
|
||||||
new Migration46_47(dbTypes)
|
new Migration46_47(dbTypes),
|
||||||
|
new Migration47_48()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,6 +543,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID);
|
s.executeUpdate(INDEX_MESSAGE_DEPENDENCIES_BY_DEPENDENCY_ID);
|
||||||
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
|
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_GROUP_ID);
|
||||||
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
|
s.executeUpdate(INDEX_STATUSES_BY_CONTACT_ID_TIMESTAMP);
|
||||||
|
s.executeUpdate(INDEX_MESSAGES_BY_CLEANUP_DEADLINE);
|
||||||
s.close();
|
s.close();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(s, LOG, WARNING);
|
tryToClose(s, LOG, WARNING);
|
||||||
@@ -1290,7 +1303,9 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
public void deleteMessage(Connection txn, MessageId m) throws DbException {
|
public void deleteMessage(Connection txn, MessageId m) throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
String sql = "UPDATE messages SET raw = NULL WHERE messageId = ?";
|
String sql = "UPDATE messages"
|
||||||
|
+ " SET raw = NULL, cleanupDeadline = NULL"
|
||||||
|
+ " WHERE messageId = ?";
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setBytes(1, m.getBytes());
|
ps.setBytes(1, m.getBytes());
|
||||||
int affected = ps.executeUpdate();
|
int affected = ps.executeUpdate();
|
||||||
@@ -1769,7 +1784,6 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
// Return early if there are no matches
|
// Return early if there are no matches
|
||||||
if (intersection.isEmpty()) return Collections.emptySet();
|
if (intersection.isEmpty()) return Collections.emptySet();
|
||||||
}
|
}
|
||||||
if (intersection == null) throw new AssertionError();
|
|
||||||
return intersection;
|
return intersection;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(rs, LOG, WARNING);
|
tryToClose(rs, LOG, WARNING);
|
||||||
@@ -2226,6 +2240,39 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<GroupId, Collection<MessageId>> getMessagesToDelete(
|
||||||
|
Connection txn) throws DbException {
|
||||||
|
long now = clock.currentTimeMillis();
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT messageId, groupId FROM messages"
|
||||||
|
+ " WHERE cleanupDeadline <= ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setLong(1, now);
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
Map<GroupId, Collection<MessageId>> ids = new HashMap<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
MessageId m = new MessageId(rs.getBytes(1));
|
||||||
|
GroupId g = new GroupId(rs.getBytes(2));
|
||||||
|
Collection<MessageId> messageIds = ids.get(g);
|
||||||
|
if (messageIds == null) {
|
||||||
|
messageIds = new ArrayList<>();
|
||||||
|
ids.put(g, messageIds);
|
||||||
|
}
|
||||||
|
messageIds.add(m);
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return ids;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(rs, LOG, WARNING);
|
||||||
|
tryToClose(ps, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getNextSendTime(Connection txn, ContactId c)
|
public long getNextSendTime(Connection txn, ContactId c)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -2256,6 +2303,31 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getNextCleanupDeadline(Connection txn) throws DbException {
|
||||||
|
Statement s = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "SELECT cleanupDeadline FROM messages"
|
||||||
|
+ " WHERE cleanupDeadline IS NOT NULL"
|
||||||
|
+ " ORDER BY cleanupDeadline LIMIT 1";
|
||||||
|
s = txn.createStatement();
|
||||||
|
rs = s.executeQuery(sql);
|
||||||
|
long nextDeadline = NO_CLEANUP_DEADLINE;
|
||||||
|
if (rs.next()) {
|
||||||
|
nextDeadline = rs.getLong(1);
|
||||||
|
if (rs.next()) throw new AssertionError();
|
||||||
|
}
|
||||||
|
rs.close();
|
||||||
|
s.close();
|
||||||
|
return nextDeadline;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(rs, LOG, WARNING);
|
||||||
|
tryToClose(s, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PendingContact getPendingContact(Connection txn, PendingContactId p)
|
public PendingContact getPendingContact(Connection txn, PendingContactId p)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -2776,7 +2848,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void raiseSeenFlag(Connection txn, ContactId c, MessageId m)
|
public boolean raiseSeenFlag(Connection txn, ContactId c, MessageId m)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
@@ -2788,6 +2860,7 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
int affected = ps.executeUpdate();
|
int affected = ps.executeUpdate();
|
||||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
ps.close();
|
ps.close();
|
||||||
|
return affected == 1;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
tryToClose(ps, LOG, WARNING);
|
tryToClose(ps, LOG, WARNING);
|
||||||
throw new DbException(e);
|
throw new DbException(e);
|
||||||
@@ -3021,6 +3094,25 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCleanupTimerDuration(Connection txn, MessageId m,
|
||||||
|
long duration) throws DbException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "UPDATE messages SET cleanupTimerDuration = ?"
|
||||||
|
+ " WHERE messageId = ? AND cleanupTimerDuration IS NULL";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setLong(1, duration);
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(ps, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setContactVerified(Connection txn, ContactId c)
|
public void setContactVerified(Connection txn, ContactId c)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -3128,22 +3220,24 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setMessageShared(Connection txn, MessageId m)
|
public void setMessageShared(Connection txn, MessageId m, boolean shared)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
PreparedStatement ps = null;
|
PreparedStatement ps = null;
|
||||||
try {
|
try {
|
||||||
String sql = "UPDATE messages SET shared = TRUE"
|
String sql = "UPDATE messages SET shared = ?"
|
||||||
+ " WHERE messageId = ?";
|
+ " WHERE messageId = ?";
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setBytes(1, m.getBytes());
|
ps.setBoolean(1, shared);
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
int affected = ps.executeUpdate();
|
int affected = ps.executeUpdate();
|
||||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
ps.close();
|
ps.close();
|
||||||
// Update denormalised column in statuses
|
// Update denormalised column in statuses
|
||||||
sql = "UPDATE statuses SET messageShared = TRUE"
|
sql = "UPDATE statuses SET messageShared = ?"
|
||||||
+ " WHERE messageId = ?";
|
+ " WHERE messageId = ?";
|
||||||
ps = txn.prepareStatement(sql);
|
ps = txn.prepareStatement(sql);
|
||||||
ps.setBytes(1, m.getBytes());
|
ps.setBoolean(1, shared);
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
affected = ps.executeUpdate();
|
affected = ps.executeUpdate();
|
||||||
if (affected < 0) throw new DbStateException();
|
if (affected < 0) throw new DbStateException();
|
||||||
ps.close();
|
ps.close();
|
||||||
@@ -3272,6 +3366,60 @@ abstract class JdbcDatabase implements Database<Connection> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long startCleanupTimer(Connection txn, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
long now = clock.currentTimeMillis();
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
ResultSet rs = null;
|
||||||
|
try {
|
||||||
|
String sql = "UPDATE messages"
|
||||||
|
+ " SET cleanupDeadline = ? + cleanupTimerDuration"
|
||||||
|
+ " WHERE messageId = ?"
|
||||||
|
+ " AND cleanupTimerDuration IS NOT NULL"
|
||||||
|
+ " AND cleanupDeadline IS NULL";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setLong(1, now);
|
||||||
|
ps.setBytes(2, m.getBytes());
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
if (affected == 0) return TIMER_NOT_STARTED;
|
||||||
|
sql = "SELECT cleanupDeadline FROM messages WHERE messageId = ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
rs = ps.executeQuery();
|
||||||
|
if (!rs.next()) throw new DbStateException();
|
||||||
|
long deadline = rs.getLong(1);
|
||||||
|
if (rs.next()) throw new DbStateException();
|
||||||
|
rs.close();
|
||||||
|
ps.close();
|
||||||
|
return deadline;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(rs, LOG, WARNING);
|
||||||
|
tryToClose(ps, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopCleanupTimer(Connection txn, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
PreparedStatement ps = null;
|
||||||
|
try {
|
||||||
|
String sql = "UPDATE messages SET cleanupDeadline = NULL"
|
||||||
|
+ " WHERE messageId = ?";
|
||||||
|
ps = txn.prepareStatement(sql);
|
||||||
|
ps.setBytes(1, m.getBytes());
|
||||||
|
int affected = ps.executeUpdate();
|
||||||
|
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||||
|
ps.close();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(ps, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
|
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
|
||||||
int maxLatency) throws DbException {
|
int maxLatency) throws DbException {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.db.JdbcUtils.tryToClose;
|
||||||
|
|
||||||
|
class Migration47_48 implements Migration<Connection> {
|
||||||
|
|
||||||
|
private static final Logger LOG = getLogger(Migration47_48.class.getName());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getStartVersion() {
|
||||||
|
return 47;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getEndVersion() {
|
||||||
|
return 48;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(Connection txn) throws DbException {
|
||||||
|
Statement s = null;
|
||||||
|
try {
|
||||||
|
s = txn.createStatement();
|
||||||
|
// Null if no timer duration has been set
|
||||||
|
s.execute("ALTER TABLE messages"
|
||||||
|
+ " ADD COLUMN cleanupTimerDuration BIGINT");
|
||||||
|
// Null if no timer duration has been set or the timer
|
||||||
|
// hasn't started
|
||||||
|
s.execute("ALTER TABLE messages"
|
||||||
|
+ " ADD COLUMN cleanupDeadline BIGINT");
|
||||||
|
s.execute("CREATE INDEX IF NOT EXISTS messagesByCleanupDeadline"
|
||||||
|
+ " ON messages (cleanupDeadline)");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
tryToClose(s, LOG, WARNING);
|
||||||
|
throw new DbException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,5 @@ interface ClientVersioningConstants {
|
|||||||
// Metadata keys
|
// Metadata keys
|
||||||
String MSG_KEY_UPDATE_VERSION = "version";
|
String MSG_KEY_UPDATE_VERSION = "version";
|
||||||
String MSG_KEY_LOCAL = "local";
|
String MSG_KEY_LOCAL = "local";
|
||||||
String GROUP_KEY_CONTACT_ID = "contactId";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ import static java.util.Collections.emptyList;
|
|||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
|
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||||
|
|
||||||
@@ -161,13 +160,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
db.addGroup(txn, g);
|
db.addGroup(txn, g);
|
||||||
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
||||||
// Attach the contact ID to the group
|
// Attach the contact ID to the group
|
||||||
BdfDictionary meta = new BdfDictionary();
|
clientHelper.setContactId(txn, g.getId(), c.getId());
|
||||||
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
|
||||||
try {
|
|
||||||
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
|
||||||
} catch (FormatException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
// Create and store the first local update
|
// Create and store the first local update
|
||||||
List<ClientVersion> versions = new ArrayList<>(clients);
|
List<ClientVersion> versions = new ArrayList<>(clients);
|
||||||
Collections.sort(versions);
|
Collections.sort(versions);
|
||||||
@@ -229,7 +222,7 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
Map<ClientMajorVersion, Visibility> after =
|
Map<ClientMajorVersion, Visibility> after =
|
||||||
getVisibilities(newLocalStates, newRemoteStates);
|
getVisibilities(newLocalStates, newRemoteStates);
|
||||||
// Call hooks for any visibilities that have changed
|
// Call hooks for any visibilities that have changed
|
||||||
ContactId c = getContactId(txn, m.getGroupId());
|
ContactId c = clientHelper.getContactId(txn, m.getGroupId());
|
||||||
if (!before.equals(after)) {
|
if (!before.equals(after)) {
|
||||||
Contact contact = db.getContact(txn, c);
|
Contact contact = db.getContact(txn, c);
|
||||||
callVisibilityHooks(txn, contact, before, after);
|
callVisibilityHooks(txn, contact, before, after);
|
||||||
@@ -521,17 +514,6 @@ class ClientVersioningManagerImpl implements ClientVersioningManager,
|
|||||||
storeUpdate(txn, g, states, 1);
|
storeUpdate(txn, g, states, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ContactId getContactId(Transaction txn, GroupId g)
|
|
||||||
throws DbException {
|
|
||||||
try {
|
|
||||||
BdfDictionary meta =
|
|
||||||
clientHelper.getGroupMetadataAsDictionary(txn, g);
|
|
||||||
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
|
||||||
} catch (FormatException e) {
|
|
||||||
throw new DbException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ClientState> updateStatesFromRemoteStates(
|
private List<ClientState> updateStatesFromRemoteStates(
|
||||||
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
|
List<ClientState> oldLocalStates, List<ClientState> remoteStates) {
|
||||||
Set<ClientMajorVersion> remoteSet = new HashSet<>();
|
Set<ClientMajorVersion> remoteSet = new HashSet<>();
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1
|
Bridge obfs4 192.95.36.142:443 CDF2E852BF539B82BD10E27E9115A31734E378C2 cert=qUVQ0srL1JI/vO6V6m/24anYXiJD3QP2HgzUKQtQ7GRqqUvs7P+tG43RtAqdhLOALP7DJQ iat-mode=1
|
||||||
Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
|
Bridge obfs4 38.229.1.78:80 C8CBDB2464FC9804A69531437BCF2BE31FDD2EE4 cert=Hmyfd2ev46gGY7NoVxA9ngrPF2zCZtzskRTzoWXbxNkzeVnGFPWmrTtILRyqCTjHR+s9dg iat-mode=1
|
||||||
Bridge obfs4 38.229.33.83:80 0BAC39417268B96B9F514E7F63FA6FBA1A788955 cert=VwEFpk9F/UN9JED7XpG1XOjm/O8ZCXK80oPecgWnNDZDv5pdkhq1OpbAH0wNqOT6H6BmRQ iat-mode=1
|
|
||||||
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
|
Bridge obfs4 37.218.245.14:38224 D9A82D2F9C2F65A18407B1D2B764F130847F8B5D cert=bjRaMrr1BRiAW8IE9U5z27fQaYgOhX1UCmOpg2pFpoMvo6ZgQMzLsaTzzQNTlm7hNcb+Sg iat-mode=0
|
||||||
|
Bridge obfs4 85.31.186.98:443 011F2599C0E9B27EE74B353155E244813763C3E5 cert=ayq0XzCwhpdysn5o0EyDUbmSOx3X/oTEbzDMvczHOdBJKlvIdHHLJGkZARtT4dcBFArPPg iat-mode=0
|
||||||
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
|
Bridge obfs4 85.31.186.26:443 91A6354697E6B02A386312F68D82CF86824D3606 cert=PBwr+S8JTVZo6MPdHnkTwXJPILWADLqfMGoVvhZClMq/Urndyd42BwX9YFJHZnBB3H0XCw iat-mode=0
|
||||||
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
|
Bridge obfs4 193.11.166.194:27015 2D82C2E354D531A68469ADF7F878FA6060C6BACA cert=4TLQPJrTSaDffMK7Nbao6LC7G9OW/NHkUwIdjLSS3KYf0Nv4/nQiiI8dY2TcsQx01NniOg iat-mode=0
|
||||||
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
|
Bridge obfs4 193.11.166.194:27020 86AC7B8D430DAC4117E9F42C9EAED18133863AAF cert=0LDeJH4JzMDtkJJrFphJCiPqKx7loozKN7VNfuukMGfHO0Z8OGdzHVkhVAOfo1mUdv9cMg iat-mode=0
|
||||||
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
|
Bridge obfs4 193.11.166.194:27025 1AE2C08904527FEA90C4C4F8C1083EA59FBC6FAF cert=ItvYZzW5tn6v3G4UnQa6Qz04Npro6e81AP70YujmK/KXwDFPTs3aHXcHp4n8Vt6w/bv8cA iat-mode=0
|
||||||
Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
|
Bridge obfs4 209.148.46.65:443 74FAD13168806246602538555B5521A0383A1875 cert=ssH+9rP8dG2NLDN2XuFw63hIO/9MNNinLmxQDpVa+7kTOa9/m+tGWT1SmSYpQ9uTBGa6Hw iat-mode=0
|
||||||
|
Bridge obfs4 146.57.248.225:22 10A6CD36A537FCE513A322361547444B393989F0 cert=K1gDtDAIcUfeLqbstggjIw2rtgIKqdIhUlHp82XRqNSq/mtAjp1BIC9vHKJ2FAEpGssTPw iat-mode=0
|
||||||
Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0
|
Bridge obfs4 45.145.95.6:27015 C5B7CD6946FF10C5B3E89691A7D3F2C122D2117C cert=TD7PbUO0/0k6xYHMPW3vJxICfkMZNdkRrb63Zhl5j9dW3iRGiCx0A7mPhe5T2EDzQ35+Zw iat-mode=0
|
||||||
Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
|
Bridge obfs4 51.222.13.177:80 5EDAC3B810E12B01F6FD8050D2FD3E277B289A08 cert=2uplIpLQ0q9+0qMFrK5pkaYRDOe460LL9WHBvatgkuRr/SL31wBOEupaMMJ6koRE6Ld0ew iat-mode=0
|
||||||
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0
|
Bridge obfs4 78.46.188.239:37356 5A2D2F4158D0453E00C7C176978D3F41D69C45DB cert=3c0SwxpOisbohNxEc4tb875RVW8eOu1opRTVXJhafaKA/PNNtI7ElQIVOVZg1AdL5bxGCw iat-mode=0
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package org.briarproject.bramble;
|
package org.briarproject.bramble;
|
||||||
|
|
||||||
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
|
import org.briarproject.bramble.system.TimeTravelModule;
|
||||||
|
|
||||||
public interface BrambleCoreIntegrationTestEagerSingletons
|
public interface BrambleCoreIntegrationTestEagerSingletons
|
||||||
extends BrambleCoreEagerSingletons {
|
extends BrambleCoreEagerSingletons {
|
||||||
|
|
||||||
void inject(DefaultTaskSchedulerModule.EagerSingletons init);
|
void inject(TimeTravelModule.EagerSingletons init);
|
||||||
|
|
||||||
class Helper {
|
class Helper {
|
||||||
|
|
||||||
public static void injectEagerSingletons(
|
public static void injectEagerSingletons(
|
||||||
BrambleCoreIntegrationTestEagerSingletons c) {
|
BrambleCoreIntegrationTestEagerSingletons c) {
|
||||||
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
|
BrambleCoreEagerSingletons.Helper.injectEagerSingletons(c);
|
||||||
c.inject(new DefaultTaskSchedulerModule.EagerSingletons());
|
c.inject(new TimeTravelModule.EagerSingletons());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.bramble.db;
|
package org.briarproject.bramble.db;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.cleanup.event.CleanupTimerStartedEvent;
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.contact.PendingContactId;
|
import org.briarproject.bramble.api.contact.PendingContactId;
|
||||||
@@ -69,6 +70,8 @@ import static java.util.Arrays.asList;
|
|||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Collections.emptyMap;
|
import static java.util.Collections.emptyMap;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
|
import static java.util.concurrent.TimeUnit.HOURS;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE;
|
||||||
@@ -510,11 +513,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
throws Exception {
|
throws Exception {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
// Check whether the group is in the DB (which it's not)
|
// Check whether the group is in the DB (which it's not)
|
||||||
exactly(8).of(database).startTransaction();
|
exactly(10).of(database).startTransaction();
|
||||||
will(returnValue(txn));
|
will(returnValue(txn));
|
||||||
exactly(8).of(database).containsGroup(txn, groupId);
|
exactly(10).of(database).containsGroup(txn, groupId);
|
||||||
will(returnValue(false));
|
will(returnValue(false));
|
||||||
exactly(8).of(database).abortTransaction(txn);
|
exactly(10).of(database).abortTransaction(txn);
|
||||||
// Allow other checks to pass
|
// Allow other checks to pass
|
||||||
allowing(database).containsContact(txn, contactId);
|
allowing(database).containsContact(txn, contactId);
|
||||||
will(returnValue(true));
|
will(returnValue(true));
|
||||||
@@ -523,7 +526,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
eventExecutor, shutdownManager);
|
eventExecutor, shutdownManager);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getGroup(transaction, groupId));
|
db.getGroup(transaction, groupId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchGroupException expected) {
|
} catch (NoSuchGroupException expected) {
|
||||||
@@ -531,7 +534,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getGroupMetadata(transaction, groupId));
|
db.getGroupMetadata(transaction, groupId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchGroupException expected) {
|
} catch (NoSuchGroupException expected) {
|
||||||
@@ -539,7 +542,23 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
|
db.getMessageIds(transaction, groupId));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchGroupException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(true, transaction ->
|
||||||
|
db.getMessageIds(transaction, groupId, new Metadata()));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchGroupException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(true, transaction ->
|
||||||
db.getMessageMetadata(transaction, groupId));
|
db.getMessageMetadata(transaction, groupId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchGroupException expected) {
|
} catch (NoSuchGroupException expected) {
|
||||||
@@ -547,7 +566,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageMetadata(transaction, groupId,
|
db.getMessageMetadata(transaction, groupId,
|
||||||
new Metadata()));
|
new Metadata()));
|
||||||
fail();
|
fail();
|
||||||
@@ -556,7 +575,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageStatus(transaction, contactId, groupId));
|
db.getMessageStatus(transaction, contactId, groupId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchGroupException expected) {
|
} catch (NoSuchGroupException expected) {
|
||||||
@@ -594,11 +613,11 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
throws Exception {
|
throws Exception {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
// Check whether the message is in the DB (which it's not)
|
// Check whether the message is in the DB (which it's not)
|
||||||
exactly(12).of(database).startTransaction();
|
exactly(15).of(database).startTransaction();
|
||||||
will(returnValue(txn));
|
will(returnValue(txn));
|
||||||
exactly(12).of(database).containsMessage(txn, messageId);
|
exactly(15).of(database).containsMessage(txn, messageId);
|
||||||
will(returnValue(false));
|
will(returnValue(false));
|
||||||
exactly(12).of(database).abortTransaction(txn);
|
exactly(15).of(database).abortTransaction(txn);
|
||||||
// Allow other checks to pass
|
// Allow other checks to pass
|
||||||
allowing(database).containsContact(txn, contactId);
|
allowing(database).containsContact(txn, contactId);
|
||||||
will(returnValue(true));
|
will(returnValue(true));
|
||||||
@@ -623,7 +642,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessage(transaction, messageId));
|
db.getMessage(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -631,7 +650,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageMetadata(transaction, messageId));
|
db.getMessageMetadata(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -639,7 +658,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageState(transaction, messageId));
|
db.getMessageState(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -647,7 +666,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageStatus(transaction, contactId, messageId));
|
db.getMessageStatus(transaction, contactId, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -662,6 +681,15 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
// Expected
|
// Expected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(false, transaction ->
|
||||||
|
db.setCleanupTimerDuration(transaction, message.getId(),
|
||||||
|
HOURS.toMillis(1)));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchMessageException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(false, transaction ->
|
||||||
db.setMessagePermanent(transaction, message.getId()));
|
db.setMessagePermanent(transaction, message.getId()));
|
||||||
@@ -687,7 +715,7 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageDependencies(transaction, messageId));
|
db.getMessageDependencies(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
@@ -695,12 +723,28 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
db.transaction(false, transaction ->
|
db.transaction(true, transaction ->
|
||||||
db.getMessageDependents(transaction, messageId));
|
db.getMessageDependents(transaction, messageId));
|
||||||
fail();
|
fail();
|
||||||
} catch (NoSuchMessageException expected) {
|
} catch (NoSuchMessageException expected) {
|
||||||
// Expected
|
// Expected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(false, transaction ->
|
||||||
|
db.startCleanupTimer(transaction, messageId));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchMessageException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(false, transaction ->
|
||||||
|
db.stopCleanupTimer(transaction, messageId));
|
||||||
|
fail();
|
||||||
|
} catch (NoSuchMessageException expected) {
|
||||||
|
// Expected
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -981,6 +1025,9 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
|
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
|
||||||
will(returnValue(true));
|
will(returnValue(true));
|
||||||
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
|
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).startCleanupTimer(txn, messageId);
|
||||||
|
will(returnValue(TIMER_NOT_STARTED)); // No cleanup duration was set
|
||||||
oneOf(database).commitTransaction(txn);
|
oneOf(database).commitTransaction(txn);
|
||||||
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
|
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
|
||||||
}});
|
}});
|
||||||
@@ -993,6 +1040,56 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReceiveDuplicateAck() throws Exception {
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
oneOf(database).startTransaction();
|
||||||
|
will(returnValue(txn));
|
||||||
|
oneOf(database).containsContact(txn, contactId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
|
||||||
|
will(returnValue(false)); // Already acked
|
||||||
|
oneOf(database).commitTransaction(txn);
|
||||||
|
}});
|
||||||
|
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||||
|
eventExecutor, shutdownManager);
|
||||||
|
|
||||||
|
db.transaction(false, transaction -> {
|
||||||
|
Ack a = new Ack(singletonList(messageId));
|
||||||
|
db.receiveAck(transaction, contactId, a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testReceiveAckWithCleanupTimer() throws Exception {
|
||||||
|
long deadline = System.currentTimeMillis();
|
||||||
|
context.checking(new Expectations() {{
|
||||||
|
oneOf(database).startTransaction();
|
||||||
|
will(returnValue(txn));
|
||||||
|
oneOf(database).containsContact(txn, contactId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).containsVisibleMessage(txn, contactId, messageId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).raiseSeenFlag(txn, contactId, messageId);
|
||||||
|
will(returnValue(true));
|
||||||
|
oneOf(database).startCleanupTimer(txn, messageId);
|
||||||
|
will(returnValue(deadline));
|
||||||
|
oneOf(database).commitTransaction(txn);
|
||||||
|
oneOf(eventBus).broadcast(with(any(
|
||||||
|
CleanupTimerStartedEvent.class)));
|
||||||
|
oneOf(eventBus).broadcast(with(any(MessagesAckedEvent.class)));
|
||||||
|
}});
|
||||||
|
DatabaseComponent db = createDatabaseComponent(database, eventBus,
|
||||||
|
eventExecutor, shutdownManager);
|
||||||
|
|
||||||
|
db.transaction(false, transaction -> {
|
||||||
|
Ack a = new Ack(singletonList(messageId));
|
||||||
|
db.receiveAck(transaction, contactId, a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReceiveMessage() throws Exception {
|
public void testReceiveMessage() throws Exception {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
|
|||||||
@@ -53,10 +53,11 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
|
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Collections.emptyList;
|
import static java.util.Collections.emptyList;
|
||||||
import static java.util.Collections.emptyMap;
|
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static java.util.Collections.singletonMap;
|
import static java.util.Collections.singletonMap;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.NO_CLEANUP_DEADLINE;
|
||||||
|
import static org.briarproject.bramble.api.db.DatabaseComponent.TIMER_NOT_STARTED;
|
||||||
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
import static org.briarproject.bramble.api.db.Metadata.REMOVE;
|
||||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||||
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE;
|
||||||
@@ -351,7 +352,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
assertTrue(ids.isEmpty());
|
assertTrue(ids.isEmpty());
|
||||||
|
|
||||||
// Sharing the message should make it sendable
|
// Sharing the message should make it sendable
|
||||||
db.setMessageShared(txn, messageId);
|
db.setMessageShared(txn, messageId, true);
|
||||||
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
|
ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE, MAX_LATENCY);
|
||||||
assertEquals(singletonList(messageId), ids);
|
assertEquals(singletonList(messageId), ids);
|
||||||
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
|
ids = db.getMessagesToOffer(txn, contactId, 100, MAX_LATENCY);
|
||||||
@@ -631,8 +632,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
|
|
||||||
// The group should not be visible to the contact
|
// The group should not be visible to the contact
|
||||||
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
||||||
assertEquals(emptyMap(),
|
assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
|
||||||
db.getGroupVisibility(txn, groupId));
|
|
||||||
|
|
||||||
// Make the group visible to the contact
|
// Make the group visible to the contact
|
||||||
db.addGroupVisibility(txn, contactId, groupId, false);
|
db.addGroupVisibility(txn, contactId, groupId, false);
|
||||||
@@ -655,8 +655,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
// Make the group invisible again
|
// Make the group invisible again
|
||||||
db.removeGroupVisibility(txn, contactId, groupId);
|
db.removeGroupVisibility(txn, contactId, groupId);
|
||||||
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId));
|
||||||
assertEquals(emptyMap(),
|
assertTrue(db.getGroupVisibility(txn, groupId).isEmpty());
|
||||||
db.getGroupVisibility(txn, groupId));
|
|
||||||
|
|
||||||
db.commitTransaction(txn);
|
db.commitTransaction(txn);
|
||||||
db.close();
|
db.close();
|
||||||
@@ -2040,7 +2039,7 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
assertEquals(Long.MAX_VALUE, db.getNextSendTime(txn, contactId));
|
||||||
|
|
||||||
// Share the message - now it should be sendable immediately
|
// Share the message - now it should be sendable immediately
|
||||||
db.setMessageShared(txn, messageId);
|
db.setMessageShared(txn, messageId, true);
|
||||||
assertEquals(0, db.getNextSendTime(txn, contactId));
|
assertEquals(0, db.getNextSendTime(txn, contactId));
|
||||||
|
|
||||||
// Mark the message as requested - it should still be sendable
|
// Mark the message as requested - it should still be sendable
|
||||||
@@ -2347,6 +2346,87 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
|
|||||||
db.close();
|
db.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCleanupTimer() throws Exception {
|
||||||
|
long duration = 60_000;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
AtomicLong time = new AtomicLong(now);
|
||||||
|
Database<Connection> db =
|
||||||
|
open(false, new TestMessageFactory(), new SettableClock(time));
|
||||||
|
Connection txn = db.startTransaction();
|
||||||
|
|
||||||
|
// No messages should be due or scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Add a group and a message
|
||||||
|
db.addGroup(txn, group);
|
||||||
|
db.addMessage(txn, message, DELIVERED, false, false, null);
|
||||||
|
|
||||||
|
// No messages should be due or scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Set the message's cleanup timer duration
|
||||||
|
db.setCleanupTimerDuration(txn, messageId, duration);
|
||||||
|
|
||||||
|
// No messages should be due or scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Start the message's cleanup timer
|
||||||
|
assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
|
||||||
|
|
||||||
|
// The timer can't be started again
|
||||||
|
assertEquals(TIMER_NOT_STARTED, db.startCleanupTimer(txn, messageId));
|
||||||
|
|
||||||
|
// No messages should be due for deletion, but the message should be
|
||||||
|
// scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Stop the timer
|
||||||
|
db.stopCleanupTimer(txn, messageId);
|
||||||
|
|
||||||
|
// No messages should be due or scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Start the timer again
|
||||||
|
assertEquals(now + duration, db.startCleanupTimer(txn, messageId));
|
||||||
|
|
||||||
|
// No messages should be due for deletion, but the message should be
|
||||||
|
// scheduled for deletion
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// 1 ms before the timer expires, no messages should be due for
|
||||||
|
// deletion but the message should be scheduled for deletion
|
||||||
|
time.set(now + duration - 1);
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// When the timer expires, the message should be due and scheduled for
|
||||||
|
// deletion
|
||||||
|
time.set(now + duration);
|
||||||
|
assertEquals(singletonMap(groupId, singletonList(messageId)),
|
||||||
|
db.getMessagesToDelete(txn));
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// 1 ms after the timer expires, the message should be due and
|
||||||
|
// scheduled for deletion
|
||||||
|
time.set(now + duration + 1);
|
||||||
|
assertEquals(singletonMap(groupId, singletonList(messageId)),
|
||||||
|
db.getMessagesToDelete(txn));
|
||||||
|
assertEquals(now + duration, db.getNextCleanupDeadline(txn));
|
||||||
|
|
||||||
|
// Once the message has been deleted, it should no longer be due
|
||||||
|
// or scheduled for deletion
|
||||||
|
db.deleteMessage(txn, messageId);
|
||||||
|
assertTrue(db.getMessagesToDelete(txn).isEmpty());
|
||||||
|
assertEquals(NO_CLEANUP_DEADLINE, db.getNextCleanupDeadline(txn));
|
||||||
|
}
|
||||||
|
|
||||||
private Database<Connection> open(boolean resume) throws Exception {
|
private Database<Connection> open(boolean resume) throws Exception {
|
||||||
return open(resume, new TestMessageFactory(), new SystemClock());
|
return open(resume, new TestMessageFactory(), new SystemClock());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package org.briarproject.bramble.system;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
|
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||||
|
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.PriorityBlockingQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link TaskScheduler} for use in tests. The scheduler keeps all scheduled
|
||||||
|
* tasks in a queue until {@link #runTasks()} is called.
|
||||||
|
*/
|
||||||
|
@NotNullByDefault
|
||||||
|
class TestTaskScheduler implements TaskScheduler {
|
||||||
|
|
||||||
|
private final Queue<Task> queue = new PriorityBlockingQueue<>();
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
TestTaskScheduler(Clock clock) {
|
||||||
|
this.clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cancellable schedule(Runnable task, Executor executor, long delay,
|
||||||
|
TimeUnit unit) {
|
||||||
|
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||||
|
return schedule(task, executor, delay, unit, cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
|
||||||
|
long delay, long interval, TimeUnit unit) {
|
||||||
|
AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||||
|
return scheduleWithFixedDelay(task, executor, delay, interval, unit,
|
||||||
|
cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cancellable schedule(Runnable task, Executor executor, long delay,
|
||||||
|
TimeUnit unit, AtomicBoolean cancelled) {
|
||||||
|
long delayMillis = MILLISECONDS.convert(delay, unit);
|
||||||
|
long dueMillis = clock.currentTimeMillis() + delayMillis;
|
||||||
|
Task t = new Task(task, executor, dueMillis, cancelled);
|
||||||
|
queue.add(t);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cancellable scheduleWithFixedDelay(Runnable task, Executor executor,
|
||||||
|
long delay, long interval, TimeUnit unit, AtomicBoolean cancelled) {
|
||||||
|
// All executions of this periodic task share a cancelled flag
|
||||||
|
Runnable wrapped = () -> {
|
||||||
|
task.run();
|
||||||
|
scheduleWithFixedDelay(task, executor, interval, interval, unit,
|
||||||
|
cancelled);
|
||||||
|
};
|
||||||
|
return schedule(wrapped, executor, delay, unit, cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs any scheduled tasks that are due.
|
||||||
|
*/
|
||||||
|
void runTasks() throws InterruptedException {
|
||||||
|
long now = clock.currentTimeMillis();
|
||||||
|
while (true) {
|
||||||
|
Task t = queue.peek();
|
||||||
|
if (t == null || t.dueMillis > now) return;
|
||||||
|
t = queue.poll();
|
||||||
|
// Submit the task to its executor and wait for it to finish
|
||||||
|
if (!t.run().await(1, MINUTES)) fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Task
|
||||||
|
implements Cancellable, Comparable<Task> {
|
||||||
|
|
||||||
|
private final Runnable task;
|
||||||
|
private final Executor executor;
|
||||||
|
private final long dueMillis;
|
||||||
|
private final AtomicBoolean cancelled;
|
||||||
|
|
||||||
|
private Task(Runnable task, Executor executor, long dueMillis,
|
||||||
|
AtomicBoolean cancelled) {
|
||||||
|
this.task = task;
|
||||||
|
this.executor = executor;
|
||||||
|
this.dueMillis = dueMillis;
|
||||||
|
this.cancelled = cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("UseCompareMethod") // Animal Sniffer
|
||||||
|
@Override
|
||||||
|
public int compareTo(Task task) {
|
||||||
|
return Long.valueOf(dueMillis).compareTo(task.dueMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submits the task to its executor and returns a latch that will be
|
||||||
|
* released when the task finishes.
|
||||||
|
*/
|
||||||
|
public CountDownLatch run() {
|
||||||
|
if (cancelled.get()) return new CountDownLatch(0);
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
executor.execute(() -> {
|
||||||
|
task.run();
|
||||||
|
latch.countDown();
|
||||||
|
});
|
||||||
|
return latch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancel() {
|
||||||
|
cancelled.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package org.briarproject.bramble.system;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
|
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||||
|
import org.briarproject.bramble.test.SettableClock;
|
||||||
|
import org.briarproject.bramble.test.TimeTravel;
|
||||||
|
|
||||||
|
import java.util.concurrent.RejectedExecutionHandler;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
|
||||||
|
@Module
|
||||||
|
public class TimeTravelModule {
|
||||||
|
|
||||||
|
public static class EagerSingletons {
|
||||||
|
@Inject
|
||||||
|
TaskScheduler scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduledExecutorService;
|
||||||
|
private final Clock clock;
|
||||||
|
private final TaskScheduler taskScheduler;
|
||||||
|
private final TimeTravel timeTravel;
|
||||||
|
|
||||||
|
public TimeTravelModule() {
|
||||||
|
this(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeTravelModule(boolean travel) {
|
||||||
|
// Discard tasks that are submitted during shutdown
|
||||||
|
RejectedExecutionHandler policy =
|
||||||
|
new ScheduledThreadPoolExecutor.DiscardPolicy();
|
||||||
|
scheduledExecutorService =
|
||||||
|
new ScheduledThreadPoolExecutor(1, policy);
|
||||||
|
if (travel) {
|
||||||
|
// Use a SettableClock and TestTaskScheduler to allow time travel
|
||||||
|
AtomicLong time = new AtomicLong(System.currentTimeMillis());
|
||||||
|
clock = new SettableClock(time);
|
||||||
|
TestTaskScheduler testTaskScheduler = new TestTaskScheduler(clock);
|
||||||
|
taskScheduler = testTaskScheduler;
|
||||||
|
timeTravel = new TimeTravel() {
|
||||||
|
@Override
|
||||||
|
public void setCurrentTimeMillis(long now)
|
||||||
|
throws InterruptedException {
|
||||||
|
time.set(now);
|
||||||
|
testTaskScheduler.runTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCurrentTimeMillis(long add)
|
||||||
|
throws InterruptedException {
|
||||||
|
time.addAndGet(add);
|
||||||
|
testTaskScheduler.runTasks();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Use the default clock and task scheduler
|
||||||
|
clock = new SystemClock();
|
||||||
|
taskScheduler = new TaskSchedulerImpl(scheduledExecutorService);
|
||||||
|
timeTravel = new TimeTravel() {
|
||||||
|
@Override
|
||||||
|
public void setCurrentTimeMillis(long now) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCurrentTimeMillis(long add) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
Clock provideClock() {
|
||||||
|
return clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
TaskScheduler provideTaskScheduler(LifecycleManager lifecycleManager) {
|
||||||
|
lifecycleManager.registerForShutdown(scheduledExecutorService);
|
||||||
|
return taskScheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
TimeTravel provideTimeTravel() {
|
||||||
|
return timeTravel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ package org.briarproject.bramble.test;
|
|||||||
import org.briarproject.bramble.api.FeatureFlags;
|
import org.briarproject.bramble.api.FeatureFlags;
|
||||||
import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
|
import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
|
||||||
import org.briarproject.bramble.event.DefaultEventExecutorModule;
|
import org.briarproject.bramble.event.DefaultEventExecutorModule;
|
||||||
import org.briarproject.bramble.system.DefaultTaskSchedulerModule;
|
|
||||||
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
|
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
|
||||||
|
import org.briarproject.bramble.system.TimeTravelModule;
|
||||||
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
@@ -12,11 +12,11 @@ import dagger.Provides;
|
|||||||
@Module(includes = {
|
@Module(includes = {
|
||||||
DefaultBatteryManagerModule.class,
|
DefaultBatteryManagerModule.class,
|
||||||
DefaultEventExecutorModule.class,
|
DefaultEventExecutorModule.class,
|
||||||
DefaultTaskSchedulerModule.class,
|
|
||||||
DefaultWakefulIoExecutorModule.class,
|
DefaultWakefulIoExecutorModule.class,
|
||||||
TestDatabaseConfigModule.class,
|
TestDatabaseConfigModule.class,
|
||||||
TestPluginConfigModule.class,
|
TestPluginConfigModule.class,
|
||||||
TestSecureRandomModule.class
|
TestSecureRandomModule.class,
|
||||||
|
TimeTravelModule.class
|
||||||
})
|
})
|
||||||
public class BrambleCoreIntegrationTestModule {
|
public class BrambleCoreIntegrationTestModule {
|
||||||
|
|
||||||
@@ -33,6 +33,11 @@ public class BrambleCoreIntegrationTestModule {
|
|||||||
public boolean shouldEnableProfilePictures() {
|
public boolean shouldEnableProfilePictures() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldEnableDisappearingMessages() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import static org.briarproject.bramble.test.TestUtils.getContact;
|
|||||||
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
||||||
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
||||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.GROUP_KEY_CONTACT_ID;
|
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_LOCAL;
|
||||||
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
import static org.briarproject.bramble.versioning.ClientVersioningConstants.MSG_KEY_UPDATE_VERSION;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
@@ -60,8 +59,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
private final ClientId clientId = getClientId();
|
private final ClientId clientId = getClientId();
|
||||||
private final long now = System.currentTimeMillis();
|
private final long now = System.currentTimeMillis();
|
||||||
private final Transaction txn = new Transaction(null, false);
|
private final Transaction txn = new Transaction(null, false);
|
||||||
private final BdfDictionary groupMeta = BdfDictionary.of(
|
|
||||||
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
|
||||||
|
|
||||||
private ClientVersioningManagerImpl createInstance() {
|
private ClientVersioningManagerImpl createInstance() {
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
@@ -123,8 +120,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(db).addGroup(txn, contactGroup);
|
oneOf(db).addGroup(txn, contactGroup);
|
||||||
oneOf(db).setGroupVisibility(txn, contact.getId(),
|
oneOf(db).setGroupVisibility(txn, contact.getId(),
|
||||||
contactGroup.getId(), SHARED);
|
contactGroup.getId(), SHARED);
|
||||||
oneOf(clientHelper).mergeGroupMetadata(txn, contactGroup.getId(),
|
oneOf(clientHelper).setContactId(txn, contactGroup.getId(),
|
||||||
groupMeta);
|
contact.getId());
|
||||||
oneOf(clock).currentTimeMillis();
|
oneOf(clock).currentTimeMillis();
|
||||||
will(returnValue(now));
|
will(returnValue(now));
|
||||||
oneOf(clientHelper).createMessage(contactGroup.getId(), now,
|
oneOf(clientHelper).createMessage(contactGroup.getId(), now,
|
||||||
@@ -460,9 +457,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
|
oneOf(db).deleteMessage(txn, oldRemoteUpdateId);
|
||||||
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
|
oneOf(db).deleteMessageMetadata(txn, oldRemoteUpdateId);
|
||||||
// Get contact ID
|
// Get contact ID
|
||||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
||||||
contactGroup.getId());
|
will(returnValue(contact.getId()));
|
||||||
will(returnValue(groupMeta));
|
|
||||||
// No states or visibilities have changed
|
// No states or visibilities have changed
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -492,10 +488,9 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
// Load the latest local update
|
// Load the latest local update
|
||||||
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
|
oneOf(clientHelper).getMessageAsList(txn, oldLocalUpdateId);
|
||||||
will(returnValue(oldLocalUpdateBody));
|
will(returnValue(oldLocalUpdateBody));
|
||||||
// Get client ID
|
// Get contact ID
|
||||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
||||||
contactGroup.getId());
|
will(returnValue(contact.getId()));
|
||||||
will(returnValue(groupMeta));
|
|
||||||
// No states or visibilities have changed
|
// No states or visibilities have changed
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@@ -546,8 +541,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
||||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
||||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||||
BdfDictionary groupMeta = BdfDictionary.of(
|
|
||||||
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(clientHelper).toList(newRemoteUpdate);
|
oneOf(clientHelper).toList(newRemoteUpdate);
|
||||||
@@ -577,9 +570,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
||||||
newLocalUpdateMeta, true, false);
|
newLocalUpdateMeta, true, false);
|
||||||
// The client's visibility has changed
|
// The client's visibility has changed
|
||||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
||||||
contactGroup.getId());
|
will(returnValue(contact.getId()));
|
||||||
will(returnValue(groupMeta));
|
|
||||||
oneOf(db).getContact(txn, contact.getId());
|
oneOf(db).getContact(txn, contact.getId());
|
||||||
will(returnValue(contact));
|
will(returnValue(contact));
|
||||||
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
|
oneOf(hook).onClientVisibilityChanging(txn, contact, visibility);
|
||||||
@@ -619,8 +611,6 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
BdfDictionary newLocalUpdateMeta = BdfDictionary.of(
|
||||||
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
new BdfEntry(MSG_KEY_UPDATE_VERSION, 2L),
|
||||||
new BdfEntry(MSG_KEY_LOCAL, true));
|
new BdfEntry(MSG_KEY_LOCAL, true));
|
||||||
BdfDictionary groupMeta = BdfDictionary.of(
|
|
||||||
new BdfEntry(GROUP_KEY_CONTACT_ID, contact.getId().getInt()));
|
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(clientHelper).toList(newRemoteUpdate);
|
oneOf(clientHelper).toList(newRemoteUpdate);
|
||||||
@@ -650,9 +640,8 @@ public class ClientVersioningManagerImplTest extends BrambleMockTestCase {
|
|||||||
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
oneOf(clientHelper).addLocalMessage(txn, newLocalUpdate,
|
||||||
newLocalUpdateMeta, true, false);
|
newLocalUpdateMeta, true, false);
|
||||||
// The client's visibility has changed
|
// The client's visibility has changed
|
||||||
oneOf(clientHelper).getGroupMetadataAsDictionary(txn,
|
oneOf(clientHelper).getContactId(txn, contactGroup.getId());
|
||||||
contactGroup.getId());
|
will(returnValue(contact.getId()));
|
||||||
will(returnValue(groupMeta));
|
|
||||||
oneOf(db).getContact(txn, contact.getId());
|
oneOf(db).getContact(txn, contact.getId());
|
||||||
will(returnValue(contact));
|
will(returnValue(contact));
|
||||||
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);
|
oneOf(hook).onClientVisibilityChanging(txn, contact, INVISIBLE);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ dependencies {
|
|||||||
implementation fileTree(dir: 'libs', include: '*.jar')
|
implementation fileTree(dir: 'libs', include: '*.jar')
|
||||||
implementation 'net.java.dev.jna:jna:4.5.2'
|
implementation 'net.java.dev.jna:jna:4.5.2'
|
||||||
implementation 'net.java.dev.jna:jna-platform:4.5.2'
|
implementation 'net.java.dev.jna:jna-platform:4.5.2'
|
||||||
tor 'org.briarproject:tor:0.3.5.13-1@zip'
|
tor 'org.briarproject:tor:0.3.5.13@zip'
|
||||||
tor 'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a@zip'
|
tor 'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a@zip'
|
||||||
|
|
||||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'
|
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import org.briarproject.bramble.system.JavaSystemModule;
|
|||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
|
|
||||||
@Module(includes = {
|
@Module(includes = {
|
||||||
|
CircumventionModule.class,
|
||||||
JavaNetworkModule.class,
|
JavaNetworkModule.class,
|
||||||
JavaSystemModule.class,
|
JavaSystemModule.class,
|
||||||
CircumventionModule.class,
|
|
||||||
SocksModule.class
|
SocksModule.class
|
||||||
})
|
})
|
||||||
public class BrambleJavaModule {
|
public class BrambleJavaModule {
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import javax.annotation.concurrent.Immutable;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.net.SocketFactory;
|
import javax.net.SocketFactory;
|
||||||
|
|
||||||
import static java.util.logging.Level.INFO;
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.util.OsUtils.isLinux;
|
import static org.briarproject.bramble.util.OsUtils.isLinux;
|
||||||
|
|
||||||
@@ -97,15 +96,8 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
|
|||||||
String architecture = null;
|
String architecture = null;
|
||||||
if (isLinux()) {
|
if (isLinux()) {
|
||||||
String arch = System.getProperty("os.arch");
|
String arch = System.getProperty("os.arch");
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("System's os.arch is " + arch);
|
|
||||||
}
|
|
||||||
if (arch.equals("amd64")) {
|
if (arch.equals("amd64")) {
|
||||||
architecture = "linux-x86_64";
|
architecture = "linux-x86_64";
|
||||||
} else if (arch.equals("aarch64")) {
|
|
||||||
architecture = "linux-aarch64";
|
|
||||||
} else if (arch.equals("arm")) {
|
|
||||||
architecture = "linux-armhf";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (architecture == null) {
|
if (architecture == null) {
|
||||||
@@ -113,10 +105,6 @@ public class UnixTorPluginFactory implements DuplexPluginFactory {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("The selected architecture for Tor is " + architecture);
|
|
||||||
}
|
|
||||||
|
|
||||||
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
|
Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
|
||||||
MAX_POLLING_INTERVAL, BACKOFF_BASE);
|
MAX_POLLING_INTERVAL, BACKOFF_BASE);
|
||||||
TorRendezvousCrypto torRendezvousCrypto = new TorRendezvousCryptoImpl();
|
TorRendezvousCrypto torRendezvousCrypto = new TorRendezvousCryptoImpl();
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ dependencyVerification {
|
|||||||
'org.apache.ant:ant:1.9.4:ant-1.9.4.jar:649ae0730251de07b8913f49286d46bba7b92d47c5f332610aa426c4f02161d8',
|
'org.apache.ant:ant:1.9.4:ant-1.9.4.jar:649ae0730251de07b8913f49286d46bba7b92d47c5f332610aa426c4f02161d8',
|
||||||
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
|
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
|
||||||
'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a:obfs4proxy-0.0.12-dev-40245c4a.zip:172029e7058b3a83ac93ac4991a44bf76e16ce8d46f558f5836d57da3cb3a766',
|
'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a:obfs4proxy-0.0.12-dev-40245c4a.zip:172029e7058b3a83ac93ac4991a44bf76e16ce8d46f558f5836d57da3cb3a766',
|
||||||
'org.briarproject:tor:0.3.5.13-1:tor-0.3.5.13-1.zip:ef35c16bf8dc1f4c75ed71d9f55e4514f383d124ec96b859aca647c990927c99',
|
'org.briarproject:tor:0.3.5.13:tor-0.3.5.13.zip:1c5f0b821ee2aadb0ea04aa96caab3ca0a08370cce8de81c2dfe04d172f8a2a0',
|
||||||
'org.checkerframework:checker-compat-qual:2.5.3:checker-compat-qual-2.5.3.jar:d76b9afea61c7c082908023f0cbc1427fab9abd2df915c8b8a3e7a509bccbc6d',
|
'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.checkerframework:checker-qual:2.5.2:checker-qual-2.5.2.jar:64b02691c8b9d4e7700f8ee2e742dce7ea2c6e81e662b7522c9ee3bf568c040a',
|
||||||
'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53',
|
'org.codehaus.mojo:animal-sniffer-annotations:1.17:animal-sniffer-annotations-1.17.jar:92654f493ecfec52082e76354f0ebf87648dc3d5cec2e3c3cdb947c016747a53',
|
||||||
|
|||||||
@@ -19,15 +19,11 @@ android {
|
|||||||
compileSdkVersion 30
|
compileSdkVersion 30
|
||||||
buildToolsVersion '30.0.2'
|
buildToolsVersion '30.0.2'
|
||||||
|
|
||||||
packagingOptions {
|
|
||||||
doNotStrip '**/*.so'
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 10220
|
versionCode 10216
|
||||||
versionName "1.2.20"
|
versionName "1.2.16"
|
||||||
applicationId "org.briarproject.briar.android"
|
applicationId "org.briarproject.briar.android"
|
||||||
|
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|||||||
@@ -4,20 +4,16 @@ import android.app.Activity;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.account.AccountManager;
|
import org.briarproject.bramble.api.account.AccountManager;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.settings.Settings;
|
|
||||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.test.espresso.intent.rule.IntentsTestRule;
|
import androidx.test.espresso.intent.rule.IntentsTestRule;
|
||||||
|
|
||||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
||||||
import static org.briarproject.briar.android.controller.BriarControllerImpl.DOZE_ASK_AGAIN;
|
|
||||||
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
@@ -31,8 +27,6 @@ public abstract class UiTest {
|
|||||||
protected AccountManager accountManager;
|
protected AccountManager accountManager;
|
||||||
@Inject
|
@Inject
|
||||||
protected LifecycleManager lifecycleManager;
|
protected LifecycleManager lifecycleManager;
|
||||||
@Inject
|
|
||||||
protected SettingsManager settingsManager;
|
|
||||||
|
|
||||||
public UiTest() {
|
public UiTest() {
|
||||||
BriarTestComponentApplication app = getApplicationContext();
|
BriarTestComponentApplication app = getApplicationContext();
|
||||||
@@ -45,8 +39,22 @@ public abstract class UiTest {
|
|||||||
protected class CleanAccountTestRule<A extends Activity>
|
protected class CleanAccountTestRule<A extends Activity>
|
||||||
extends IntentsTestRule<A> {
|
extends IntentsTestRule<A> {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final Runnable runnable;
|
||||||
|
|
||||||
public CleanAccountTestRule(Class<A> activityClass) {
|
public CleanAccountTestRule(Class<A> activityClass) {
|
||||||
super(activityClass);
|
super(activityClass);
|
||||||
|
this.runnable = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this if you need to run code before launching the activity.
|
||||||
|
* Note: You need to use {@link #launchActivity(Intent)} yourself
|
||||||
|
* to start the activity.
|
||||||
|
*/
|
||||||
|
public CleanAccountTestRule(Class<A> activityClass, Runnable runnable) {
|
||||||
|
super(activityClass, false, false);
|
||||||
|
this.runnable = runnable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -54,17 +62,16 @@ public abstract class UiTest {
|
|||||||
super.beforeActivityLaunched();
|
super.beforeActivityLaunched();
|
||||||
accountManager.deleteAccount();
|
accountManager.deleteAccount();
|
||||||
accountManager.createAccount(USERNAME, PASSWORD);
|
accountManager.createAccount(USERNAME, PASSWORD);
|
||||||
Intent serviceIntent =
|
if (runnable != null) {
|
||||||
new Intent(getApplicationContext(), BriarService.class);
|
Intent serviceIntent =
|
||||||
getApplicationContext().startService(serviceIntent);
|
new Intent(getApplicationContext(), BriarService.class);
|
||||||
try {
|
getApplicationContext().startService(serviceIntent);
|
||||||
lifecycleManager.waitForStartup();
|
try {
|
||||||
// do not show doze white-listing dialog
|
lifecycleManager.waitForStartup();
|
||||||
Settings settings = new Settings();
|
} catch (InterruptedException e) {
|
||||||
settings.putBoolean(DOZE_ASK_AGAIN, false);
|
throw new AssertionError(e);
|
||||||
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
|
}
|
||||||
} catch (InterruptedException | DbException e) {
|
runnable.run();
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
|
|||||||
import org.briarproject.bramble.BrambleAndroidModule;
|
import org.briarproject.bramble.BrambleAndroidModule;
|
||||||
import org.briarproject.bramble.BrambleCoreModule;
|
import org.briarproject.bramble.BrambleCoreModule;
|
||||||
import org.briarproject.bramble.account.BriarAccountModule;
|
import org.briarproject.bramble.account.BriarAccountModule;
|
||||||
|
import org.briarproject.bramble.system.ClockModule;
|
||||||
import org.briarproject.briar.BriarCoreModule;
|
import org.briarproject.briar.BriarCoreModule;
|
||||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||||
import org.briarproject.briar.android.attachment.media.MediaModule;
|
import org.briarproject.briar.android.attachment.media.MediaModule;
|
||||||
@@ -16,6 +17,7 @@ import dagger.Component;
|
|||||||
@Component(modules = {
|
@Component(modules = {
|
||||||
AppModule.class,
|
AppModule.class,
|
||||||
AttachmentModule.class,
|
AttachmentModule.class,
|
||||||
|
ClockModule.class,
|
||||||
MediaModule.class,
|
MediaModule.class,
|
||||||
BriarCoreModule.class,
|
BriarCoreModule.class,
|
||||||
BrambleAndroidModule.class,
|
BrambleAndroidModule.class,
|
||||||
|
|||||||
@@ -10,15 +10,12 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import androidx.test.filters.LargeTest;
|
|
||||||
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||||
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.junit.Assume.assumeTrue;
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
@LargeTest
|
|
||||||
@RunWith(Parameterized.class)
|
@RunWith(Parameterized.class)
|
||||||
public class PngSuiteImageCompressorTest
|
public class PngSuiteImageCompressorTest
|
||||||
extends AbstractImageCompressorTest {
|
extends AbstractImageCompressorTest {
|
||||||
|
|||||||
@@ -12,14 +12,11 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import androidx.test.filters.LargeTest;
|
|
||||||
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||||
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
|
||||||
import static org.junit.Assume.assumeTrue;
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
|
||||||
@LargeTest
|
|
||||||
@RunWith(Parameterized.class)
|
@RunWith(Parameterized.class)
|
||||||
public class PngSuiteImageSizeCalculatorTest
|
public class PngSuiteImageSizeCalculatorTest
|
||||||
extends AbstractImageSizeCalculatorTest {
|
extends AbstractImageSizeCalculatorTest {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.briarproject.briar.android.conversation;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.rule.ActivityTestRule;
|
||||||
|
|
||||||
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||||
|
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||||
|
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||||
|
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
|
||||||
|
import static org.briarproject.briar.android.conversation.ConversationActivity.CONTACT_ID;
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ConversationActivityNotSignedInTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ActivityTestRule<ConversationActivity> testRule =
|
||||||
|
new ActivityTestRule<>(ConversationActivity.class, false, false);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void openWithoutSignedIn() {
|
||||||
|
Context targetContext = getInstrumentation().getTargetContext();
|
||||||
|
Intent intent = new Intent(targetContext, ConversationActivity.class);
|
||||||
|
intent.putExtra(CONTACT_ID, 1);
|
||||||
|
testRule.launchActivity(intent);
|
||||||
|
|
||||||
|
onView(withText(R.string.sign_in_button))
|
||||||
|
.perform(waitUntilMatches(isDisplayed()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package org.briarproject.briar.android;
|
|||||||
import org.briarproject.bramble.BrambleAndroidModule;
|
import org.briarproject.bramble.BrambleAndroidModule;
|
||||||
import org.briarproject.bramble.BrambleCoreModule;
|
import org.briarproject.bramble.BrambleCoreModule;
|
||||||
import org.briarproject.bramble.account.BriarAccountModule;
|
import org.briarproject.bramble.account.BriarAccountModule;
|
||||||
|
import org.briarproject.bramble.system.ClockModule;
|
||||||
import org.briarproject.briar.BriarCoreModule;
|
import org.briarproject.briar.BriarCoreModule;
|
||||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||||
import org.briarproject.briar.android.attachment.media.MediaModule;
|
import org.briarproject.briar.android.attachment.media.MediaModule;
|
||||||
@@ -17,6 +18,7 @@ import dagger.Component;
|
|||||||
@Component(modules = {
|
@Component(modules = {
|
||||||
AppModule.class,
|
AppModule.class,
|
||||||
AttachmentModule.class,
|
AttachmentModule.class,
|
||||||
|
ClockModule.class,
|
||||||
MediaModule.class,
|
MediaModule.class,
|
||||||
BriarCoreModule.class,
|
BriarCoreModule.class,
|
||||||
BrambleAndroidModule.class,
|
BrambleAndroidModule.class,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import androidx.test.espresso.contrib.DrawerActions;
|
|||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.rule.ActivityTestRule;
|
import androidx.test.rule.ActivityTestRule;
|
||||||
|
|
||||||
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
|
|
||||||
import static androidx.test.espresso.Espresso.onView;
|
import static androidx.test.espresso.Espresso.onView;
|
||||||
import static androidx.test.espresso.action.ViewActions.click;
|
import static androidx.test.espresso.action.ViewActions.click;
|
||||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||||
@@ -30,9 +29,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
|
|||||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||||
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
|
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.hasScreenLock;
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.junit.Assume.assumeTrue;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class SettingsActivityScreenshotTest extends ScreenshotTest {
|
public class SettingsActivityScreenshotTest extends ScreenshotTest {
|
||||||
@@ -79,8 +76,6 @@ public class SettingsActivityScreenshotTest extends ScreenshotTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void appLock() {
|
public void appLock() {
|
||||||
assumeTrue("device has no screen lock",
|
|
||||||
hasScreenLock(getApplicationContext()));
|
|
||||||
// scroll down
|
// scroll down
|
||||||
onView(withClassName(is(RecyclerView.class.getName())))
|
onView(withClassName(is(RecyclerView.class.getName())))
|
||||||
.perform(scrollTo(hasDescendant(
|
.perform(scrollTo(hasDescendant(
|
||||||
|
|||||||
@@ -27,10 +27,7 @@
|
|||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="18"
|
|
||||||
tools:ignore="ScopedStorage" />
|
|
||||||
|
|
||||||
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
@@ -41,7 +38,6 @@
|
|||||||
android:name="org.briarproject.briar.android.BriarApplicationImpl"
|
android:name="org.briarproject.briar.android.BriarApplicationImpl"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:banner="@mipmap/tv_banner"
|
android:banner="@mipmap/tv_banner"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher_round"
|
android:icon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:logo="@mipmap/ic_launcher_round"
|
android:logo="@mipmap/ic_launcher_round"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.briarproject.bramble.api.system.AndroidWakeLockManager;
|
|||||||
import org.briarproject.bramble.api.system.Clock;
|
import org.briarproject.bramble.api.system.Clock;
|
||||||
import org.briarproject.bramble.api.system.LocationUtils;
|
import org.briarproject.bramble.api.system.LocationUtils;
|
||||||
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
|
import org.briarproject.bramble.plugin.tor.CircumventionProvider;
|
||||||
|
import org.briarproject.bramble.system.ClockModule;
|
||||||
import org.briarproject.briar.BriarCoreEagerSingletons;
|
import org.briarproject.briar.BriarCoreEagerSingletons;
|
||||||
import org.briarproject.briar.BriarCoreModule;
|
import org.briarproject.briar.BriarCoreModule;
|
||||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||||
@@ -42,6 +43,7 @@ import org.briarproject.briar.api.android.DozeWatchdog;
|
|||||||
import org.briarproject.briar.api.android.LockManager;
|
import org.briarproject.briar.api.android.LockManager;
|
||||||
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
import org.briarproject.briar.api.android.ScreenFilterMonitor;
|
||||||
import org.briarproject.briar.api.attachment.AttachmentReader;
|
import org.briarproject.briar.api.attachment.AttachmentReader;
|
||||||
|
import org.briarproject.briar.api.autodelete.AutoDeleteManager;
|
||||||
import org.briarproject.briar.api.blog.BlogManager;
|
import org.briarproject.briar.api.blog.BlogManager;
|
||||||
import org.briarproject.briar.api.blog.BlogPostFactory;
|
import org.briarproject.briar.api.blog.BlogPostFactory;
|
||||||
import org.briarproject.briar.api.blog.BlogSharingManager;
|
import org.briarproject.briar.api.blog.BlogSharingManager;
|
||||||
@@ -76,6 +78,7 @@ import dagger.Component;
|
|||||||
BriarAccountModule.class,
|
BriarAccountModule.class,
|
||||||
AppModule.class,
|
AppModule.class,
|
||||||
AttachmentModule.class,
|
AttachmentModule.class,
|
||||||
|
ClockModule.class,
|
||||||
MediaModule.class
|
MediaModule.class
|
||||||
})
|
})
|
||||||
public interface AndroidComponent
|
public interface AndroidComponent
|
||||||
@@ -184,6 +187,8 @@ public interface AndroidComponent
|
|||||||
|
|
||||||
Thread.UncaughtExceptionHandler exceptionHandler();
|
Thread.UncaughtExceptionHandler exceptionHandler();
|
||||||
|
|
||||||
|
AutoDeleteManager autoDeleteManager();
|
||||||
|
|
||||||
void inject(SignInReminderReceiver briarService);
|
void inject(SignInReminderReceiver briarService);
|
||||||
|
|
||||||
void inject(BriarService briarService);
|
void inject(BriarService briarService);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import org.briarproject.briar.android.splash.SplashScreenActivity;
|
|||||||
import org.briarproject.briar.android.util.BriarNotificationBuilder;
|
import org.briarproject.briar.android.util.BriarNotificationBuilder;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
|
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
|
||||||
|
import org.briarproject.briar.api.conversation.ConversationResponse;
|
||||||
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
|
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
|
||||||
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
|
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
|
||||||
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
|
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
|
||||||
@@ -226,6 +227,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
|||||||
} else if (e instanceof ConversationMessageReceivedEvent) {
|
} else if (e instanceof ConversationMessageReceivedEvent) {
|
||||||
ConversationMessageReceivedEvent<?> p =
|
ConversationMessageReceivedEvent<?> p =
|
||||||
(ConversationMessageReceivedEvent<?>) e;
|
(ConversationMessageReceivedEvent<?>) e;
|
||||||
|
if (p.getMessageHeader() instanceof ConversationResponse) {
|
||||||
|
ConversationResponse r =
|
||||||
|
(ConversationResponse) p.getMessageHeader();
|
||||||
|
// don't show notification for own auto-decline responses
|
||||||
|
if (r.isAutoDecline()) return;
|
||||||
|
}
|
||||||
showContactNotification(p.getContactId());
|
showContactNotification(p.getContactId());
|
||||||
} else if (e instanceof GroupMessageAddedEvent) {
|
} else if (e instanceof GroupMessageAddedEvent) {
|
||||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ import org.briarproject.bramble.util.StringUtils;
|
|||||||
import org.briarproject.briar.android.account.DozeHelperModule;
|
import org.briarproject.briar.android.account.DozeHelperModule;
|
||||||
import org.briarproject.briar.android.account.LockManagerImpl;
|
import org.briarproject.briar.android.account.LockManagerImpl;
|
||||||
import org.briarproject.briar.android.account.SetupModule;
|
import org.briarproject.briar.android.account.SetupModule;
|
||||||
import org.briarproject.briar.android.blog.BlogModule;
|
|
||||||
import org.briarproject.briar.android.contact.ContactListModule;
|
import org.briarproject.briar.android.contact.ContactListModule;
|
||||||
import org.briarproject.briar.android.forum.ForumModule;
|
import org.briarproject.briar.android.forum.ForumModule;
|
||||||
import org.briarproject.briar.android.introduction.IntroductionModule;
|
|
||||||
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
|
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
|
||||||
import org.briarproject.briar.android.logging.LoggingModule;
|
import org.briarproject.briar.android.logging.LoggingModule;
|
||||||
import org.briarproject.briar.android.login.LoginModule;
|
import org.briarproject.briar.android.login.LoginModule;
|
||||||
@@ -84,9 +82,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
|||||||
SettingsModule.class,
|
SettingsModule.class,
|
||||||
DevReportModule.class,
|
DevReportModule.class,
|
||||||
ContactListModule.class,
|
ContactListModule.class,
|
||||||
IntroductionModule.class,
|
|
||||||
// below need to be within same scope as ViewModelProvider.Factory
|
// below need to be within same scope as ViewModelProvider.Factory
|
||||||
BlogModule.class,
|
|
||||||
ForumModule.class,
|
ForumModule.class,
|
||||||
GroupListModule.class,
|
GroupListModule.class,
|
||||||
GroupConversationModule.class,
|
GroupConversationModule.class,
|
||||||
@@ -287,6 +283,11 @@ public class AppModule {
|
|||||||
public boolean shouldEnableProfilePictures() {
|
public boolean shouldEnableProfilePictures() {
|
||||||
return IS_DEBUG_BUILD;
|
return IS_DEBUG_BUILD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldEnableDisappearingMessages() {
|
||||||
|
return IS_DEBUG_BUILD;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public class DozeFragment extends SetupFragment
|
|||||||
private DozeView dozeView;
|
private DozeView dozeView;
|
||||||
private HuaweiView huaweiView;
|
private HuaweiView huaweiView;
|
||||||
private Button next;
|
private Button next;
|
||||||
|
private ProgressBar progressBar;
|
||||||
private boolean secondAttempt = false;
|
private boolean secondAttempt = false;
|
||||||
|
|
||||||
public static DozeFragment newInstance() {
|
public static DozeFragment newInstance() {
|
||||||
@@ -57,19 +58,11 @@ public class DozeFragment extends SetupFragment
|
|||||||
huaweiView = v.findViewById(R.id.huaweiView);
|
huaweiView = v.findViewById(R.id.huaweiView);
|
||||||
huaweiView.setOnCheckedChangedListener(this);
|
huaweiView.setOnCheckedChangedListener(this);
|
||||||
next = v.findViewById(R.id.next);
|
next = v.findViewById(R.id.next);
|
||||||
ProgressBar progressBar = v.findViewById(R.id.progress);
|
progressBar = v.findViewById(R.id.progress);
|
||||||
|
|
||||||
dozeView.setOnButtonClickListener(this::askForDozeWhitelisting);
|
dozeView.setOnButtonClickListener(this::askForDozeWhitelisting);
|
||||||
next.setOnClickListener(this);
|
next.setOnClickListener(this);
|
||||||
|
|
||||||
viewModel.getIsCreatingAccount()
|
|
||||||
.observe(getViewLifecycleOwner(), isCreatingAccount -> {
|
|
||||||
if (isCreatingAccount) {
|
|
||||||
next.setVisibility(INVISIBLE);
|
|
||||||
progressBar.setVisibility(VISIBLE);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +104,15 @@ public class DozeFragment extends SetupFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
|
setNextClicked();
|
||||||
viewModel.dozeExceptionConfirmed();
|
viewModel.dozeExceptionConfirmed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void setNextClicked() {
|
||||||
|
super.setNextClicked();
|
||||||
|
|
||||||
|
next.setVisibility(INVISIBLE);
|
||||||
|
progressBar.setVisibility(VISIBLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ public class SetPasswordFragment extends SetupFragment {
|
|||||||
private TextInputEditText passwordConfirmation;
|
private TextInputEditText passwordConfirmation;
|
||||||
private StrengthMeter strengthMeter;
|
private StrengthMeter strengthMeter;
|
||||||
private Button nextButton;
|
private Button nextButton;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
|
||||||
public static SetPasswordFragment newInstance() {
|
public static SetPasswordFragment newInstance() {
|
||||||
return new SetPasswordFragment();
|
return new SetPasswordFragment();
|
||||||
@@ -63,7 +64,7 @@ public class SetPasswordFragment extends SetupFragment {
|
|||||||
v.findViewById(R.id.password_confirm_wrapper);
|
v.findViewById(R.id.password_confirm_wrapper);
|
||||||
passwordConfirmation = v.findViewById(R.id.password_confirm);
|
passwordConfirmation = v.findViewById(R.id.password_confirm);
|
||||||
nextButton = v.findViewById(R.id.next);
|
nextButton = v.findViewById(R.id.next);
|
||||||
ProgressBar progressBar = v.findViewById(R.id.progress);
|
progressBar = v.findViewById(R.id.progress);
|
||||||
|
|
||||||
passwordEntry.addTextChangedListener(this);
|
passwordEntry.addTextChangedListener(this);
|
||||||
passwordConfirmation.addTextChangedListener(this);
|
passwordConfirmation.addTextChangedListener(this);
|
||||||
@@ -74,17 +75,6 @@ public class SetPasswordFragment extends SetupFragment {
|
|||||||
passwordConfirmation.setImeOptions(IME_ACTION_DONE);
|
passwordConfirmation.setImeOptions(IME_ACTION_DONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.getIsCreatingAccount()
|
|
||||||
.observe(getViewLifecycleOwner(), isCreatingAccount -> {
|
|
||||||
if (isCreatingAccount) {
|
|
||||||
nextButton.setVisibility(INVISIBLE);
|
|
||||||
progressBar.setVisibility(VISIBLE);
|
|
||||||
// this also avoids the keyboard popping up
|
|
||||||
passwordEntry.setFocusable(false);
|
|
||||||
passwordConfirmation.setFocusable(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +116,20 @@ public class SetPasswordFragment extends SetupFragment {
|
|||||||
IBinder token = passwordEntry.getWindowToken();
|
IBinder token = passwordEntry.getWindowToken();
|
||||||
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
|
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
|
||||||
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
|
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
|
||||||
|
|
||||||
|
setNextClicked();
|
||||||
viewModel.setPassword(passwordEntry.getText().toString());
|
viewModel.setPassword(passwordEntry.getText().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void setNextClicked() {
|
||||||
|
super.setNextClicked();
|
||||||
|
|
||||||
|
passwordEntry.setFocusable(false);
|
||||||
|
passwordConfirmation.setFocusable(false);
|
||||||
|
if (!viewModel.needToShowDozeFragment()) {
|
||||||
|
nextButton.setVisibility(INVISIBLE);
|
||||||
|
progressBar.setVisibility(VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.KeyEvent;
|
|||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.TextView.OnEditorActionListener;
|
import android.widget.TextView.OnEditorActionListener;
|
||||||
@@ -18,6 +19,8 @@ import org.briarproject.briar.android.fragment.BaseFragment;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
@@ -32,6 +35,7 @@ abstract class SetupFragment extends BaseFragment implements TextWatcher,
|
|||||||
OnEditorActionListener, OnClickListener {
|
OnEditorActionListener, OnClickListener {
|
||||||
|
|
||||||
private final static String STATE_KEY_CLICKED = "setupFragmentClicked";
|
private final static String STATE_KEY_CLICKED = "setupFragmentClicked";
|
||||||
|
private boolean clicked = false;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ViewModelProvider.Factory viewModelFactory;
|
ViewModelProvider.Factory viewModelFactory;
|
||||||
@@ -44,6 +48,27 @@ abstract class SetupFragment extends BaseFragment implements TextWatcher,
|
|||||||
.get(SetupViewModel.class);
|
.get(SetupViewModel.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
clicked = savedInstanceState.getBoolean(STATE_KEY_CLICKED);
|
||||||
|
}
|
||||||
|
if (clicked) {
|
||||||
|
setNextClicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.putBoolean(STATE_KEY_CLICKED, clicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void setNextClicked() {
|
||||||
|
this.clicked = true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.help_action, menu);
|
inflater.inflate(R.menu.help_action, menu);
|
||||||
@@ -89,4 +114,5 @@ abstract class SetupFragment extends BaseFragment implements TextWatcher,
|
|||||||
public void afterTextChanged(Editable editable) {
|
public void afterTextChanged(Editable editable) {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ import javax.inject.Inject;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.briar.android.account.SetupViewModel.State.AUTHOR_NAME;
|
import static org.briarproject.briar.android.account.SetupViewModel.State.AUTHOR_NAME;
|
||||||
@@ -38,8 +36,6 @@ class SetupViewModel extends AndroidViewModel {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private String authorName, password;
|
private String authorName, password;
|
||||||
private final MutableLiveEvent<State> state = new MutableLiveEvent<>();
|
private final MutableLiveEvent<State> state = new MutableLiveEvent<>();
|
||||||
private final MutableLiveData<Boolean> isCreatingAccount =
|
|
||||||
new MutableLiveData<>(false);
|
|
||||||
|
|
||||||
private final AccountManager accountManager;
|
private final AccountManager accountManager;
|
||||||
private final Executor ioExecutor;
|
private final Executor ioExecutor;
|
||||||
@@ -71,10 +67,6 @@ class SetupViewModel extends AndroidViewModel {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveData<Boolean> getIsCreatingAccount() {
|
|
||||||
return isCreatingAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setAuthorName(String authorName) {
|
void setAuthorName(String authorName) {
|
||||||
this.authorName = authorName;
|
this.authorName = authorName;
|
||||||
state.setEvent(SET_PASSWORD);
|
state.setEvent(SET_PASSWORD);
|
||||||
@@ -105,7 +97,6 @@ class SetupViewModel extends AndroidViewModel {
|
|||||||
private void createAccount() {
|
private void createAccount() {
|
||||||
if (authorName == null) throw new IllegalStateException();
|
if (authorName == null) throw new IllegalStateException();
|
||||||
if (password == null) throw new IllegalStateException();
|
if (password == null) throw new IllegalStateException();
|
||||||
isCreatingAccount.setValue(true);
|
|
||||||
ioExecutor.execute(() -> {
|
ioExecutor.execute(() -> {
|
||||||
if (accountManager.createAccount(authorName, password)) {
|
if (accountManager.createAccount(authorName, password)) {
|
||||||
LOG.info("Created account");
|
LOG.info("Created account");
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import org.briarproject.briar.android.account.SetupActivity;
|
|||||||
import org.briarproject.briar.android.account.UnlockActivity;
|
import org.briarproject.briar.android.account.UnlockActivity;
|
||||||
import org.briarproject.briar.android.blog.BlogActivity;
|
import org.briarproject.briar.android.blog.BlogActivity;
|
||||||
import org.briarproject.briar.android.blog.BlogFragment;
|
import org.briarproject.briar.android.blog.BlogFragment;
|
||||||
|
import org.briarproject.briar.android.blog.BlogModule;
|
||||||
import org.briarproject.briar.android.blog.BlogPostFragment;
|
import org.briarproject.briar.android.blog.BlogPostFragment;
|
||||||
import org.briarproject.briar.android.blog.FeedFragment;
|
import org.briarproject.briar.android.blog.FeedFragment;
|
||||||
|
import org.briarproject.briar.android.blog.FeedPostFragment;
|
||||||
import org.briarproject.briar.android.blog.ReblogActivity;
|
import org.briarproject.briar.android.blog.ReblogActivity;
|
||||||
import org.briarproject.briar.android.blog.ReblogFragment;
|
import org.briarproject.briar.android.blog.ReblogFragment;
|
||||||
import org.briarproject.briar.android.blog.RssFeedImportActivity;
|
import org.briarproject.briar.android.blog.RssFeedImportActivity;
|
||||||
@@ -25,6 +27,7 @@ import org.briarproject.briar.android.contact.add.remote.NicknameFragment;
|
|||||||
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
|
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
|
||||||
import org.briarproject.briar.android.conversation.AliasDialogFragment;
|
import org.briarproject.briar.android.conversation.AliasDialogFragment;
|
||||||
import org.briarproject.briar.android.conversation.ConversationActivity;
|
import org.briarproject.briar.android.conversation.ConversationActivity;
|
||||||
|
import org.briarproject.briar.android.conversation.ConversationSettingsDialog;
|
||||||
import org.briarproject.briar.android.conversation.ImageActivity;
|
import org.briarproject.briar.android.conversation.ImageActivity;
|
||||||
import org.briarproject.briar.android.conversation.ImageFragment;
|
import org.briarproject.briar.android.conversation.ImageFragment;
|
||||||
import org.briarproject.briar.android.forum.CreateForumActivity;
|
import org.briarproject.briar.android.forum.CreateForumActivity;
|
||||||
@@ -83,6 +86,7 @@ import dagger.Component;
|
|||||||
@ActivityScope
|
@ActivityScope
|
||||||
@Component(modules = {
|
@Component(modules = {
|
||||||
ActivityModule.class,
|
ActivityModule.class,
|
||||||
|
BlogModule.class,
|
||||||
CreateGroupModule.class,
|
CreateGroupModule.class,
|
||||||
GroupInvitationModule.class,
|
GroupInvitationModule.class,
|
||||||
GroupMemberModule.class,
|
GroupMemberModule.class,
|
||||||
@@ -149,6 +153,8 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(BlogPostFragment fragment);
|
void inject(BlogPostFragment fragment);
|
||||||
|
|
||||||
|
void inject(FeedPostFragment fragment);
|
||||||
|
|
||||||
void inject(ReblogFragment fragment);
|
void inject(ReblogFragment fragment);
|
||||||
|
|
||||||
void inject(ReblogActivity activity);
|
void inject(ReblogActivity activity);
|
||||||
@@ -233,4 +239,6 @@ public interface ActivityComponent {
|
|||||||
|
|
||||||
void inject(ConfirmAvatarDialogFragment fragment);
|
void inject(ConfirmAvatarDialogFragment fragment);
|
||||||
|
|
||||||
|
void inject(ConversationSettingsDialog dialog);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ public interface RequestCodes {
|
|||||||
int REQUEST_INTRODUCTION = 2;
|
int REQUEST_INTRODUCTION = 2;
|
||||||
int REQUEST_GROUP_INVITE = 3;
|
int REQUEST_GROUP_INVITE = 3;
|
||||||
int REQUEST_SHARE_FORUM = 4;
|
int REQUEST_SHARE_FORUM = 4;
|
||||||
|
int REQUEST_WRITE_BLOG_POST = 5;
|
||||||
int REQUEST_SHARE_BLOG = 6;
|
int REQUEST_SHARE_BLOG = 6;
|
||||||
int REQUEST_RINGTONE = 7;
|
int REQUEST_RINGTONE = 7;
|
||||||
int REQUEST_PERMISSION_CAMERA_LOCATION = 8;
|
int REQUEST_PERMISSION_CAMERA_LOCATION = 8;
|
||||||
|
|||||||
@@ -28,12 +28,9 @@ import javax.inject.Inject;
|
|||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static java.util.logging.Level.INFO;
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes;
|
|
||||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE;
|
import static org.briarproject.briar.android.attachment.AttachmentItem.State.AVAILABLE;
|
||||||
@@ -89,17 +86,7 @@ class AttachmentRetrieverImpl implements AttachmentRetriever {
|
|||||||
List<AttachmentHeader> headers = messageHeader.getAttachmentHeaders();
|
List<AttachmentHeader> headers = messageHeader.getAttachmentHeaders();
|
||||||
List<LiveData<AttachmentItem>> items = new ArrayList<>(headers.size());
|
List<LiveData<AttachmentItem>> items = new ArrayList<>(headers.size());
|
||||||
boolean needsSize = headers.size() == 1;
|
boolean needsSize = headers.size() == 1;
|
||||||
List<String> supported = asList(getSupportedImageContentTypes());
|
|
||||||
for (AttachmentHeader h : headers) {
|
for (AttachmentHeader h : headers) {
|
||||||
// Fail early if we don't support the content type
|
|
||||||
if (!supported.contains(h.getContentType())) {
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("Unsupported content type " + h.getContentType());
|
|
||||||
}
|
|
||||||
AttachmentItem item = new AttachmentItem(h, "", ERROR);
|
|
||||||
items.add(new MutableLiveData<>(item));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// try cache for existing item live data
|
// try cache for existing item live data
|
||||||
MutableLiveData<AttachmentItem> liveData =
|
MutableLiveData<AttachmentItem> liveData =
|
||||||
itemsWithSize.get(h.getMessageId());
|
itemsWithSize.get(h.getMessageId());
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||||
|
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||||
|
import org.briarproject.briar.api.blog.BlogPostHeader;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface BaseController {
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void onStart();
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void onStop();
|
||||||
|
|
||||||
|
void loadBlogPosts(GroupId g,
|
||||||
|
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
|
||||||
|
|
||||||
|
void loadBlogPost(BlogPostHeader header,
|
||||||
|
ResultExceptionHandler<BlogPostItem, DbException> handler);
|
||||||
|
|
||||||
|
void loadBlogPost(GroupId g, MessageId m,
|
||||||
|
ResultExceptionHandler<BlogPostItem, DbException> handler);
|
||||||
|
|
||||||
|
void repeatPost(BlogPostItem item, @Nullable String comment,
|
||||||
|
ExceptionHandler<DbException> handler);
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface BlogListener {
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void onBlogPostAdded(BlogPostHeader header, boolean local);
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void onBlogRemoved();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
|
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
import org.briarproject.briar.android.controller.DbControllerImpl;
|
||||||
|
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||||
|
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||||
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
|
import org.briarproject.briar.api.blog.Blog;
|
||||||
|
import org.briarproject.briar.api.blog.BlogCommentHeader;
|
||||||
|
import org.briarproject.briar.api.blog.BlogManager;
|
||||||
|
import org.briarproject.briar.api.blog.BlogPostHeader;
|
||||||
|
import org.briarproject.briar.util.HtmlUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
|
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
|
||||||
|
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
|
abstract class BaseControllerImpl extends DbControllerImpl
|
||||||
|
implements BaseController, EventListener {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
Logger.getLogger(BaseControllerImpl.class.getName());
|
||||||
|
|
||||||
|
protected final EventBus eventBus;
|
||||||
|
protected final AndroidNotificationManager notificationManager;
|
||||||
|
protected final IdentityManager identityManager;
|
||||||
|
protected final BlogManager blogManager;
|
||||||
|
|
||||||
|
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
|
||||||
|
private final Map<MessageId, BlogPostHeader> headerCache =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
BaseControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
|
LifecycleManager lifecycleManager, EventBus eventBus,
|
||||||
|
AndroidNotificationManager notificationManager,
|
||||||
|
IdentityManager identityManager, BlogManager blogManager) {
|
||||||
|
super(dbExecutor, lifecycleManager);
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.notificationManager = notificationManager;
|
||||||
|
this.identityManager = identityManager;
|
||||||
|
this.blogManager = blogManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CallSuper
|
||||||
|
public void onStart() {
|
||||||
|
eventBus.addListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@CallSuper
|
||||||
|
public void onStop() {
|
||||||
|
eventBus.removeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadBlogPosts(GroupId groupId,
|
||||||
|
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
|
||||||
|
runOnDbThread(() -> {
|
||||||
|
try {
|
||||||
|
Collection<BlogPostItem> items = loadItems(groupId);
|
||||||
|
handler.onResult(items);
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<BlogPostItem> loadItems(GroupId groupId) throws DbException {
|
||||||
|
long start = now();
|
||||||
|
Collection<BlogPostHeader> headers =
|
||||||
|
blogManager.getPostHeaders(groupId);
|
||||||
|
logDuration(LOG, "Loading headers", start);
|
||||||
|
Collection<BlogPostItem> items = new ArrayList<>(headers.size());
|
||||||
|
start = now();
|
||||||
|
for (BlogPostHeader h : headers) {
|
||||||
|
headerCache.put(h.getId(), h);
|
||||||
|
BlogPostItem item = getItem(h);
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
logDuration(LOG, "Loading bodies", start);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadBlogPost(BlogPostHeader header,
|
||||||
|
ResultExceptionHandler<BlogPostItem, DbException> handler) {
|
||||||
|
|
||||||
|
String text = textCache.get(header.getId());
|
||||||
|
if (text != null) {
|
||||||
|
LOG.info("Loaded text from cache");
|
||||||
|
handler.onResult(new BlogPostItem(header, text));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runOnDbThread(() -> {
|
||||||
|
try {
|
||||||
|
long start = now();
|
||||||
|
BlogPostItem item = getItem(header);
|
||||||
|
logDuration(LOG, "Loading text", start);
|
||||||
|
handler.onResult(item);
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadBlogPost(GroupId g, MessageId m,
|
||||||
|
ResultExceptionHandler<BlogPostItem, DbException> handler) {
|
||||||
|
|
||||||
|
BlogPostHeader header = headerCache.get(m);
|
||||||
|
if (header != null) {
|
||||||
|
LOG.info("Loaded header from cache");
|
||||||
|
loadBlogPost(header, handler);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runOnDbThread(() -> {
|
||||||
|
try {
|
||||||
|
long start = now();
|
||||||
|
BlogPostHeader header1 = getPostHeader(g, m);
|
||||||
|
BlogPostItem item = getItem(header1);
|
||||||
|
logDuration(LOG, "Loading post", start);
|
||||||
|
handler.onResult(item);
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void repeatPost(BlogPostItem item, @Nullable String comment,
|
||||||
|
ExceptionHandler<DbException> handler) {
|
||||||
|
runOnDbThread(() -> {
|
||||||
|
try {
|
||||||
|
LocalAuthor a = identityManager.getLocalAuthor();
|
||||||
|
Blog b = blogManager.getPersonalBlog(a);
|
||||||
|
BlogPostHeader h = item.getHeader();
|
||||||
|
blogManager.addLocalComment(a, b.getId(), comment, h);
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private BlogPostHeader getPostHeader(GroupId g, MessageId m)
|
||||||
|
throws DbException {
|
||||||
|
BlogPostHeader header = headerCache.get(m);
|
||||||
|
if (header == null) {
|
||||||
|
header = blogManager.getPostHeader(g, m);
|
||||||
|
headerCache.put(m, header);
|
||||||
|
}
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
|
@DatabaseExecutor
|
||||||
|
private BlogPostItem getItem(BlogPostHeader h) throws DbException {
|
||||||
|
String text;
|
||||||
|
if (h instanceof BlogCommentHeader) {
|
||||||
|
BlogCommentHeader c = (BlogCommentHeader) h;
|
||||||
|
BlogCommentItem item = new BlogCommentItem(c);
|
||||||
|
text = getPostText(item.getPostHeader().getId());
|
||||||
|
item.setText(text);
|
||||||
|
return item;
|
||||||
|
} else {
|
||||||
|
text = getPostText(h.getId());
|
||||||
|
return new BlogPostItem(h, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DatabaseExecutor
|
||||||
|
private String getPostText(MessageId m) throws DbException {
|
||||||
|
String text = textCache.get(m);
|
||||||
|
if (text == null) {
|
||||||
|
text = HtmlUtils.clean(blogManager.getPostText(m), ARTICLE);
|
||||||
|
textCache.put(m, text);
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
|
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper;
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||||
|
import static android.view.View.INVISIBLE;
|
||||||
|
import static android.view.View.VISIBLE;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||||
|
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
|
abstract class BasePostFragment extends BaseFragment {
|
||||||
|
|
||||||
|
static final String POST_ID = "briar.POST_ID";
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
getLogger(BasePostFragment.class.getName());
|
||||||
|
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
protected MessageId postId;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
private BlogPostViewHolder ui;
|
||||||
|
private BlogPostItem post;
|
||||||
|
private Runnable refresher;
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater,
|
||||||
|
@Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
// retrieve MessageId of blog post from arguments
|
||||||
|
byte[] p = requireArguments().getByteArray(POST_ID);
|
||||||
|
if (p == null) throw new IllegalStateException("No post ID in args");
|
||||||
|
postId = new MessageId(p);
|
||||||
|
|
||||||
|
View view = inflater.inflate(R.layout.fragment_blog_post, container,
|
||||||
|
false);
|
||||||
|
progressBar = view.findViewById(R.id.progressBar);
|
||||||
|
progressBar.setVisibility(VISIBLE);
|
||||||
|
ui = new BlogPostViewHolder(view, true, new OnBlogPostClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onBlogPostClick(BlogPostItem post) {
|
||||||
|
// We're already there
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthorClick(BlogPostItem post) {
|
||||||
|
if (getContext() == null) return;
|
||||||
|
Intent i = new Intent(getContext(), BlogActivity.class);
|
||||||
|
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
|
||||||
|
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
getContext().startActivity(i);
|
||||||
|
}
|
||||||
|
}, getFragmentManager());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
startPeriodicUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
stopPeriodicUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
protected void onBlogPostLoaded(BlogPostItem post) {
|
||||||
|
progressBar.setVisibility(INVISIBLE);
|
||||||
|
this.post = post;
|
||||||
|
ui.bindItem(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPeriodicUpdate() {
|
||||||
|
refresher = () -> {
|
||||||
|
LOG.info("Updating Content...");
|
||||||
|
ui.updateDate(post.getTimestamp());
|
||||||
|
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
|
||||||
|
};
|
||||||
|
LOG.info("Adding Handler Callback");
|
||||||
|
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopPeriodicUpdate() {
|
||||||
|
if (refresher != null) {
|
||||||
|
LOG.info("Removing Handler Callback");
|
||||||
|
handler.removeCallbacks(refresher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
|
||||||
import org.briarproject.bramble.api.db.Transaction;
|
|
||||||
import org.briarproject.bramble.api.db.TransactionManager;
|
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
|
||||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
|
||||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
|
||||||
import org.briarproject.bramble.api.sync.MessageId;
|
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
|
||||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
|
||||||
import org.briarproject.briar.android.viewmodel.LiveResult;
|
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
|
||||||
import org.briarproject.briar.api.blog.Blog;
|
|
||||||
import org.briarproject.briar.api.blog.BlogCommentHeader;
|
|
||||||
import org.briarproject.briar.api.blog.BlogManager;
|
|
||||||
import org.briarproject.briar.api.blog.BlogPostHeader;
|
|
||||||
import org.briarproject.briar.util.HtmlUtils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
|
||||||
import static org.briarproject.briar.util.HtmlUtils.ARTICLE;
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
abstract class BaseViewModel extends DbViewModel implements EventListener {
|
|
||||||
|
|
||||||
private static final Logger LOG = getLogger(BaseViewModel.class.getName());
|
|
||||||
|
|
||||||
private final EventBus eventBus;
|
|
||||||
protected final IdentityManager identityManager;
|
|
||||||
protected final AndroidNotificationManager notificationManager;
|
|
||||||
protected final BlogManager blogManager;
|
|
||||||
|
|
||||||
protected final MutableLiveData<LiveResult<ListUpdate>> blogPosts =
|
|
||||||
new MutableLiveData<>();
|
|
||||||
|
|
||||||
BaseViewModel(Application application,
|
|
||||||
@DatabaseExecutor Executor dbExecutor,
|
|
||||||
LifecycleManager lifecycleManager,
|
|
||||||
TransactionManager db,
|
|
||||||
AndroidExecutor androidExecutor,
|
|
||||||
EventBus eventBus,
|
|
||||||
IdentityManager identityManager,
|
|
||||||
AndroidNotificationManager notificationManager,
|
|
||||||
BlogManager blogManager) {
|
|
||||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
|
||||||
this.eventBus = eventBus;
|
|
||||||
this.identityManager = identityManager;
|
|
||||||
this.notificationManager = notificationManager;
|
|
||||||
this.blogManager = blogManager;
|
|
||||||
eventBus.addListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCleared() {
|
|
||||||
super.onCleared();
|
|
||||||
eventBus.removeListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DatabaseExecutor
|
|
||||||
protected List<BlogPostItem> loadBlogPosts(Transaction txn, GroupId groupId)
|
|
||||||
throws DbException {
|
|
||||||
long start = now();
|
|
||||||
List<BlogPostHeader> headers =
|
|
||||||
blogManager.getPostHeaders(txn, groupId);
|
|
||||||
logDuration(LOG, "Loading headers", start);
|
|
||||||
List<BlogPostItem> items = new ArrayList<>(headers.size());
|
|
||||||
start = now();
|
|
||||||
for (BlogPostHeader h : headers) {
|
|
||||||
BlogPostItem item = getItem(txn, h);
|
|
||||||
items.add(item);
|
|
||||||
}
|
|
||||||
logDuration(LOG, "Loading bodies", start);
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
@DatabaseExecutor
|
|
||||||
protected BlogPostItem getItem(Transaction txn, BlogPostHeader h)
|
|
||||||
throws DbException {
|
|
||||||
String text;
|
|
||||||
if (h instanceof BlogCommentHeader) {
|
|
||||||
BlogCommentHeader c = (BlogCommentHeader) h;
|
|
||||||
BlogCommentItem item = new BlogCommentItem(c);
|
|
||||||
text = getPostText(txn, item.getPostHeader().getId());
|
|
||||||
item.setText(text);
|
|
||||||
return item;
|
|
||||||
} else {
|
|
||||||
text = getPostText(txn, h.getId());
|
|
||||||
return new BlogPostItem(h, text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@DatabaseExecutor
|
|
||||||
private String getPostText(Transaction txn, MessageId m)
|
|
||||||
throws DbException {
|
|
||||||
return HtmlUtils.clean(blogManager.getPostText(txn, m), ARTICLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
LiveData<LiveResult<BlogPostItem>> loadBlogPost(GroupId g, MessageId m) {
|
|
||||||
MutableLiveData<LiveResult<BlogPostItem>> result =
|
|
||||||
new MutableLiveData<>();
|
|
||||||
runOnDbThread(true, txn -> {
|
|
||||||
long start = now();
|
|
||||||
BlogPostHeader header = blogManager.getPostHeader(txn, g, m);
|
|
||||||
BlogPostItem item = getItem(txn, header);
|
|
||||||
logDuration(LOG, "Loading post", start);
|
|
||||||
result.postValue(new LiveResult<>(item));
|
|
||||||
}, e -> {
|
|
||||||
logException(LOG, WARNING, e);
|
|
||||||
result.postValue(new LiveResult<>(e));
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onBlogPostAdded(BlogPostHeader header, boolean local) {
|
|
||||||
runOnDbThread(true, txn -> {
|
|
||||||
BlogPostItem item = getItem(txn, header);
|
|
||||||
txn.attach(() -> onBlogPostItemAdded(item, local));
|
|
||||||
}, this::handleException);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
private void onBlogPostItemAdded(BlogPostItem item, boolean local) {
|
|
||||||
List<BlogPostItem> items = addListItem(getBlogPostItems(), item);
|
|
||||||
if (items != null) {
|
|
||||||
Collections.sort(items);
|
|
||||||
blogPosts.setValue(new LiveResult<>(new ListUpdate(local, items)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void repeatPost(BlogPostItem item, @Nullable String comment) {
|
|
||||||
runOnDbThread(() -> {
|
|
||||||
try {
|
|
||||||
LocalAuthor a = identityManager.getLocalAuthor();
|
|
||||||
Blog b = blogManager.getPersonalBlog(a);
|
|
||||||
BlogPostHeader h = item.getHeader();
|
|
||||||
blogManager.addLocalComment(a, b.getId(), comment, h);
|
|
||||||
} catch (DbException e) {
|
|
||||||
handleException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
LiveData<LiveResult<ListUpdate>> getBlogPosts() {
|
|
||||||
return blogPosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
@Nullable
|
|
||||||
protected List<BlogPostItem> getBlogPostItems() {
|
|
||||||
LiveResult<ListUpdate> value = blogPosts.getValue();
|
|
||||||
if (value == null) return null;
|
|
||||||
ListUpdate result = value.getResultOrNull();
|
|
||||||
return result == null ? null : result.getItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this after {@link ListUpdate#getPostAddedWasLocal()} was processed.
|
|
||||||
* This prevents it from getting processed again.
|
|
||||||
*/
|
|
||||||
@UiThread
|
|
||||||
void resetLocalUpdate() {
|
|
||||||
LiveResult<ListUpdate> value = blogPosts.getValue();
|
|
||||||
if (value == null) return;
|
|
||||||
ListUpdate result = value.getResultOrNull();
|
|
||||||
result.postAddedWasLocal = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ListUpdate {
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private Boolean postAddedWasLocal;
|
|
||||||
private final List<BlogPostItem> items;
|
|
||||||
|
|
||||||
ListUpdate(@Nullable Boolean postAddedWasLocal,
|
|
||||||
List<BlogPostItem> items) {
|
|
||||||
this.postAddedWasLocal = postAddedWasLocal;
|
|
||||||
this.items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return null when not a single post was added with this update.
|
|
||||||
* true when a single post was added locally and false if remotely.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public Boolean getPostAddedWasLocal() {
|
|
||||||
return postAddedWasLocal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<BlogPostItem> getItems() {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,11 +6,9 @@ import android.os.Bundle;
|
|||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
import org.briarproject.bramble.api.sync.MessageId;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.activity.BriarActivity;
|
import org.briarproject.briar.android.activity.BriarActivity;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||||
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
|
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
|
||||||
|
|
||||||
@@ -18,10 +16,6 @@ import javax.annotation.Nullable;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
|
||||||
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
|
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
@@ -29,16 +23,7 @@ public class BlogActivity extends BriarActivity
|
|||||||
implements BaseFragmentListener {
|
implements BaseFragmentListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ViewModelProvider.Factory viewModelFactory;
|
BlogController blogController;
|
||||||
|
|
||||||
private BlogViewModel viewModel;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void injectActivity(ActivityComponent component) {
|
|
||||||
component.inject(this);
|
|
||||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
|
||||||
.get(BlogViewModel.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle state) {
|
public void onCreate(@Nullable Bundle state) {
|
||||||
@@ -46,46 +31,32 @@ public class BlogActivity extends BriarActivity
|
|||||||
|
|
||||||
// GroupId from Intent
|
// GroupId from Intent
|
||||||
Intent i = getIntent();
|
Intent i = getIntent();
|
||||||
GroupId groupId =
|
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||||
new GroupId(requireNonNull(i.getByteArrayExtra(GROUP_ID)));
|
if (b == null) throw new IllegalStateException("No group ID in intent");
|
||||||
// Get post info from intent
|
GroupId groupId = new GroupId(b);
|
||||||
@Nullable byte[] postId = i.getByteArrayExtra(POST_ID);
|
blogController.setGroupId(groupId);
|
||||||
|
|
||||||
viewModel.setGroupId(groupId, postId == null);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_fragment_container_toolbar);
|
setContentView(R.layout.activity_fragment_container_toolbar);
|
||||||
Toolbar toolbar = setUpCustomToolbar(false);
|
Toolbar toolbar = setUpCustomToolbar(false);
|
||||||
|
|
||||||
// Open Sharing Status on Toolbar click
|
// Open Sharing Status on Toolbar click
|
||||||
toolbar.setOnClickListener(v -> {
|
if (toolbar != null) {
|
||||||
Intent i1 = new Intent(BlogActivity.this,
|
toolbar.setOnClickListener(v -> {
|
||||||
BlogSharingStatusActivity.class);
|
Intent i1 = new Intent(BlogActivity.this,
|
||||||
i1.putExtra(GROUP_ID, groupId.getBytes());
|
BlogSharingStatusActivity.class);
|
||||||
startActivity(i1);
|
i1.putExtra(GROUP_ID, groupId.getBytes());
|
||||||
});
|
startActivity(i1);
|
||||||
|
});
|
||||||
viewModel.getBlog().observe(this, blog ->
|
}
|
||||||
setTitle(blog.getBlog().getAuthor().getName())
|
|
||||||
);
|
|
||||||
viewModel.getSharingInfo().observe(this, info ->
|
|
||||||
setToolbarSubTitle(info.total, info.online)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
if (postId == null) {
|
showInitialFragment(BlogFragment.newInstance(groupId));
|
||||||
showInitialFragment(BlogFragment.newInstance(groupId));
|
|
||||||
} else {
|
|
||||||
MessageId messageId = new MessageId(postId);
|
|
||||||
BaseFragment f =
|
|
||||||
BlogPostFragment.newInstance(groupId, messageId);
|
|
||||||
showInitialFragment(f);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setToolbarSubTitle(int total, int online) {
|
@Override
|
||||||
requireNonNull(getSupportActionBar())
|
public void injectActivity(ActivityComponent component) {
|
||||||
.setSubtitle(getString(R.string.shared_with, total, online));
|
component.inject(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import java.util.Collections;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.concurrent.NotThreadSafe;
|
// This class is not thread-safe
|
||||||
|
|
||||||
@NotThreadSafe
|
|
||||||
class BlogCommentItem extends BlogPostItem {
|
class BlogCommentItem extends BlogPostItem {
|
||||||
|
|
||||||
private static final BlogCommentComparator COMPARATOR =
|
private static final BlogCommentComparator COMPARATOR =
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public interface BlogController extends BaseController {
|
||||||
|
|
||||||
|
void setGroupId(GroupId g);
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void setBlogSharingListener(BlogSharingListener listener);
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void unsetBlogSharingListener(BlogSharingListener listener);
|
||||||
|
|
||||||
|
void loadBlogPosts(
|
||||||
|
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
|
||||||
|
|
||||||
|
void loadBlogPost(MessageId m,
|
||||||
|
ResultExceptionHandler<BlogPostItem, DbException> handler);
|
||||||
|
|
||||||
|
void loadBlog(ResultExceptionHandler<BlogItem, DbException> handler);
|
||||||
|
|
||||||
|
void deleteBlog(ResultExceptionHandler<Void, DbException> handler);
|
||||||
|
|
||||||
|
void loadSharingContacts(
|
||||||
|
ResultExceptionHandler<Collection<ContactId>, DbException> handler);
|
||||||
|
|
||||||
|
interface BlogSharingListener extends BlogListener {
|
||||||
|
@UiThread
|
||||||
|
void onBlogInvitationAccepted(ContactId c);
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void onBlogLeft(ContactId c);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Activity;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.db.TransactionManager;
|
|
||||||
import org.briarproject.bramble.api.event.Event;
|
import org.briarproject.bramble.api.event.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
import org.briarproject.briar.android.controller.ActivityLifecycleController;
|
||||||
import org.briarproject.briar.android.sharing.SharingController;
|
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.briar.android.sharing.SharingController.SharingInfo;
|
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.blog.Blog;
|
import org.briarproject.briar.api.blog.Blog;
|
||||||
import org.briarproject.briar.api.blog.BlogInvitationResponse;
|
import org.briarproject.briar.api.blog.BlogInvitationResponse;
|
||||||
@@ -35,54 +35,85 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class BlogViewModel extends BaseViewModel {
|
class BlogControllerImpl extends BaseControllerImpl
|
||||||
|
implements ActivityLifecycleController, BlogController, EventListener {
|
||||||
|
|
||||||
private static final Logger LOG = getLogger(BlogViewModel.class.getName());
|
private static final Logger LOG =
|
||||||
|
Logger.getLogger(BlogControllerImpl.class.getName());
|
||||||
|
|
||||||
private final BlogSharingManager blogSharingManager;
|
private final BlogSharingManager blogSharingManager;
|
||||||
private final SharingController sharingController;
|
|
||||||
|
|
||||||
private volatile GroupId groupId;
|
// UI thread
|
||||||
|
@Nullable
|
||||||
|
private BlogSharingListener listener;
|
||||||
|
|
||||||
private final MutableLiveData<BlogItem> blog = new MutableLiveData<>();
|
private volatile GroupId groupId = null;
|
||||||
private final MutableLiveData<Boolean> blogRemoved =
|
|
||||||
new MutableLiveData<>();
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
BlogViewModel(Application application,
|
BlogControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
@DatabaseExecutor Executor dbExecutor,
|
LifecycleManager lifecycleManager, EventBus eventBus,
|
||||||
LifecycleManager lifecycleManager,
|
|
||||||
TransactionManager db,
|
|
||||||
AndroidExecutor androidExecutor,
|
|
||||||
EventBus eventBus,
|
|
||||||
IdentityManager identityManager,
|
|
||||||
AndroidNotificationManager notificationManager,
|
AndroidNotificationManager notificationManager,
|
||||||
BlogManager blogManager,
|
IdentityManager identityManager, BlogManager blogManager,
|
||||||
BlogSharingManager blogSharingManager,
|
BlogSharingManager blogSharingManager) {
|
||||||
SharingController sharingController) {
|
super(dbExecutor, lifecycleManager, eventBus, notificationManager,
|
||||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
|
identityManager, blogManager);
|
||||||
eventBus, identityManager, notificationManager, blogManager);
|
|
||||||
this.blogSharingManager = blogSharingManager;
|
this.blogSharingManager = blogSharingManager;
|
||||||
this.sharingController = sharingController;
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreate(Activity activity) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityStart() {
|
||||||
|
super.onStart();
|
||||||
|
notificationManager.blockNotification(groupId);
|
||||||
|
notificationManager.clearBlogPostNotification(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityStop() {
|
||||||
|
super.onStop();
|
||||||
|
notificationManager.unblockNotification(groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityDestroy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setGroupId(GroupId g) {
|
||||||
|
groupId = g;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBlogSharingListener(BlogSharingListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unsetBlogSharingListener(BlogSharingListener listener) {
|
||||||
|
if (this.listener == listener) this.listener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void eventOccurred(Event e) {
|
public void eventOccurred(Event e) {
|
||||||
|
if (groupId == null || listener == null)
|
||||||
|
throw new IllegalStateException();
|
||||||
if (e instanceof BlogPostAddedEvent) {
|
if (e instanceof BlogPostAddedEvent) {
|
||||||
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
|
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
|
||||||
if (b.getGroupId().equals(groupId)) {
|
if (b.getGroupId().equals(groupId)) {
|
||||||
LOG.info("Blog post added");
|
LOG.info("Blog post added");
|
||||||
onBlogPostAdded(b.getHeader(), b.isLocal());
|
listener.onBlogPostAdded(b.getHeader(), b.isLocal());
|
||||||
}
|
}
|
||||||
} else if (e instanceof BlogInvitationResponseReceivedEvent) {
|
} else if (e instanceof BlogInvitationResponseReceivedEvent) {
|
||||||
BlogInvitationResponseReceivedEvent b =
|
BlogInvitationResponseReceivedEvent b =
|
||||||
@@ -90,36 +121,41 @@ class BlogViewModel extends BaseViewModel {
|
|||||||
BlogInvitationResponse r = b.getMessageHeader();
|
BlogInvitationResponse r = b.getMessageHeader();
|
||||||
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
|
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
|
||||||
LOG.info("Blog invitation accepted");
|
LOG.info("Blog invitation accepted");
|
||||||
sharingController.add(b.getContactId());
|
listener.onBlogInvitationAccepted(b.getContactId());
|
||||||
}
|
}
|
||||||
} else if (e instanceof ContactLeftShareableEvent) {
|
} else if (e instanceof ContactLeftShareableEvent) {
|
||||||
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
|
ContactLeftShareableEvent s = (ContactLeftShareableEvent) e;
|
||||||
if (s.getGroupId().equals(groupId)) {
|
if (s.getGroupId().equals(groupId)) {
|
||||||
LOG.info("Blog left by contact");
|
LOG.info("Blog left by contact");
|
||||||
sharingController.remove(s.getContactId());
|
listener.onBlogLeft(s.getContactId());
|
||||||
}
|
}
|
||||||
} else if (e instanceof GroupRemovedEvent) {
|
} else if (e instanceof GroupRemovedEvent) {
|
||||||
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
||||||
if (g.getGroup().getId().equals(groupId)) {
|
if (g.getGroup().getId().equals(groupId)) {
|
||||||
LOG.info("Blog removed");
|
LOG.info("Blog removed");
|
||||||
blogRemoved.setValue(true);
|
listener.onBlogRemoved();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* Set this before calling any other methods.
|
public void loadBlogPosts(
|
||||||
*/
|
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
|
||||||
@UiThread
|
if (groupId == null) throw new IllegalStateException();
|
||||||
public void setGroupId(GroupId groupId, boolean loadAllPosts) {
|
loadBlogPosts(groupId, handler);
|
||||||
if (this.groupId == groupId) return; // configuration change
|
|
||||||
this.groupId = groupId;
|
|
||||||
loadBlog(groupId);
|
|
||||||
if (loadAllPosts) loadBlogPosts(groupId);
|
|
||||||
loadSharingContacts(groupId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadBlog(GroupId groupId) {
|
@Override
|
||||||
|
public void loadBlogPost(MessageId m,
|
||||||
|
ResultExceptionHandler<BlogPostItem, DbException> handler) {
|
||||||
|
if (groupId == null) throw new IllegalStateException();
|
||||||
|
loadBlogPost(groupId, m, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadBlog(
|
||||||
|
ResultExceptionHandler<BlogItem, DbException> handler) {
|
||||||
|
if (groupId == null) throw new IllegalStateException();
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
long start = now();
|
long start = now();
|
||||||
@@ -127,65 +163,50 @@ class BlogViewModel extends BaseViewModel {
|
|||||||
Blog b = blogManager.getBlog(groupId);
|
Blog b = blogManager.getBlog(groupId);
|
||||||
boolean ours = a.getId().equals(b.getAuthor().getId());
|
boolean ours = a.getId().equals(b.getAuthor().getId());
|
||||||
boolean removable = blogManager.canBeRemoved(b);
|
boolean removable = blogManager.canBeRemoved(b);
|
||||||
blog.postValue(new BlogItem(b, ours, removable));
|
BlogItem blog = new BlogItem(b, ours, removable);
|
||||||
logDuration(LOG, "Loading blog", start);
|
logDuration(LOG, "Loading blog", start);
|
||||||
|
handler.onResult(blog);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
handleException(e);
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void blockAndClearNotifications() {
|
@Override
|
||||||
notificationManager.blockNotification(groupId);
|
public void deleteBlog(ResultExceptionHandler<Void, DbException> handler) {
|
||||||
notificationManager.clearBlogPostNotification(groupId);
|
if (groupId == null) throw new IllegalStateException();
|
||||||
}
|
|
||||||
|
|
||||||
void unblockNotifications() {
|
|
||||||
notificationManager.unblockNotification(groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadBlogPosts(GroupId groupId) {
|
|
||||||
loadFromDb(txn -> new ListUpdate(null, loadBlogPosts(txn, groupId)),
|
|
||||||
blogPosts::setValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadSharingContacts(GroupId groupId) {
|
|
||||||
runOnDbThread(true, txn -> {
|
|
||||||
Collection<Contact> contacts =
|
|
||||||
blogSharingManager.getSharedWith(txn, groupId);
|
|
||||||
txn.attach(() -> onSharingContactsLoaded(contacts));
|
|
||||||
}, this::handleException);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
private void onSharingContactsLoaded(Collection<Contact> contacts) {
|
|
||||||
Collection<ContactId> contactIds = new ArrayList<>(contacts.size());
|
|
||||||
for (Contact c : contacts) contactIds.add(c.getId());
|
|
||||||
sharingController.addAll(contactIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
void deleteBlog() {
|
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
long start = now();
|
long start = now();
|
||||||
Blog b = blogManager.getBlog(groupId);
|
Blog b = blogManager.getBlog(groupId);
|
||||||
blogManager.removeBlog(b);
|
blogManager.removeBlog(b);
|
||||||
logDuration(LOG, "Removing blog", start);
|
logDuration(LOG, "Removing blog", start);
|
||||||
|
handler.onResult(null);
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
handleException(e);
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveData<BlogItem> getBlog() {
|
@Override
|
||||||
return blog;
|
public void loadSharingContacts(
|
||||||
|
ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
|
||||||
|
if (groupId == null) throw new IllegalStateException();
|
||||||
|
runOnDbThread(() -> {
|
||||||
|
try {
|
||||||
|
Collection<Contact> contacts =
|
||||||
|
blogSharingManager.getSharedWith(groupId);
|
||||||
|
Collection<ContactId> contactIds =
|
||||||
|
new ArrayList<>(contacts.size());
|
||||||
|
for (Contact c : contacts) contactIds.add(c.getId());
|
||||||
|
handler.onResult(contactIds);
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveData<Boolean> getBlogRemoved() {
|
|
||||||
return blogRemoved;
|
|
||||||
}
|
|
||||||
|
|
||||||
LiveData<SharingInfo> getSharingInfo() {
|
|
||||||
return sharingController.getSharingInfo();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@@ -10,25 +12,33 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.identity.Author;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate;
|
import org.briarproject.briar.android.activity.BriarActivity;
|
||||||
|
import org.briarproject.briar.android.blog.BlogController.BlogSharingListener;
|
||||||
|
import org.briarproject.briar.android.controller.SharingController;
|
||||||
|
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
|
import org.briarproject.briar.android.sharing.BlogSharingStatusActivity;
|
||||||
import org.briarproject.briar.android.sharing.ShareBlogActivity;
|
import org.briarproject.briar.android.sharing.ShareBlogActivity;
|
||||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||||
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
import org.briarproject.briar.api.blog.BlogPostHeader;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
|
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
|
||||||
|
|
||||||
@@ -38,22 +48,31 @@ import static android.widget.Toast.LENGTH_SHORT;
|
|||||||
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
|
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
|
||||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
|
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_BLOG;
|
||||||
|
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
|
||||||
|
import static org.briarproject.briar.android.controller.SharingController.SharingListener;
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class BlogFragment extends BaseFragment
|
public class BlogFragment extends BaseFragment
|
||||||
implements OnBlogPostClickListener {
|
implements BlogSharingListener, SharingListener,
|
||||||
|
OnBlogPostClickListener {
|
||||||
|
|
||||||
private final static String TAG = BlogFragment.class.getName();
|
private final static String TAG = BlogFragment.class.getName();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ViewModelProvider.Factory viewModelFactory;
|
BlogController blogController;
|
||||||
|
@Inject
|
||||||
|
SharingController sharingController;
|
||||||
|
@Nullable
|
||||||
|
private Parcelable layoutManagerState;
|
||||||
|
|
||||||
private GroupId groupId;
|
private GroupId groupId;
|
||||||
private BlogViewModel viewModel;
|
private BlogPostAdapter adapter;
|
||||||
private final BlogPostAdapter adapter = new BlogPostAdapter(false, this);
|
private LayoutManager layoutManager;
|
||||||
private BriarRecyclerView list;
|
private BriarRecyclerView list;
|
||||||
|
private MenuItem writeButton, deleteButton;
|
||||||
|
private boolean isMyBlog = false, canDeleteBlog = false;
|
||||||
|
|
||||||
static BlogFragment newInstance(GroupId groupId) {
|
static BlogFragment newInstance(GroupId groupId) {
|
||||||
BlogFragment f = new BlogFragment();
|
BlogFragment f = new BlogFragment();
|
||||||
@@ -68,8 +87,8 @@ public class BlogFragment extends BaseFragment
|
|||||||
@Override
|
@Override
|
||||||
public void injectFragment(ActivityComponent component) {
|
public void injectFragment(ActivityComponent component) {
|
||||||
component.inject(this);
|
component.inject(this);
|
||||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
blogController.setBlogSharingListener(this);
|
||||||
.get(BlogViewModel.class);
|
sharingController.setSharingListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -84,82 +103,106 @@ public class BlogFragment extends BaseFragment
|
|||||||
|
|
||||||
View v = inflater.inflate(R.layout.fragment_blog, container, false);
|
View v = inflater.inflate(R.layout.fragment_blog, container, false);
|
||||||
|
|
||||||
|
adapter = new BlogPostAdapter(requireActivity(), this,
|
||||||
|
getFragmentManager());
|
||||||
list = v.findViewById(R.id.postList);
|
list = v.findViewById(R.id.postList);
|
||||||
LayoutManager layoutManager = new LinearLayoutManager(getActivity());
|
layoutManager = new LinearLayoutManager(getActivity());
|
||||||
list.setLayoutManager(layoutManager);
|
list.setLayoutManager(layoutManager);
|
||||||
list.setAdapter(adapter);
|
list.setAdapter(adapter);
|
||||||
list.showProgressBar();
|
list.showProgressBar();
|
||||||
list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
|
list.setEmptyText(getString(R.string.blogs_other_blog_empty_state));
|
||||||
|
|
||||||
viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result ->
|
if (savedInstanceState != null) {
|
||||||
result.onError(this::handleException)
|
layoutManagerState =
|
||||||
.onSuccess(this::onBlogPostsLoaded)
|
savedInstanceState.getParcelable("layoutManager");
|
||||||
);
|
}
|
||||||
viewModel.getBlogRemoved().observe(getViewLifecycleOwner(), removed -> {
|
|
||||||
if (removed) finish();
|
|
||||||
});
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
viewModel.blockAndClearNotifications();
|
sharingController.onStart();
|
||||||
|
loadBlog();
|
||||||
|
loadSharedContacts();
|
||||||
|
loadBlogPosts(false);
|
||||||
list.startPeriodicUpdate();
|
list.startPeriodicUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
viewModel.unblockNotifications();
|
sharingController.onStop();
|
||||||
list.stopPeriodicUpdate();
|
list.stopPeriodicUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
blogController.unsetBlogSharingListener(this);
|
||||||
|
sharingController.unsetSharingListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
if (layoutManager != null) {
|
||||||
|
layoutManagerState = layoutManager.onSaveInstanceState();
|
||||||
|
outState.putParcelable("layoutManager", layoutManagerState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.blogs_blog_actions, menu);
|
inflater.inflate(R.menu.blogs_blog_actions, menu);
|
||||||
MenuItem writeButton = menu.findItem(R.id.action_write_blog_post);
|
writeButton = menu.findItem(R.id.action_write_blog_post);
|
||||||
MenuItem deleteButton = menu.findItem(R.id.action_blog_delete);
|
if (isMyBlog) writeButton.setVisible(true);
|
||||||
viewModel.getBlog().observe(getViewLifecycleOwner(), blog -> {
|
deleteButton = menu.findItem(R.id.action_blog_delete);
|
||||||
if (blog.isOurs()) writeButton.setVisible(true);
|
if (canDeleteBlog) deleteButton.setEnabled(true);
|
||||||
if (blog.canBeRemoved()) deleteButton.setEnabled(true);
|
|
||||||
});
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
super.onCreateOptionsMenu(menu, inflater);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
int itemId = item.getItemId();
|
switch (item.getItemId()) {
|
||||||
if (itemId == R.id.action_write_blog_post) {
|
case R.id.action_write_blog_post:
|
||||||
Intent i = new Intent(getActivity(), WriteBlogPostActivity.class);
|
Intent i = new Intent(getActivity(),
|
||||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
WriteBlogPostActivity.class);
|
||||||
startActivity(i);
|
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||||
return true;
|
startActivityForResult(i, REQUEST_WRITE_BLOG_POST);
|
||||||
} else if (itemId == R.id.action_blog_share) {
|
return true;
|
||||||
Intent i = new Intent(getActivity(), ShareBlogActivity.class);
|
case R.id.action_blog_share:
|
||||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
Intent i2 = new Intent(getActivity(), ShareBlogActivity.class);
|
||||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
startActivityForResult(i, REQUEST_SHARE_BLOG);
|
i2.putExtra(GROUP_ID, groupId.getBytes());
|
||||||
return true;
|
startActivityForResult(i2, REQUEST_SHARE_BLOG);
|
||||||
} else if (itemId == R.id.action_blog_sharing_status) {
|
return true;
|
||||||
Intent i =
|
case R.id.action_blog_sharing_status:
|
||||||
new Intent(getActivity(), BlogSharingStatusActivity.class);
|
Intent i3 = new Intent(getActivity(),
|
||||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
BlogSharingStatusActivity.class);
|
||||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
startActivity(i);
|
i3.putExtra(GROUP_ID, groupId.getBytes());
|
||||||
return true;
|
startActivity(i3);
|
||||||
} else if (itemId == R.id.action_blog_delete) {
|
return true;
|
||||||
showDeleteDialog();
|
case R.id.action_blog_delete:
|
||||||
return true;
|
showDeleteDialog();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityResult(int request, int result,
|
public void onActivityResult(int request, int result,
|
||||||
@Nullable Intent data) {
|
@Nullable Intent data) {
|
||||||
super.onActivityResult(request, result, data);
|
super.onActivityResult(request, result, data);
|
||||||
if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
|
|
||||||
|
if (request == REQUEST_WRITE_BLOG_POST && result == RESULT_OK) {
|
||||||
|
displaySnackbar(R.string.blogs_blog_post_created, true);
|
||||||
|
loadBlogPosts(true);
|
||||||
|
} else if (request == REQUEST_SHARE_BLOG && result == RESULT_OK) {
|
||||||
displaySnackbar(R.string.blogs_sharing_snackbar, false);
|
displaySnackbar(R.string.blogs_sharing_snackbar, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,26 +212,35 @@ public class BlogFragment extends BaseFragment
|
|||||||
return TAG;
|
return TAG;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onBlogPostsLoaded(ListUpdate update) {
|
@Override
|
||||||
adapter.submitList(update.getItems(), () -> {
|
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
|
||||||
Boolean wasLocal = update.getPostAddedWasLocal();
|
blogController.loadBlogPost(header,
|
||||||
if (wasLocal != null && wasLocal) {
|
new UiResultExceptionHandler<BlogPostItem, DbException>(
|
||||||
list.scrollToPosition(0);
|
this) {
|
||||||
displaySnackbar(R.string.blogs_blog_post_created,
|
@Override
|
||||||
false);
|
public void onResultUi(BlogPostItem post) {
|
||||||
} else if (wasLocal != null) {
|
adapter.add(post);
|
||||||
displaySnackbar(R.string.blogs_blog_post_received,
|
if (local) {
|
||||||
true);
|
list.scrollToPosition(0);
|
||||||
}
|
displaySnackbar(R.string.blogs_blog_post_created,
|
||||||
viewModel.resetLocalUpdate();
|
false);
|
||||||
list.showData();
|
} else {
|
||||||
});
|
displaySnackbar(R.string.blogs_blog_post_received,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBlogPostClick(BlogPostItem post) {
|
public void onBlogPostClick(BlogPostItem post) {
|
||||||
BlogPostFragment f =
|
BlogPostFragment f = BlogPostFragment.newInstance(post.getId());
|
||||||
BlogPostFragment.newInstance(groupId, post.getId());
|
|
||||||
showNextFragment(f);
|
showNextFragment(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,10 +256,111 @@ public class BlogFragment extends BaseFragment
|
|||||||
getContext().startActivity(i);
|
getContext().startActivity(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadBlogPosts(boolean reload) {
|
||||||
|
blogController.loadBlogPosts(
|
||||||
|
new UiResultExceptionHandler<Collection<BlogPostItem>,
|
||||||
|
DbException>(this) {
|
||||||
|
@Override
|
||||||
|
public void onResultUi(Collection<BlogPostItem> posts) {
|
||||||
|
if (posts.isEmpty()) {
|
||||||
|
list.showData();
|
||||||
|
} else {
|
||||||
|
adapter.addAll(posts);
|
||||||
|
if (reload || layoutManagerState == null) {
|
||||||
|
list.scrollToPosition(0);
|
||||||
|
} else {
|
||||||
|
layoutManager.onRestoreInstanceState(
|
||||||
|
layoutManagerState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadBlog() {
|
||||||
|
blogController.loadBlog(
|
||||||
|
new UiResultExceptionHandler<BlogItem, DbException>(this) {
|
||||||
|
@Override
|
||||||
|
public void onResultUi(BlogItem blog) {
|
||||||
|
setToolbarTitle(blog.getBlog().getAuthor());
|
||||||
|
if (blog.isOurs())
|
||||||
|
showWriteButton();
|
||||||
|
if (blog.canBeRemoved())
|
||||||
|
enableDeleteButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setToolbarTitle(Author a) {
|
||||||
|
getActivity().setTitle(a.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSharedContacts() {
|
||||||
|
blogController.loadSharingContacts(
|
||||||
|
new UiResultExceptionHandler<Collection<ContactId>,
|
||||||
|
DbException>(this) {
|
||||||
|
@Override
|
||||||
|
public void onResultUi(Collection<ContactId> contacts) {
|
||||||
|
sharingController.addAll(contacts);
|
||||||
|
int online = sharingController.getOnlineCount();
|
||||||
|
setToolbarSubTitle(contacts.size(), online);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLinkClick(String url) {
|
public void onBlogInvitationAccepted(ContactId c) {
|
||||||
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
|
sharingController.add(c);
|
||||||
f.show(getParentFragmentManager(), f.getUniqueTag());
|
setToolbarSubTitle(sharingController.getTotalCount(),
|
||||||
|
sharingController.getOnlineCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlogLeft(ContactId c) {
|
||||||
|
sharingController.remove(c);
|
||||||
|
setToolbarSubTitle(sharingController.getTotalCount(),
|
||||||
|
sharingController.getOnlineCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSharingInfoUpdated(int total, int online) {
|
||||||
|
setToolbarSubTitle(total, online);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setToolbarSubTitle(int total, int online) {
|
||||||
|
ActionBar actionBar =
|
||||||
|
((BriarActivity) getActivity()).getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setSubtitle(
|
||||||
|
getString(R.string.shared_with, total, online));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showWriteButton() {
|
||||||
|
isMyBlog = true;
|
||||||
|
if (writeButton != null)
|
||||||
|
writeButton.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enableDeleteButton() {
|
||||||
|
canDeleteBlog = true;
|
||||||
|
if (deleteButton != null)
|
||||||
|
deleteButton.setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void displaySnackbar(int stringId, boolean scroll) {
|
private void displaySnackbar(int stringId, boolean scroll) {
|
||||||
@@ -220,21 +373,38 @@ public class BlogFragment extends BaseFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showDeleteDialog() {
|
private void showDeleteDialog() {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(requireContext(),
|
DialogInterface.OnClickListener okListener =
|
||||||
|
(dialog, which) -> deleteBlog();
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
|
||||||
R.style.BriarDialogTheme);
|
R.style.BriarDialogTheme);
|
||||||
builder.setTitle(getString(R.string.blogs_remove_blog));
|
builder.setTitle(getString(R.string.blogs_remove_blog));
|
||||||
builder.setMessage(
|
builder.setMessage(
|
||||||
getString(R.string.blogs_remove_blog_dialog_message));
|
getString(R.string.blogs_remove_blog_dialog_message));
|
||||||
builder.setPositiveButton(R.string.cancel, null);
|
builder.setPositiveButton(R.string.cancel, null);
|
||||||
builder.setNegativeButton(R.string.blogs_remove_blog_ok,
|
builder.setNegativeButton(R.string.blogs_remove_blog_ok, okListener);
|
||||||
(dialog, which) -> deleteBlog());
|
|
||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteBlog() {
|
private void deleteBlog() {
|
||||||
viewModel.deleteBlog();
|
blogController.deleteBlog(
|
||||||
Toast.makeText(getActivity(), R.string.blogs_blog_removed, LENGTH_SHORT)
|
new UiResultExceptionHandler<Void, DbException>(this) {
|
||||||
.show();
|
@Override
|
||||||
|
public void onResultUi(Void result) {
|
||||||
|
Toast.makeText(getActivity(),
|
||||||
|
R.string.blogs_blog_removed, LENGTH_SHORT)
|
||||||
|
.show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlogRemoved() {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,35 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
import org.briarproject.briar.android.activity.ActivityScope;
|
||||||
|
import org.briarproject.briar.android.activity.BaseActivity;
|
||||||
|
import org.briarproject.briar.android.controller.SharingController;
|
||||||
|
import org.briarproject.briar.android.controller.SharingControllerImpl;
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
import dagger.Binds;
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.multibindings.IntoMap;
|
import dagger.Provides;
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
public interface BlogModule {
|
public class BlogModule {
|
||||||
|
|
||||||
@Binds
|
@ActivityScope
|
||||||
@IntoMap
|
@Provides
|
||||||
@ViewModelKey(FeedViewModel.class)
|
BlogController provideBlogController(BaseActivity activity,
|
||||||
ViewModel bindFeedViewModel(FeedViewModel feedViewModel);
|
BlogControllerImpl blogController) {
|
||||||
|
activity.addLifecycleController(blogController);
|
||||||
|
return blogController;
|
||||||
|
}
|
||||||
|
|
||||||
@Binds
|
@ActivityScope
|
||||||
@IntoMap
|
@Provides
|
||||||
@ViewModelKey(BlogViewModel.class)
|
FeedController provideFeedController(FeedControllerImpl feedController) {
|
||||||
ViewModel bindBlogViewModel(BlogViewModel blogViewModel);
|
return feedController;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ActivityScope
|
||||||
|
@Provides
|
||||||
|
SharingController provideSharingController(
|
||||||
|
SharingControllerImpl sharingController) {
|
||||||
|
return sharingController;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -7,44 +8,52 @@ import android.view.ViewGroup;
|
|||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.util.BriarAdapter;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.ListAdapter;
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class BlogPostAdapter extends ListAdapter<BlogPostItem, BlogPostViewHolder> {
|
class BlogPostAdapter extends BriarAdapter<BlogPostItem, BlogPostViewHolder> {
|
||||||
|
|
||||||
private final boolean authorClickable;
|
|
||||||
private final OnBlogPostClickListener listener;
|
private final OnBlogPostClickListener listener;
|
||||||
|
@Nullable
|
||||||
|
private final FragmentManager fragmentManager;
|
||||||
|
|
||||||
BlogPostAdapter(boolean authorClickable, OnBlogPostClickListener listener) {
|
BlogPostAdapter(Context ctx, OnBlogPostClickListener listener,
|
||||||
super(new DiffUtil.ItemCallback<BlogPostItem>() {
|
@Nullable FragmentManager fragmentManager) {
|
||||||
@Override
|
super(ctx, BlogPostItem.class);
|
||||||
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
|
|
||||||
return a.getId().equals(b.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
|
|
||||||
return a.isRead() == b.isRead();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.authorClickable = authorClickable;
|
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
|
this.fragmentManager = fragmentManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BlogPostViewHolder onCreateViewHolder(ViewGroup parent,
|
public BlogPostViewHolder onCreateViewHolder(ViewGroup parent,
|
||||||
int viewType) {
|
int viewType) {
|
||||||
View v = LayoutInflater.from(parent.getContext()).inflate(
|
View v = LayoutInflater.from(ctx).inflate(
|
||||||
R.layout.list_item_blog_post, parent, false);
|
R.layout.list_item_blog_post, parent, false);
|
||||||
return new BlogPostViewHolder(v, false, listener, authorClickable);
|
return new BlogPostViewHolder(v, false, listener, fragmentManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(BlogPostViewHolder ui, int position) {
|
public void onBindViewHolder(BlogPostViewHolder ui, int position) {
|
||||||
ui.bindItem(getItem(position));
|
ui.bindItem(getItemAt(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(BlogPostItem a, BlogPostItem b) {
|
||||||
|
return a.compareTo(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(BlogPostItem a, BlogPostItem b) {
|
||||||
|
return a.isRead() == b.isRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(BlogPostItem a, BlogPostItem b) {
|
||||||
|
return a.getId().equals(b.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,159 +1,76 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
|
||||||
import org.briarproject.bramble.api.sync.MessageId;
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
import org.briarproject.briar.R;
|
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.blog.BaseController.BlogListener;
|
||||||
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||||
|
import org.briarproject.briar.api.blog.BlogPostHeader;
|
||||||
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
|
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
|
||||||
import static android.view.View.INVISIBLE;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
import static java.util.Objects.requireNonNull;
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
|
||||||
import static org.briarproject.briar.android.util.UiUtils.MIN_DATE_RESOLUTION;
|
|
||||||
|
|
||||||
|
@UiThread
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class BlogPostFragment extends BaseFragment
|
public class BlogPostFragment extends BasePostFragment implements BlogListener {
|
||||||
implements OnBlogPostClickListener {
|
|
||||||
|
|
||||||
private static final String TAG = BlogPostFragment.class.getName();
|
private static final String TAG = BlogPostFragment.class.getName();
|
||||||
private static final Logger LOG = getLogger(TAG);
|
|
||||||
|
|
||||||
static final String POST_ID = "briar.POST_ID";
|
|
||||||
|
|
||||||
protected BlogViewModel viewModel;
|
|
||||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
private ProgressBar progressBar;
|
|
||||||
private BlogPostViewHolder ui;
|
|
||||||
private BlogPostItem post;
|
|
||||||
private Runnable refresher;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ViewModelProvider.Factory viewModelFactory;
|
BlogController blogController;
|
||||||
|
|
||||||
static BlogPostFragment newInstance(GroupId blogId, MessageId postId) {
|
static BlogPostFragment newInstance(MessageId postId) {
|
||||||
BlogPostFragment f = new BlogPostFragment();
|
BlogPostFragment f = new BlogPostFragment();
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putByteArray(GROUP_ID, blogId.getBytes());
|
|
||||||
bundle.putByteArray(POST_ID, postId.getBytes());
|
bundle.putByteArray(POST_ID, postId.getBytes());
|
||||||
|
|
||||||
f.setArguments(bundle);
|
f.setArguments(bundle);
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void injectFragment(ActivityComponent component) {
|
|
||||||
component.inject(this);
|
|
||||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
|
||||||
.get(BlogViewModel.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater,
|
|
||||||
@Nullable ViewGroup container,
|
|
||||||
@Nullable Bundle savedInstanceState) {
|
|
||||||
Bundle args = requireArguments();
|
|
||||||
GroupId groupId =
|
|
||||||
new GroupId(requireNonNull(args.getByteArray(GROUP_ID)));
|
|
||||||
MessageId postId =
|
|
||||||
new MessageId(requireNonNull(args.getByteArray(POST_ID)));
|
|
||||||
|
|
||||||
View view = inflater.inflate(R.layout.fragment_blog_post, container,
|
|
||||||
false);
|
|
||||||
progressBar = view.findViewById(R.id.progressBar);
|
|
||||||
progressBar.setVisibility(VISIBLE);
|
|
||||||
ui = new BlogPostViewHolder(view, true, this, false);
|
|
||||||
LifecycleOwner owner = getViewLifecycleOwner();
|
|
||||||
viewModel.loadBlogPost(groupId, postId).observe(owner, result ->
|
|
||||||
result.onError(this::handleException)
|
|
||||||
.onSuccess(this::onBlogPostLoaded)
|
|
||||||
);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
startPeriodicUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
stopPeriodicUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
private void onBlogPostLoaded(BlogPostItem post) {
|
|
||||||
progressBar.setVisibility(INVISIBLE);
|
|
||||||
this.post = post;
|
|
||||||
ui.bindItem(post);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBlogPostClick(BlogPostItem post) {
|
|
||||||
// We're already there
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthorClick(BlogPostItem post) {
|
|
||||||
Intent i = new Intent(requireContext(), BlogActivity.class);
|
|
||||||
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
|
|
||||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
|
||||||
requireContext().startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLinkClick(String url) {
|
|
||||||
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
|
|
||||||
f.show(getParentFragmentManager(), f.getUniqueTag());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startPeriodicUpdate() {
|
|
||||||
refresher = () -> {
|
|
||||||
LOG.info("Updating Content...");
|
|
||||||
ui.updateDate(post.getTimestamp());
|
|
||||||
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
|
|
||||||
};
|
|
||||||
LOG.info("Adding Handler Callback");
|
|
||||||
handler.postDelayed(refresher, MIN_DATE_RESOLUTION);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopPeriodicUpdate() {
|
|
||||||
if (refresher != null) {
|
|
||||||
LOG.info("Removing Handler Callback");
|
|
||||||
handler.removeCallbacks(refresher);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUniqueTag() {
|
public String getUniqueTag() {
|
||||||
return TAG;
|
return TAG;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectFragment(ActivityComponent component) {
|
||||||
|
component.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
blogController.loadBlogPost(postId,
|
||||||
|
new UiResultExceptionHandler<BlogPostItem, DbException>(
|
||||||
|
this) {
|
||||||
|
@Override
|
||||||
|
public void onResultUi(BlogPostItem post) {
|
||||||
|
onBlogPostLoaded(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
|
||||||
|
// doesn't matter here
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlogRemoved() {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
|
|||||||
private final BlogPostHeader header;
|
private final BlogPostHeader header;
|
||||||
@Nullable
|
@Nullable
|
||||||
protected String text;
|
protected String text;
|
||||||
private final boolean read;
|
private boolean read;
|
||||||
|
|
||||||
BlogPostItem(BlogPostHeader header, @Nullable String text) {
|
BlogPostItem(BlogPostHeader header, @Nullable String text) {
|
||||||
this.header = header;
|
this.header = header;
|
||||||
@@ -74,6 +74,9 @@ public class BlogPostItem implements Comparable<BlogPostItem> {
|
|||||||
|
|
||||||
protected static int compare(BlogPostHeader h1, BlogPostHeader h2) {
|
protected static int compare(BlogPostHeader h1, BlogPostHeader h2) {
|
||||||
// The newest post comes first
|
// The newest post comes first
|
||||||
return Long.compare(h2.getTimeReceived(), h1.getTimeReceived());
|
long aTime = h1.getTimeReceived(), bTime = h2.getTimeReceived();
|
||||||
|
if (aTime > bTime) return -1;
|
||||||
|
if (aTime < bTime) return 1;
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,31 +9,31 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.sync.MessageId;
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.view.AuthorView;
|
import org.briarproject.briar.android.view.AuthorView;
|
||||||
import org.briarproject.briar.api.blog.BlogCommentHeader;
|
import org.briarproject.briar.api.blog.BlogCommentHeader;
|
||||||
import org.briarproject.briar.api.blog.BlogPostHeader;
|
import org.briarproject.briar.api.blog.BlogPostHeader;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||||
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
|
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH;
|
import static org.briarproject.briar.android.util.UiUtils.TEASER_LENGTH;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.getSpanned;
|
import static org.briarproject.briar.android.util.UiUtils.getSpanned;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.getTeaser;
|
import static org.briarproject.briar.android.util.UiUtils.getTeaser;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
|
import static org.briarproject.briar.android.util.UiUtils.makeLinksClickable;
|
||||||
import static org.briarproject.briar.android.view.AuthorView.COMMENTER;
|
import static org.briarproject.briar.api.blog.MessageType.POST;
|
||||||
import static org.briarproject.briar.android.view.AuthorView.REBLOGGER;
|
|
||||||
import static org.briarproject.briar.android.view.AuthorView.RSS_FEED_REBLOGGED;
|
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@NotNullByDefault
|
|
||||||
class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private final Context ctx;
|
private final Context ctx;
|
||||||
@@ -43,16 +43,20 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
|||||||
private final ImageButton reblogButton;
|
private final ImageButton reblogButton;
|
||||||
private final TextView text;
|
private final TextView text;
|
||||||
private final ViewGroup commentContainer;
|
private final ViewGroup commentContainer;
|
||||||
private final boolean fullText, authorClickable;
|
private final boolean fullText;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
private final OnBlogPostClickListener listener;
|
private final OnBlogPostClickListener listener;
|
||||||
|
@Nullable
|
||||||
|
private final FragmentManager fragmentManager;
|
||||||
|
|
||||||
BlogPostViewHolder(View v, boolean fullText,
|
BlogPostViewHolder(View v, boolean fullText,
|
||||||
OnBlogPostClickListener listener, boolean authorClickable) {
|
@NonNull OnBlogPostClickListener listener,
|
||||||
|
@Nullable FragmentManager fragmentManager) {
|
||||||
super(v);
|
super(v);
|
||||||
this.fullText = fullText;
|
this.fullText = fullText;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.authorClickable = authorClickable;
|
this.fragmentManager = fragmentManager;
|
||||||
|
|
||||||
ctx = v.getContext();
|
ctx = v.getContext();
|
||||||
layout = v.findViewById(R.id.postLayout);
|
layout = v.findViewById(R.id.postLayout);
|
||||||
@@ -63,6 +67,10 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
|||||||
commentContainer = v.findViewById(R.id.commentContainer);
|
commentContainer = v.findViewById(R.id.commentContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setVisibility(int visibility) {
|
||||||
|
layout.setVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
void hideReblogButton() {
|
void hideReblogButton() {
|
||||||
reblogButton.setVisibility(GONE);
|
reblogButton.setVisibility(GONE);
|
||||||
}
|
}
|
||||||
@@ -79,15 +87,15 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
|||||||
return "blogPost" + id.hashCode();
|
return "blogPost" + id.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
void bindItem(BlogPostItem item) {
|
void bindItem(@Nullable BlogPostItem item) {
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
setTransitionName(item.getId());
|
setTransitionName(item.getId());
|
||||||
if (!fullText) {
|
if (!fullText) {
|
||||||
layout.setClickable(true);
|
layout.setClickable(true);
|
||||||
layout.setOnClickListener(v -> listener.onBlogPostClick(item));
|
layout.setOnClickListener(v -> listener.onBlogPostClick(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isReblog = item instanceof BlogCommentItem;
|
|
||||||
|
|
||||||
// author and date
|
// author and date
|
||||||
BlogPostHeader post = item.getPostHeader();
|
BlogPostHeader post = item.getPostHeader();
|
||||||
author.setAuthor(post.getAuthor(), post.getAuthorInfo());
|
author.setAuthor(post.getAuthor(), post.getAuthorInfo());
|
||||||
@@ -95,7 +103,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
|||||||
author.setPersona(
|
author.setPersona(
|
||||||
item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL);
|
item.isRssFeed() ? AuthorView.RSS_FEED : AuthorView.NORMAL);
|
||||||
// TODO make author clickable more often #624
|
// TODO make author clickable more often #624
|
||||||
if (authorClickable && !isReblog) {
|
if (!fullText && item.getHeader().getType() == POST) {
|
||||||
author.setAuthorClickable(v -> listener.onAuthorClick(item));
|
author.setAuthorClickable(v -> listener.onAuthorClick(item));
|
||||||
} else {
|
} else {
|
||||||
author.setAuthorNotClickable();
|
author.setAuthorNotClickable();
|
||||||
@@ -106,7 +114,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
|||||||
if (fullText) {
|
if (fullText) {
|
||||||
text.setText(postText);
|
text.setText(postText);
|
||||||
text.setTextIsSelectable(true);
|
text.setTextIsSelectable(true);
|
||||||
makeLinksClickable(text, listener::onLinkClick);
|
makeLinksClickable(text, fragmentManager);
|
||||||
} else {
|
} else {
|
||||||
text.setTextIsSelectable(false);
|
text.setTextIsSelectable(false);
|
||||||
if (postText.length() > TEASER_LENGTH)
|
if (postText.length() > TEASER_LENGTH)
|
||||||
@@ -124,33 +132,32 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder {
|
|||||||
|
|
||||||
// comments
|
// comments
|
||||||
commentContainer.removeAllViews();
|
commentContainer.removeAllViews();
|
||||||
if (isReblog) {
|
if (item instanceof BlogCommentItem) {
|
||||||
onBindComment((BlogCommentItem) item, authorClickable);
|
onBindComment((BlogCommentItem) item);
|
||||||
} else {
|
} else {
|
||||||
reblogger.setVisibility(GONE);
|
reblogger.setVisibility(GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onBindComment(BlogCommentItem item, boolean authorClickable) {
|
private void onBindComment(BlogCommentItem item) {
|
||||||
// reblogger
|
// reblogger
|
||||||
reblogger.setAuthor(item.getAuthor(), item.getAuthorInfo());
|
reblogger.setAuthor(item.getAuthor(), item.getAuthorInfo());
|
||||||
reblogger.setDate(item.getTimestamp());
|
reblogger.setDate(item.getTimestamp());
|
||||||
if (authorClickable) {
|
if (!fullText) {
|
||||||
reblogger.setAuthorClickable(v -> listener.onAuthorClick(item));
|
reblogger.setAuthorClickable(v -> listener.onAuthorClick(item));
|
||||||
} else {
|
|
||||||
reblogger.setAuthorNotClickable();
|
|
||||||
}
|
}
|
||||||
reblogger.setVisibility(VISIBLE);
|
reblogger.setVisibility(VISIBLE);
|
||||||
reblogger.setPersona(REBLOGGER);
|
reblogger.setPersona(AuthorView.REBLOGGER);
|
||||||
|
|
||||||
author.setPersona(item.getHeader().getRootPost().isRssFeed() ?
|
author.setPersona(item.getHeader().getRootPost().isRssFeed() ?
|
||||||
RSS_FEED_REBLOGGED : COMMENTER);
|
AuthorView.RSS_FEED_REBLOGGED :
|
||||||
|
AuthorView.COMMENTER);
|
||||||
|
|
||||||
// comments
|
// comments
|
||||||
// TODO use nested RecyclerView instead like we do for Image Attachments
|
|
||||||
for (BlogCommentHeader c : item.getComments()) {
|
for (BlogCommentHeader c : item.getComments()) {
|
||||||
View v = LayoutInflater.from(ctx).inflate(
|
View v = LayoutInflater.from(ctx)
|
||||||
R.layout.list_item_blog_comment, commentContainer, false);
|
.inflate(R.layout.list_item_blog_comment,
|
||||||
|
commentContainer, false);
|
||||||
|
|
||||||
AuthorView author = v.findViewById(R.id.authorView);
|
AuthorView author = v.findViewById(R.id.authorView);
|
||||||
TextView text = v.findViewById(R.id.textView);
|
TextView text = v.findViewById(R.id.textView);
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||||
|
import org.briarproject.briar.api.blog.Blog;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public interface FeedController extends BaseController {
|
||||||
|
|
||||||
|
void loadBlogPosts(
|
||||||
|
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
|
||||||
|
|
||||||
|
void loadPersonalBlog(ResultExceptionHandler<Blog, DbException> handler);
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void setFeedListener(FeedListener listener);
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void unsetFeedListener(FeedListener listener);
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
interface FeedListener extends BlogListener {
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void onBlogAdded();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.db.NoSuchGroupException;
|
||||||
|
import org.briarproject.bramble.api.db.NoSuchMessageException;
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
|
import org.briarproject.bramble.api.identity.Author;
|
||||||
|
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.event.GroupAddedEvent;
|
||||||
|
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
||||||
|
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||||
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
|
import org.briarproject.briar.api.blog.Blog;
|
||||||
|
import org.briarproject.briar.api.blog.BlogManager;
|
||||||
|
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
|
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
|
||||||
|
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
|
class FeedControllerImpl extends BaseControllerImpl implements FeedController {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
Logger.getLogger(FeedControllerImpl.class.getName());
|
||||||
|
|
||||||
|
// UI thread
|
||||||
|
@Nullable
|
||||||
|
private FeedListener listener;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
FeedControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
|
LifecycleManager lifecycleManager, EventBus eventBus,
|
||||||
|
AndroidNotificationManager notificationManager,
|
||||||
|
IdentityManager identityManager, BlogManager blogManager) {
|
||||||
|
super(dbExecutor, lifecycleManager, eventBus, notificationManager,
|
||||||
|
identityManager, blogManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
if (listener == null) throw new IllegalStateException();
|
||||||
|
notificationManager.blockAllBlogPostNotifications();
|
||||||
|
notificationManager.clearAllBlogPostNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
notificationManager.unblockAllBlogPostNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFeedListener(FeedListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unsetFeedListener(FeedListener listener) {
|
||||||
|
if (this.listener == listener) this.listener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void eventOccurred(Event e) {
|
||||||
|
if (listener == null) throw new IllegalStateException();
|
||||||
|
if (e instanceof BlogPostAddedEvent) {
|
||||||
|
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
|
||||||
|
LOG.info("Blog post added");
|
||||||
|
listener.onBlogPostAdded(b.getHeader(), b.isLocal());
|
||||||
|
} else if (e instanceof GroupAddedEvent) {
|
||||||
|
GroupAddedEvent g = (GroupAddedEvent) e;
|
||||||
|
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
|
||||||
|
LOG.info("Blog added");
|
||||||
|
listener.onBlogAdded();
|
||||||
|
}
|
||||||
|
} else if (e instanceof GroupRemovedEvent) {
|
||||||
|
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
||||||
|
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
|
||||||
|
LOG.info("Blog removed");
|
||||||
|
listener.onBlogRemoved();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadBlogPosts(
|
||||||
|
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
|
||||||
|
runOnDbThread(() -> {
|
||||||
|
try {
|
||||||
|
long start = now();
|
||||||
|
Collection<BlogPostItem> posts = new ArrayList<>();
|
||||||
|
for (Blog b : blogManager.getBlogs()) {
|
||||||
|
try {
|
||||||
|
posts.addAll(loadItems(b.getId()));
|
||||||
|
} catch (NoSuchGroupException | NoSuchMessageException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logDuration(LOG, "Loading all posts", start);
|
||||||
|
handler.onResult(posts);
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadPersonalBlog(
|
||||||
|
ResultExceptionHandler<Blog, DbException> handler) {
|
||||||
|
runOnDbThread(() -> {
|
||||||
|
try {
|
||||||
|
long start = now();
|
||||||
|
Author a = identityManager.getLocalAuthor();
|
||||||
|
Blog b = blogManager.getPersonalBlog(a);
|
||||||
|
logDuration(LOG, "Loading personal blog", start);
|
||||||
|
handler.onResult(b);
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
handler.onException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.android.blog;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@@ -9,43 +10,53 @@ import android.view.MenuItem;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
import org.briarproject.briar.android.blog.BaseViewModel.ListUpdate;
|
import org.briarproject.briar.android.blog.FeedController.FeedListener;
|
||||||
|
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||||
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
|
||||||
import org.briarproject.briar.api.blog.Blog;
|
import org.briarproject.briar.api.blog.Blog;
|
||||||
|
import org.briarproject.briar.api.blog.BlogPostHeader;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.annotation.UiThread;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
|
||||||
|
import static android.app.Activity.RESULT_OK;
|
||||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||||
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
|
import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG;
|
||||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||||
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
|
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_WRITE_BLOG_POST;
|
||||||
|
|
||||||
|
@UiThread
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class FeedFragment extends BaseFragment
|
public class FeedFragment extends BaseFragment implements
|
||||||
implements OnBlogPostClickListener {
|
OnBlogPostClickListener, FeedListener {
|
||||||
|
|
||||||
public final static String TAG = FeedFragment.class.getName();
|
public final static String TAG = FeedFragment.class.getName();
|
||||||
|
private static final Logger LOG = Logger.getLogger(TAG);
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
ViewModelProvider.Factory viewModelFactory;
|
FeedController feedController;
|
||||||
|
|
||||||
private FeedViewModel viewModel;
|
private BlogPostAdapter adapter;
|
||||||
private final BlogPostAdapter adapter = new BlogPostAdapter(true, this);
|
|
||||||
private LinearLayoutManager layoutManager;
|
private LinearLayoutManager layoutManager;
|
||||||
private BriarRecyclerView list;
|
private BriarRecyclerView list;
|
||||||
|
@Nullable
|
||||||
|
private Blog personalBlog;
|
||||||
|
@Nullable
|
||||||
|
private Parcelable layoutManagerState;
|
||||||
|
|
||||||
public static FeedFragment newInstance() {
|
public static FeedFragment newInstance() {
|
||||||
FeedFragment f = new FeedFragment();
|
FeedFragment f = new FeedFragment();
|
||||||
@@ -59,8 +70,7 @@ public class FeedFragment extends BaseFragment
|
|||||||
@Override
|
@Override
|
||||||
public void injectFragment(ActivityComponent component) {
|
public void injectFragment(ActivityComponent component) {
|
||||||
component.inject(this);
|
component.inject(this);
|
||||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
feedController.setFeedListener(this);
|
||||||
.get(FeedViewModel.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -72,6 +82,9 @@ public class FeedFragment extends BaseFragment
|
|||||||
|
|
||||||
View v = inflater.inflate(R.layout.fragment_blog, container, false);
|
View v = inflater.inflate(R.layout.fragment_blog, container, false);
|
||||||
|
|
||||||
|
adapter =
|
||||||
|
new BlogPostAdapter(getActivity(), this, getFragmentManager());
|
||||||
|
|
||||||
layoutManager = new LinearLayoutManager(getActivity());
|
layoutManager = new LinearLayoutManager(getActivity());
|
||||||
list = v.findViewById(R.id.postList);
|
list = v.findViewById(R.id.postList);
|
||||||
list.setLayoutManager(layoutManager);
|
list.setLayoutManager(layoutManager);
|
||||||
@@ -80,39 +93,103 @@ public class FeedFragment extends BaseFragment
|
|||||||
list.setEmptyText(R.string.blogs_feed_empty_state);
|
list.setEmptyText(R.string.blogs_feed_empty_state);
|
||||||
list.setEmptyAction(R.string.blogs_feed_empty_state_action);
|
list.setEmptyAction(R.string.blogs_feed_empty_state_action);
|
||||||
|
|
||||||
viewModel.getBlogPosts().observe(getViewLifecycleOwner(), result ->
|
if (savedInstanceState != null) {
|
||||||
result.onError(this::handleException)
|
layoutManagerState =
|
||||||
.onSuccess(this::onBlogPostsLoaded)
|
savedInstanceState.getParcelable("layoutManager");
|
||||||
);
|
}
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode,
|
||||||
|
@Nullable Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
|
// The BlogPostAddedEvent arrives when the controller is not listening
|
||||||
|
if (requestCode == REQUEST_WRITE_BLOG_POST && resultCode == RESULT_OK) {
|
||||||
|
showSnackBar(R.string.blogs_blog_post_created);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
viewModel.blockAndClearAllBlogPostNotifications();
|
feedController.onStart();
|
||||||
list.startPeriodicUpdate();
|
list.startPeriodicUpdate();
|
||||||
|
loadPersonalBlog();
|
||||||
|
loadBlogPosts(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
viewModel.unblockAllBlogPostNotifications();
|
feedController.onStop();
|
||||||
|
adapter.clear();
|
||||||
|
list.showProgressBar();
|
||||||
list.stopPeriodicUpdate();
|
list.stopPeriodicUpdate();
|
||||||
|
// TODO save list position in database/preferences?
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onBlogPostsLoaded(ListUpdate update) {
|
@Override
|
||||||
adapter.submitList(update.getItems(), () -> {
|
public void onDestroy() {
|
||||||
Boolean wasLocal = update.getPostAddedWasLocal();
|
super.onDestroy();
|
||||||
if (wasLocal != null && wasLocal) {
|
feedController.unsetFeedListener(this);
|
||||||
showSnackBar(R.string.blogs_blog_post_created);
|
}
|
||||||
} else if (wasLocal != null) {
|
|
||||||
showSnackBar(R.string.blogs_blog_post_received);
|
@Override
|
||||||
}
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
viewModel.resetLocalUpdate();
|
super.onSaveInstanceState(outState);
|
||||||
list.showData();
|
if (layoutManager != null) {
|
||||||
});
|
layoutManagerState = layoutManager.onSaveInstanceState();
|
||||||
|
outState.putParcelable("layoutManager", layoutManagerState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadPersonalBlog() {
|
||||||
|
feedController.loadPersonalBlog(
|
||||||
|
new UiResultExceptionHandler<Blog, DbException>(this) {
|
||||||
|
@Override
|
||||||
|
public void onResultUi(Blog b) {
|
||||||
|
personalBlog = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadBlogPosts(boolean clear) {
|
||||||
|
int revision = adapter.getRevision();
|
||||||
|
feedController.loadBlogPosts(
|
||||||
|
new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
|
||||||
|
this) {
|
||||||
|
@Override
|
||||||
|
public void onResultUi(Collection<BlogPostItem> posts) {
|
||||||
|
if (revision == adapter.getRevision()) {
|
||||||
|
adapter.incrementRevision();
|
||||||
|
if (clear) adapter.setItems(posts);
|
||||||
|
else adapter.addAll(posts);
|
||||||
|
if (posts.isEmpty()) list.showData();
|
||||||
|
if (layoutManagerState == null) {
|
||||||
|
list.scrollToPosition(0); // Scroll to the top
|
||||||
|
} else {
|
||||||
|
layoutManager.onRestoreInstanceState(
|
||||||
|
layoutManagerState);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG.info("Concurrent update, reloading");
|
||||||
|
loadBlogPosts(clear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -123,46 +200,67 @@ public class FeedFragment extends BaseFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
int itemId = item.getItemId();
|
if (personalBlog == null) return false;
|
||||||
if (itemId == R.id.action_write_blog_post) {
|
switch (item.getItemId()) {
|
||||||
Blog personalBlog = viewModel.getPersonalBlog().getValue();
|
case R.id.action_write_blog_post:
|
||||||
if (personalBlog == null) return false;
|
Intent i1 =
|
||||||
Intent i = new Intent(getActivity(), WriteBlogPostActivity.class);
|
new Intent(getActivity(), WriteBlogPostActivity.class);
|
||||||
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
|
i1.putExtra(GROUP_ID, personalBlog.getId().getBytes());
|
||||||
startActivity(i);
|
startActivityForResult(i1, REQUEST_WRITE_BLOG_POST);
|
||||||
return true;
|
return true;
|
||||||
} else if (itemId == R.id.action_rss_feeds_import) {
|
case R.id.action_rss_feeds_import:
|
||||||
Intent i = new Intent(getActivity(), RssFeedImportActivity.class);
|
Intent i2 =
|
||||||
startActivity(i);
|
new Intent(getActivity(), RssFeedImportActivity.class);
|
||||||
return true;
|
startActivity(i2);
|
||||||
} else if (itemId == R.id.action_rss_feeds_manage) {
|
return true;
|
||||||
Blog personalBlog = viewModel.getPersonalBlog().getValue();
|
case R.id.action_rss_feeds_manage:
|
||||||
if (personalBlog == null) return false;
|
Intent i3 =
|
||||||
Intent i = new Intent(getActivity(), RssFeedManageActivity.class);
|
new Intent(getActivity(), RssFeedManageActivity.class);
|
||||||
i.putExtra(GROUP_ID, personalBlog.getId().getBytes());
|
i3.putExtra(GROUP_ID, personalBlog.getId().getBytes());
|
||||||
startActivity(i);
|
startActivity(i3);
|
||||||
return true;
|
return true;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
|
||||||
|
feedController.loadBlogPost(header,
|
||||||
|
new UiResultExceptionHandler<BlogPostItem, DbException>(
|
||||||
|
this) {
|
||||||
|
@Override
|
||||||
|
public void onResultUi(BlogPostItem post) {
|
||||||
|
adapter.incrementRevision();
|
||||||
|
adapter.add(post);
|
||||||
|
if (local) {
|
||||||
|
showSnackBar(R.string.blogs_blog_post_created);
|
||||||
|
} else {
|
||||||
|
showSnackBar(R.string.blogs_blog_post_received);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBlogPostClick(BlogPostItem post) {
|
public void onBlogPostClick(BlogPostItem post) {
|
||||||
Intent i = getBlogActivityIntent(post.getGroupId());
|
FeedPostFragment f =
|
||||||
i.putExtra(POST_ID, post.getId().getBytes());
|
FeedPostFragment.newInstance(post.getGroupId(), post.getId());
|
||||||
requireContext().startActivity(i);
|
showNextFragment(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthorClick(BlogPostItem post) {
|
public void onAuthorClick(BlogPostItem post) {
|
||||||
Intent i = getBlogActivityIntent(post.getGroupId());
|
Intent i = new Intent(getContext(), BlogActivity.class);
|
||||||
requireContext().startActivity(i);
|
i.putExtra(GROUP_ID, post.getGroupId().getBytes());
|
||||||
}
|
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
getContext().startActivity(i);
|
||||||
@Override
|
|
||||||
public void onLinkClick(String url) {
|
|
||||||
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
|
|
||||||
f.show(getParentFragmentManager(), f.getUniqueTag());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -170,13 +268,6 @@ public class FeedFragment extends BaseFragment
|
|||||||
return TAG;
|
return TAG;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent getBlogActivityIntent(GroupId groupId) {
|
|
||||||
Intent i = new Intent(requireContext(), BlogActivity.class);
|
|
||||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
|
||||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showSnackBar(int stringRes) {
|
private void showSnackBar(int stringRes) {
|
||||||
int firstVisible =
|
int firstVisible =
|
||||||
layoutManager.findFirstCompletelyVisibleItemPosition();
|
layoutManager.findFirstCompletelyVisibleItemPosition();
|
||||||
@@ -192,4 +283,14 @@ public class FeedFragment extends BaseFragment
|
|||||||
sb.make(list, stringRes, LENGTH_LONG).show();
|
sb.make(list, stringRes, LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlogAdded() {
|
||||||
|
loadBlogPosts(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlogRemoved() {
|
||||||
|
loadBlogPosts(true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package org.briarproject.briar.android.blog;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
|
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
|
public class FeedPostFragment extends BasePostFragment {
|
||||||
|
|
||||||
|
private static final String TAG = FeedPostFragment.class.getName();
|
||||||
|
|
||||||
|
private GroupId blogId;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
FeedController feedController;
|
||||||
|
|
||||||
|
static FeedPostFragment newInstance(GroupId blogId, MessageId postId) {
|
||||||
|
FeedPostFragment f = new FeedPostFragment();
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putByteArray(GROUP_ID, blogId.getBytes());
|
||||||
|
bundle.putByteArray(POST_ID, postId.getBytes());
|
||||||
|
|
||||||
|
f.setArguments(bundle);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void injectFragment(ActivityComponent component) {
|
||||||
|
component.inject(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater,
|
||||||
|
@Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
Bundle args = requireArguments();
|
||||||
|
byte[] b = args.getByteArray(GROUP_ID);
|
||||||
|
if (b == null) throw new IllegalStateException("No group ID in args");
|
||||||
|
blogId = new GroupId(b);
|
||||||
|
|
||||||
|
return super.onCreateView(inflater, container, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUniqueTag() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
feedController.loadBlogPost(blogId, postId,
|
||||||
|
new UiResultExceptionHandler<BlogPostItem, DbException>(
|
||||||
|
this) {
|
||||||
|
@Override
|
||||||
|
public void onResultUi(BlogPostItem post) {
|
||||||
|
onBlogPostLoaded(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package org.briarproject.briar.android.blog;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
|
||||||
import org.briarproject.bramble.api.db.Transaction;
|
|
||||||
import org.briarproject.bramble.api.db.TransactionManager;
|
|
||||||
import org.briarproject.bramble.api.event.Event;
|
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
|
||||||
import org.briarproject.bramble.api.identity.Author;
|
|
||||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
|
||||||
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
|
||||||
import org.briarproject.briar.android.viewmodel.LiveResult;
|
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
|
||||||
import org.briarproject.briar.api.blog.Blog;
|
|
||||||
import org.briarproject.briar.api.blog.BlogManager;
|
|
||||||
import org.briarproject.briar.api.blog.event.BlogPostAddedEvent;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
|
||||||
import static org.briarproject.briar.api.blog.BlogManager.CLIENT_ID;
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
class FeedViewModel extends BaseViewModel {
|
|
||||||
|
|
||||||
private static final Logger LOG = getLogger(FeedViewModel.class.getName());
|
|
||||||
|
|
||||||
private final MutableLiveData<Blog> personalBlog = new MutableLiveData<>();
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
FeedViewModel(Application application,
|
|
||||||
@DatabaseExecutor Executor dbExecutor,
|
|
||||||
LifecycleManager lifecycleManager,
|
|
||||||
TransactionManager db,
|
|
||||||
AndroidExecutor androidExecutor,
|
|
||||||
EventBus eventBus,
|
|
||||||
IdentityManager identityManager,
|
|
||||||
AndroidNotificationManager notificationManager,
|
|
||||||
BlogManager blogManager) {
|
|
||||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
|
|
||||||
eventBus, identityManager, notificationManager, blogManager);
|
|
||||||
loadPersonalBlog();
|
|
||||||
loadAllBlogPosts();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void eventOccurred(Event e) {
|
|
||||||
if (e instanceof BlogPostAddedEvent) {
|
|
||||||
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
|
|
||||||
LOG.info("Blog post added");
|
|
||||||
onBlogPostAdded(b.getHeader(), b.isLocal());
|
|
||||||
} else if (e instanceof GroupRemovedEvent) {
|
|
||||||
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
|
||||||
if (g.getGroup().getClientId().equals(CLIENT_ID)) {
|
|
||||||
LOG.info("Blog removed");
|
|
||||||
onBlogRemoved(g.getGroup().getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void blockAndClearAllBlogPostNotifications() {
|
|
||||||
notificationManager.blockAllBlogPostNotifications();
|
|
||||||
notificationManager.clearAllBlogPostNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
void unblockAllBlogPostNotifications() {
|
|
||||||
notificationManager.unblockAllBlogPostNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadPersonalBlog() {
|
|
||||||
runOnDbThread(() -> {
|
|
||||||
try {
|
|
||||||
long start = now();
|
|
||||||
Author a = identityManager.getLocalAuthor();
|
|
||||||
Blog b = blogManager.getPersonalBlog(a);
|
|
||||||
logDuration(LOG, "Loading personal blog", start);
|
|
||||||
personalBlog.postValue(b);
|
|
||||||
} catch (DbException e) {
|
|
||||||
handleException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
LiveData<Blog> getPersonalBlog() {
|
|
||||||
return personalBlog;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadAllBlogPosts() {
|
|
||||||
loadFromDb(this::loadAllBlogPosts, blogPosts::setValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@DatabaseExecutor
|
|
||||||
private ListUpdate loadAllBlogPosts(Transaction txn)
|
|
||||||
throws DbException {
|
|
||||||
long start = now();
|
|
||||||
List<BlogPostItem> posts = new ArrayList<>();
|
|
||||||
for (GroupId g : blogManager.getBlogIds(txn)) {
|
|
||||||
posts.addAll(loadBlogPosts(txn, g));
|
|
||||||
}
|
|
||||||
Collections.sort(posts);
|
|
||||||
logDuration(LOG, "Loading all posts", start);
|
|
||||||
return new ListUpdate(null, posts);
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
private void onBlogRemoved(GroupId g) {
|
|
||||||
List<BlogPostItem> items = removeListItems(getBlogPostItems(), item ->
|
|
||||||
item.getGroupId().equals(g)
|
|
||||||
);
|
|
||||||
if (items != null) {
|
|
||||||
blogPosts.setValue(new LiveResult<>(new ListUpdate(null, items)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,4 @@ interface OnBlogPostClickListener {
|
|||||||
void onBlogPostClick(BlogPostItem post);
|
void onBlogPostClick(BlogPostItem post);
|
||||||
|
|
||||||
void onAuthorClick(BlogPostItem post);
|
void onAuthorClick(BlogPostItem post);
|
||||||
|
|
||||||
void onLinkClick(String url);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import org.briarproject.briar.android.activity.ActivityComponent;
|
|||||||
import org.briarproject.briar.android.activity.BriarActivity;
|
import org.briarproject.briar.android.activity.BriarActivity;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||||
|
|
||||||
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
|
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
|
||||||
|
|
||||||
public class ReblogActivity extends BriarActivity implements
|
public class ReblogActivity extends BriarActivity implements
|
||||||
BaseFragmentListener {
|
BaseFragmentListener {
|
||||||
@@ -39,11 +39,13 @@ public class ReblogActivity extends BriarActivity implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
if (item.getItemId() == android.R.id.home) {
|
switch (item.getItemId()) {
|
||||||
onBackPressed();
|
case android.R.id.home:
|
||||||
return true;
|
onBackPressed();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -7,17 +7,20 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
import org.briarproject.bramble.api.sync.MessageId;
|
import org.briarproject.bramble.api.sync.MessageId;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
|
import org.briarproject.briar.android.controller.handler.UiExceptionHandler;
|
||||||
|
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.view.TextInputView;
|
import org.briarproject.briar.android.view.TextInputView;
|
||||||
import org.briarproject.briar.android.view.TextSendController;
|
import org.briarproject.briar.android.view.TextSendController;
|
||||||
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||||
import org.briarproject.briar.android.widget.LinkDialogFragment;
|
import org.briarproject.briar.android.view.TextSendController.SendState;
|
||||||
import org.briarproject.briar.api.attachment.AttachmentHeader;
|
import org.briarproject.briar.api.attachment.AttachmentHeader;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -25,7 +28,8 @@ import java.util.List;
|
|||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import static android.view.View.FOCUS_DOWN;
|
import static android.view.View.FOCUS_DOWN;
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
@@ -33,7 +37,8 @@ import static android.view.View.INVISIBLE;
|
|||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
import static org.briarproject.briar.android.activity.BriarActivity.GROUP_ID;
|
||||||
import static org.briarproject.briar.android.blog.BlogPostFragment.POST_ID;
|
import static org.briarproject.briar.android.blog.BasePostFragment.POST_ID;
|
||||||
|
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
|
||||||
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@@ -42,13 +47,12 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
|||||||
|
|
||||||
public static final String TAG = ReblogFragment.class.getName();
|
public static final String TAG = ReblogFragment.class.getName();
|
||||||
|
|
||||||
@Inject
|
|
||||||
ViewModelProvider.Factory viewModelFactory;
|
|
||||||
|
|
||||||
private BlogViewModel viewModel;
|
|
||||||
private ViewHolder ui;
|
private ViewHolder ui;
|
||||||
private BlogPostItem item;
|
private BlogPostItem item;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
FeedController feedController;
|
||||||
|
|
||||||
static ReblogFragment newInstance(GroupId groupId, MessageId messageId) {
|
static ReblogFragment newInstance(GroupId groupId, MessageId messageId) {
|
||||||
ReblogFragment f = new ReblogFragment();
|
ReblogFragment f = new ReblogFragment();
|
||||||
|
|
||||||
@@ -68,8 +72,6 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
|||||||
@Override
|
@Override
|
||||||
public void injectFragment(ActivityComponent component) {
|
public void injectFragment(ActivityComponent component) {
|
||||||
component.inject(this);
|
component.inject(this);
|
||||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
|
||||||
.get(BlogViewModel.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,20 +95,30 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
|||||||
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
|
ui.input.setMaxTextLength(MAX_BLOG_POST_TEXT_LENGTH);
|
||||||
showProgressBar();
|
showProgressBar();
|
||||||
|
|
||||||
viewModel.loadBlogPost(blogId, postId).observe(getViewLifecycleOwner(),
|
feedController.loadBlogPost(blogId, postId,
|
||||||
result -> result.onError(this::handleException)
|
new UiResultExceptionHandler<BlogPostItem, DbException>(
|
||||||
.onSuccess(this::bindViewHolder)
|
this) {
|
||||||
);
|
@Override
|
||||||
|
public void onResultUi(BlogPostItem result) {
|
||||||
|
item = result;
|
||||||
|
bindViewHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bindViewHolder(BlogPostItem item) {
|
private void bindViewHolder() {
|
||||||
this.item = item;
|
if (item == null) return;
|
||||||
|
|
||||||
hideProgressBar();
|
hideProgressBar();
|
||||||
|
|
||||||
ui.post.bindItem(this.item);
|
ui.post.bindItem(item);
|
||||||
ui.post.hideReblogButton();
|
ui.post.hideReblogButton();
|
||||||
|
|
||||||
ui.input.setReady(true);
|
ui.input.setReady(true);
|
||||||
@@ -114,11 +126,18 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(@Nullable String text,
|
public LiveData<SendState> onSendClick(@Nullable String text,
|
||||||
List<AttachmentHeader> headers) {
|
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
|
||||||
ui.input.hideSoftKeyboard();
|
ui.input.hideSoftKeyboard();
|
||||||
viewModel.repeatPost(item, text);
|
feedController.repeatPost(item, text,
|
||||||
|
new UiExceptionHandler<DbException>(this) {
|
||||||
|
@Override
|
||||||
|
public void onExceptionUi(DbException exception) {
|
||||||
|
handleException(exception);
|
||||||
|
}
|
||||||
|
});
|
||||||
finish();
|
finish();
|
||||||
|
return new MutableLiveData<>(SENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showProgressBar() {
|
private void showProgressBar() {
|
||||||
@@ -131,7 +150,7 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
|||||||
ui.input.setVisibility(VISIBLE);
|
ui.input.setVisibility(VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ViewHolder implements OnBlogPostClickListener {
|
private class ViewHolder {
|
||||||
|
|
||||||
private final ScrollView scrollView;
|
private final ScrollView scrollView;
|
||||||
private final ProgressBar progressBar;
|
private final ProgressBar progressBar;
|
||||||
@@ -142,25 +161,18 @@ public class ReblogFragment extends BaseFragment implements SendListener {
|
|||||||
scrollView = v.findViewById(R.id.scrollView);
|
scrollView = v.findViewById(R.id.scrollView);
|
||||||
progressBar = v.findViewById(R.id.progressBar);
|
progressBar = v.findViewById(R.id.progressBar);
|
||||||
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout),
|
post = new BlogPostViewHolder(v.findViewById(R.id.postLayout),
|
||||||
true, this, false);
|
true, new OnBlogPostClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onBlogPostClick(BlogPostItem post) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthorClick(BlogPostItem post) {
|
||||||
|
// probably don't want to allow author clicks here
|
||||||
|
}
|
||||||
|
}, getFragmentManager());
|
||||||
input = v.findViewById(R.id.inputText);
|
input = v.findViewById(R.id.inputText);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBlogPostClick(BlogPostItem post) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthorClick(BlogPostItem post) {
|
|
||||||
// probably don't want to allow author clicks here
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLinkClick(String url) {
|
|
||||||
LinkDialogFragment f = LinkDialogFragment.newInstance(url);
|
|
||||||
f.show(getParentFragmentManager(), f.getUniqueTag());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,16 @@ import java.util.logging.Logger;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
import static android.view.View.VISIBLE;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||||
|
import static org.briarproject.briar.android.view.TextSendController.SendState;
|
||||||
|
import static org.briarproject.briar.android.view.TextSendController.SendState.SENT;
|
||||||
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@@ -112,8 +116,8 @@ public class WriteBlogPostActivity extends BriarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(@Nullable String text,
|
public LiveData<SendState> onSendClick(@Nullable String text,
|
||||||
List<AttachmentHeader> headers) {
|
List<AttachmentHeader> headers, long expectedAutoDeleteTimer) {
|
||||||
if (isNullOrEmpty(text)) throw new AssertionError();
|
if (isNullOrEmpty(text)) throw new AssertionError();
|
||||||
|
|
||||||
// hide publish button, show progress bar
|
// hide publish button, show progress bar
|
||||||
@@ -122,6 +126,7 @@ public class WriteBlogPostActivity extends BriarActivity
|
|||||||
progressBar.setVisibility(VISIBLE);
|
progressBar.setVisibility(VISIBLE);
|
||||||
|
|
||||||
storePost(text);
|
storePost(text);
|
||||||
|
return new MutableLiveData<>(SENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storePost(String text) {
|
private void storePost(String text) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.briarproject.briar.android.contact;
|
package org.briarproject.briar.android.contact;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import org.briarproject.briar.android.util.BriarAdapter;
|
import org.briarproject.briar.android.util.BriarAdapter;
|
||||||
|
|
||||||
@@ -44,4 +45,8 @@ public abstract class BaseContactListAdapter<I extends ContactItem, VH extends C
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface OnContactClickListener<I> {
|
||||||
|
void onItemClick(View view, I item);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.view.ViewGroup;
|
|||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.NullSafety;
|
import org.briarproject.bramble.api.nullsafety.NullSafety;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
|
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
|
||||||
import androidx.recyclerview.widget.ListAdapter;
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
@@ -15,6 +16,9 @@ import androidx.recyclerview.widget.ListAdapter;
|
|||||||
public class ContactListAdapter extends
|
public class ContactListAdapter extends
|
||||||
ListAdapter<ContactListItem, ContactListItemViewHolder> {
|
ListAdapter<ContactListItem, ContactListItemViewHolder> {
|
||||||
|
|
||||||
|
// TODO: using the click listener interface from BaseContactListAdapter on
|
||||||
|
// purpose here because it is entangled with ContactListItemViewHolder. At
|
||||||
|
// some point we probably want to change that.
|
||||||
protected final OnContactClickListener<ContactListItem> listener;
|
protected final OnContactClickListener<ContactListItem> listener;
|
||||||
|
|
||||||
public ContactListAdapter(
|
public ContactListAdapter(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
|||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||||
|
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
|
||||||
import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
|
import org.briarproject.briar.android.contact.add.remote.AddContactActivity;
|
||||||
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
|
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
|
||||||
import org.briarproject.briar.android.conversation.ConversationActivity;
|
import org.briarproject.briar.android.conversation.ConversationActivity;
|
||||||
@@ -100,6 +101,8 @@ public class ContactListFragment extends BaseFragment
|
|||||||
.observe(getViewLifecycleOwner(), result -> {
|
.observe(getViewLifecycleOwner(), result -> {
|
||||||
result.onError(this::handleException).onSuccess(items -> {
|
result.onError(this::handleException).onSuccess(items -> {
|
||||||
adapter.submitList(items);
|
adapter.submitList(items);
|
||||||
|
// TODO remove when BriarRecyclerView was adapted
|
||||||
|
list.showData();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
viewModel.getHasPendingContacts()
|
viewModel.getHasPendingContacts()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
|||||||
@@ -3,41 +3,70 @@ package org.briarproject.briar.android.contact;
|
|||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
||||||
|
import org.briarproject.bramble.api.contact.Contact;
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
import org.briarproject.bramble.api.contact.ContactManager;
|
import org.briarproject.bramble.api.contact.ContactManager;
|
||||||
|
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
|
||||||
|
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
||||||
import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent;
|
import org.briarproject.bramble.api.contact.event.PendingContactAddedEvent;
|
||||||
import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent;
|
import org.briarproject.bramble.api.contact.event.PendingContactRemovedEvent;
|
||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.db.Transaction;
|
||||||
import org.briarproject.bramble.api.db.TransactionManager;
|
import org.briarproject.bramble.api.db.TransactionManager;
|
||||||
import org.briarproject.bramble.api.event.Event;
|
import org.briarproject.bramble.api.event.Event;
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
|
||||||
|
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||||
|
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
||||||
|
import org.briarproject.briar.android.viewmodel.LiveResult;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
|
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
|
||||||
|
import org.briarproject.briar.api.client.MessageTracker;
|
||||||
import org.briarproject.briar.api.conversation.ConversationManager;
|
import org.briarproject.briar.api.conversation.ConversationManager;
|
||||||
|
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
|
||||||
|
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
|
||||||
|
import org.briarproject.briar.api.identity.AuthorInfo;
|
||||||
import org.briarproject.briar.api.identity.AuthorManager;
|
import org.briarproject.briar.api.identity.AuthorManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import androidx.arch.core.util.Function;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.now;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
class ContactListViewModel extends ContactsViewModel {
|
class ContactListViewModel extends DbViewModel implements EventListener {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(ContactListViewModel.class.getName());
|
getLogger(ContactListViewModel.class.getName());
|
||||||
|
|
||||||
|
private final ContactManager contactManager;
|
||||||
|
private final AuthorManager authorManager;
|
||||||
|
private final ConversationManager conversationManager;
|
||||||
|
private final ConnectionRegistry connectionRegistry;
|
||||||
|
private final EventBus eventBus;
|
||||||
private final AndroidNotificationManager notificationManager;
|
private final AndroidNotificationManager notificationManager;
|
||||||
|
|
||||||
|
private final MutableLiveData<LiveResult<List<ContactListItem>>>
|
||||||
|
contactListItems = new MutableLiveData<>();
|
||||||
|
|
||||||
private final MutableLiveData<Boolean> hasPendingContacts =
|
private final MutableLiveData<Boolean> hasPendingContacts =
|
||||||
new MutableLiveData<>();
|
new MutableLiveData<>();
|
||||||
|
|
||||||
@@ -50,25 +79,99 @@ class ContactListViewModel extends ContactsViewModel {
|
|||||||
ConversationManager conversationManager,
|
ConversationManager conversationManager,
|
||||||
ConnectionRegistry connectionRegistry, EventBus eventBus,
|
ConnectionRegistry connectionRegistry, EventBus eventBus,
|
||||||
AndroidNotificationManager notificationManager) {
|
AndroidNotificationManager notificationManager) {
|
||||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
|
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||||
contactManager, authorManager, conversationManager,
|
this.contactManager = contactManager;
|
||||||
connectionRegistry, eventBus);
|
this.authorManager = authorManager;
|
||||||
|
this.conversationManager = conversationManager;
|
||||||
|
this.connectionRegistry = connectionRegistry;
|
||||||
|
this.eventBus = eventBus;
|
||||||
this.notificationManager = notificationManager;
|
this.notificationManager = notificationManager;
|
||||||
|
this.eventBus.addListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCleared() {
|
||||||
|
super.onCleared();
|
||||||
|
eventBus.removeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadContacts() {
|
||||||
|
loadList(this::loadContacts, contactListItems::setValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ContactListItem> loadContacts(Transaction txn)
|
||||||
|
throws DbException {
|
||||||
|
long start = now();
|
||||||
|
List<ContactListItem> contacts = new ArrayList<>();
|
||||||
|
for (Contact c : contactManager.getContacts(txn)) {
|
||||||
|
ContactId id = c.getId();
|
||||||
|
AuthorInfo authorInfo = authorManager.getAuthorInfo(txn, c);
|
||||||
|
MessageTracker.GroupCount count =
|
||||||
|
conversationManager.getGroupCount(txn, id);
|
||||||
|
boolean connected = connectionRegistry.isConnected(c.getId());
|
||||||
|
contacts.add(new ContactListItem(c, authorInfo, connected, count));
|
||||||
|
}
|
||||||
|
Collections.sort(contacts);
|
||||||
|
logDuration(LOG, "Full load", start);
|
||||||
|
return contacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void eventOccurred(Event e) {
|
public void eventOccurred(Event e) {
|
||||||
super.eventOccurred(e);
|
if (e instanceof ContactAddedEvent) {
|
||||||
if (e instanceof PendingContactAddedEvent ||
|
LOG.info("Contact added, reloading");
|
||||||
|
loadContacts();
|
||||||
|
} else if (e instanceof ContactConnectedEvent) {
|
||||||
|
updateItem(((ContactConnectedEvent) e).getContactId(),
|
||||||
|
item -> new ContactListItem(item, true), false);
|
||||||
|
} else if (e instanceof ContactDisconnectedEvent) {
|
||||||
|
updateItem(((ContactDisconnectedEvent) e).getContactId(),
|
||||||
|
item -> new ContactListItem(item, false), false);
|
||||||
|
} else if (e instanceof ContactRemovedEvent) {
|
||||||
|
LOG.info("Contact removed, removing item");
|
||||||
|
removeItem(((ContactRemovedEvent) e).getContactId());
|
||||||
|
} else if (e instanceof ConversationMessageReceivedEvent) {
|
||||||
|
LOG.info("Conversation message received, updating item");
|
||||||
|
ConversationMessageReceivedEvent<?> p =
|
||||||
|
(ConversationMessageReceivedEvent<?>) e;
|
||||||
|
ConversationMessageHeader h = p.getMessageHeader();
|
||||||
|
updateItem(p.getContactId(), item -> new ContactListItem(item, h),
|
||||||
|
true);
|
||||||
|
} else if (e instanceof PendingContactAddedEvent ||
|
||||||
e instanceof PendingContactRemovedEvent) {
|
e instanceof PendingContactRemovedEvent) {
|
||||||
checkForPendingContacts();
|
checkForPendingContacts();
|
||||||
|
} else if (e instanceof AvatarUpdatedEvent) {
|
||||||
|
AvatarUpdatedEvent a = (AvatarUpdatedEvent) e;
|
||||||
|
updateItem(a.getContactId(), item -> new ContactListItem(item,
|
||||||
|
a.getAttachmentHeader()), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LiveData<LiveResult<List<ContactListItem>>> getContactListItems() {
|
||||||
|
return contactListItems;
|
||||||
|
}
|
||||||
|
|
||||||
LiveData<Boolean> getHasPendingContacts() {
|
LiveData<Boolean> getHasPendingContacts() {
|
||||||
return hasPendingContacts;
|
return hasPendingContacts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateItem(ContactId c,
|
||||||
|
Function<ContactListItem, ContactListItem> replacer, boolean sort) {
|
||||||
|
List<ContactListItem> list = updateListItems(contactListItems,
|
||||||
|
itemToTest -> itemToTest.getContact().getId().equals(c),
|
||||||
|
replacer);
|
||||||
|
if (list == null) return;
|
||||||
|
if (sort) Collections.sort(list);
|
||||||
|
contactListItems.setValue(new LiveResult<>(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeItem(ContactId c) {
|
||||||
|
List<ContactListItem> list = removeListItems(contactListItems,
|
||||||
|
itemToTest -> itemToTest.getContact().getId().equals(c));
|
||||||
|
if (list == null) return;
|
||||||
|
contactListItems.setValue(new LiveResult<>(list));
|
||||||
|
}
|
||||||
|
|
||||||
void checkForPendingContacts() {
|
void checkForPendingContacts() {
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
package org.briarproject.briar.android.contact;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
|
|
||||||
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
|
||||||
import org.briarproject.bramble.api.contact.Contact;
|
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
|
||||||
import org.briarproject.bramble.api.contact.ContactManager;
|
|
||||||
import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
|
|
||||||
import org.briarproject.bramble.api.contact.event.ContactRemovedEvent;
|
|
||||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
|
||||||
import org.briarproject.bramble.api.db.DbException;
|
|
||||||
import org.briarproject.bramble.api.db.Transaction;
|
|
||||||
import org.briarproject.bramble.api.db.TransactionManager;
|
|
||||||
import org.briarproject.bramble.api.event.Event;
|
|
||||||
import org.briarproject.bramble.api.event.EventBus;
|
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
|
||||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
|
|
||||||
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
|
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
|
||||||
import org.briarproject.briar.android.viewmodel.DbViewModel;
|
|
||||||
import org.briarproject.briar.android.viewmodel.LiveResult;
|
|
||||||
import org.briarproject.briar.api.avatar.event.AvatarUpdatedEvent;
|
|
||||||
import org.briarproject.briar.api.client.MessageTracker;
|
|
||||||
import org.briarproject.briar.api.conversation.ConversationManager;
|
|
||||||
import org.briarproject.briar.api.conversation.ConversationMessageHeader;
|
|
||||||
import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent;
|
|
||||||
import org.briarproject.briar.api.identity.AuthorInfo;
|
|
||||||
import org.briarproject.briar.api.identity.AuthorManager;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import androidx.annotation.UiThread;
|
|
||||||
import androidx.arch.core.util.Function;
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
|
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.now;
|
|
||||||
|
|
||||||
@NotNullByDefault
|
|
||||||
public class ContactsViewModel extends DbViewModel implements EventListener {
|
|
||||||
|
|
||||||
private static final Logger LOG =
|
|
||||||
getLogger(ContactsViewModel.class.getName());
|
|
||||||
|
|
||||||
protected final ContactManager contactManager;
|
|
||||||
private final AuthorManager authorManager;
|
|
||||||
private final ConversationManager conversationManager;
|
|
||||||
private final ConnectionRegistry connectionRegistry;
|
|
||||||
private final EventBus eventBus;
|
|
||||||
|
|
||||||
private final MutableLiveData<LiveResult<List<ContactListItem>>>
|
|
||||||
contactListItems = new MutableLiveData<>();
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ContactsViewModel(Application application,
|
|
||||||
@DatabaseExecutor Executor dbExecutor,
|
|
||||||
LifecycleManager lifecycleManager, TransactionManager db,
|
|
||||||
AndroidExecutor androidExecutor, ContactManager contactManager,
|
|
||||||
AuthorManager authorManager,
|
|
||||||
ConversationManager conversationManager,
|
|
||||||
ConnectionRegistry connectionRegistry, EventBus eventBus) {
|
|
||||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
|
||||||
this.contactManager = contactManager;
|
|
||||||
this.authorManager = authorManager;
|
|
||||||
this.conversationManager = conversationManager;
|
|
||||||
this.connectionRegistry = connectionRegistry;
|
|
||||||
this.eventBus = eventBus;
|
|
||||||
this.eventBus.addListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCleared() {
|
|
||||||
super.onCleared();
|
|
||||||
eventBus.removeListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void loadContacts() {
|
|
||||||
loadFromDb(this::loadContacts, contactListItems::setValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ContactListItem> loadContacts(Transaction txn)
|
|
||||||
throws DbException {
|
|
||||||
long start = now();
|
|
||||||
List<ContactListItem> contacts = new ArrayList<>();
|
|
||||||
for (Contact c : contactManager.getContacts(txn)) {
|
|
||||||
ContactId id = c.getId();
|
|
||||||
if (!displayContact(id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
AuthorInfo authorInfo = authorManager.getAuthorInfo(txn, c);
|
|
||||||
MessageTracker.GroupCount count =
|
|
||||||
conversationManager.getGroupCount(txn, id);
|
|
||||||
boolean connected = connectionRegistry.isConnected(c.getId());
|
|
||||||
contacts.add(new ContactListItem(c, authorInfo, connected, count));
|
|
||||||
}
|
|
||||||
Collections.sort(contacts);
|
|
||||||
logDuration(LOG, "Full load", start);
|
|
||||||
return contacts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override this method to display only a subset of contacts.
|
|
||||||
*/
|
|
||||||
protected boolean displayContact(ContactId contactId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void eventOccurred(Event e) {
|
|
||||||
if (e instanceof ContactAddedEvent) {
|
|
||||||
LOG.info("Contact added, reloading");
|
|
||||||
loadContacts();
|
|
||||||
} else if (e instanceof ContactConnectedEvent) {
|
|
||||||
updateItem(((ContactConnectedEvent) e).getContactId(),
|
|
||||||
item -> new ContactListItem(item, true), false);
|
|
||||||
} else if (e instanceof ContactDisconnectedEvent) {
|
|
||||||
updateItem(((ContactDisconnectedEvent) e).getContactId(),
|
|
||||||
item -> new ContactListItem(item, false), false);
|
|
||||||
} else if (e instanceof ContactRemovedEvent) {
|
|
||||||
LOG.info("Contact removed, removing item");
|
|
||||||
removeItem(((ContactRemovedEvent) e).getContactId());
|
|
||||||
} else if (e instanceof ConversationMessageReceivedEvent) {
|
|
||||||
LOG.info("Conversation message received, updating item");
|
|
||||||
ConversationMessageReceivedEvent<?> p =
|
|
||||||
(ConversationMessageReceivedEvent<?>) e;
|
|
||||||
ConversationMessageHeader h = p.getMessageHeader();
|
|
||||||
updateItem(p.getContactId(), item -> new ContactListItem(item, h),
|
|
||||||
true);
|
|
||||||
} else if (e instanceof AvatarUpdatedEvent) {
|
|
||||||
AvatarUpdatedEvent a = (AvatarUpdatedEvent) e;
|
|
||||||
updateItem(a.getContactId(), item -> new ContactListItem(item,
|
|
||||||
a.getAttachmentHeader()), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<LiveResult<List<ContactListItem>>> getContactListItems() {
|
|
||||||
return contactListItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
private void updateItem(ContactId c,
|
|
||||||
Function<ContactListItem, ContactListItem> replacer, boolean sort) {
|
|
||||||
List<ContactListItem> list = updateListItems(getList(contactListItems),
|
|
||||||
itemToTest -> itemToTest.getContact().getId().equals(c),
|
|
||||||
replacer);
|
|
||||||
if (list == null) return;
|
|
||||||
if (sort) Collections.sort(list);
|
|
||||||
contactListItems.setValue(new LiveResult<>(list));
|
|
||||||
}
|
|
||||||
|
|
||||||
@UiThread
|
|
||||||
private void removeItem(ContactId c) {
|
|
||||||
removeAndUpdateListItems(contactListItems,
|
|
||||||
itemToTest -> itemToTest.getContact().getId().equals(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.briarproject.briar.android.contact;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.briar.R;
|
||||||
|
|
||||||
|
@NotNullByDefault
|
||||||
|
public class LegacyContactListAdapter extends
|
||||||
|
BaseContactListAdapter<ContactListItem, ContactListItemViewHolder> {
|
||||||
|
|
||||||
|
public LegacyContactListAdapter(Context context,
|
||||||
|
OnContactClickListener<ContactListItem> listener) {
|
||||||
|
super(context, ContactListItem.class, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ContactListItemViewHolder onCreateViewHolder(ViewGroup viewGroup,
|
||||||
|
int i) {
|
||||||
|
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
|
||||||
|
R.layout.list_item_contact, viewGroup, false);
|
||||||
|
|
||||||
|
return new ContactListItemViewHolder(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(ContactListItem c1, ContactListItem c2) {
|
||||||
|
// check for all properties that influence visual
|
||||||
|
// representation of contact
|
||||||
|
if (c1.isEmpty() != c2.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (c1.getUnreadCount() != c2.getUnreadCount()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (c1.getTimestamp() != c2.getTimestamp()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return c1.isConnected() == c2.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compare(ContactListItem c1, ContactListItem c2) {
|
||||||
|
return Long.compare(c2.getTimestamp(), c1.getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package org.briarproject.briar.android.contact;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
public interface OnContactClickListener<I> {
|
|
||||||
|
|
||||||
void onItemClick(View view, I item);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.contact.ContactId;
|
|||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.briar.android.contact.BaseContactListAdapter;
|
import org.briarproject.briar.android.contact.BaseContactListAdapter;
|
||||||
import org.briarproject.briar.android.contact.ContactItemViewHolder;
|
import org.briarproject.briar.android.contact.ContactItemViewHolder;
|
||||||
import org.briarproject.briar.android.contact.OnContactClickListener;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
|||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.sync.GroupId;
|
import org.briarproject.bramble.api.sync.GroupId;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
|
||||||
import org.briarproject.briar.android.contact.ContactItemViewHolder;
|
import org.briarproject.briar.android.contact.ContactItemViewHolder;
|
||||||
import org.briarproject.briar.android.contact.OnContactClickListener;
|
|
||||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
|
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
|
||||||
import org.briarproject.briar.android.contact.ContactItemViewHolder;
|
import org.briarproject.briar.android.contact.ContactItemViewHolder;
|
||||||
import org.briarproject.briar.android.contact.OnContactClickListener;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import android.view.ViewGroup;
|
|||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.contact.OnContactClickListener;
|
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
class ContactSelectorAdapter extends
|
class ContactSelectorAdapter extends
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import android.view.MenuItem;
|
|||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.contact.OnContactClickListener;
|
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package org.briarproject.briar.android.contactselection;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.briar.android.contact.OnContactClickListener;
|
import org.briarproject.briar.android.contact.BaseContactListAdapter.OnContactClickListener;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package org.briarproject.briar.android.controller;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
@NotNullByDefault
|
||||||
|
public interface SharingController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the listener that is called when contacts go on or offline.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
void setSharingListener(SharingListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsets the listener.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
void unsetSharingListener(SharingListener listener);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this when your lifecycle starts,
|
||||||
|
* so the listener will be called when information changes.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
void onStart();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this when your lifecycle stops,
|
||||||
|
* so that the controller knows it can stops listening to events.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
void onStop();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds one contact to be tracked.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
void add(ContactId c);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a collection of contacts to be tracked.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
void addAll(Collection<ContactId> contacts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this when the contact identified by c is no longer sharing
|
||||||
|
* the given group identified by GroupId g.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
void remove(ContactId c);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of online contacts.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
int getOnlineCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total number of contacts that have been added.
|
||||||
|
*/
|
||||||
|
@UiThread
|
||||||
|
int getTotalCount();
|
||||||
|
|
||||||
|
interface SharingListener {
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void onSharingInfoUpdated(int total, int online);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package org.briarproject.briar.android.controller;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
||||||
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
|
||||||
|
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
@NotNullByDefault
|
||||||
|
public class SharingControllerImpl implements SharingController, EventListener {
|
||||||
|
|
||||||
|
private final EventBus eventBus;
|
||||||
|
private final ConnectionRegistry connectionRegistry;
|
||||||
|
|
||||||
|
// UI thread
|
||||||
|
private final Set<ContactId> contacts = new HashSet<>();
|
||||||
|
|
||||||
|
// UI thread
|
||||||
|
@Nullable
|
||||||
|
private SharingListener listener;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SharingControllerImpl(EventBus eventBus,
|
||||||
|
ConnectionRegistry connectionRegistry) {
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.connectionRegistry = connectionRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSharingListener(SharingListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unsetSharingListener(SharingListener listener) {
|
||||||
|
if (this.listener == listener) this.listener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
eventBus.addListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
eventBus.removeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void eventOccurred(Event e) {
|
||||||
|
if (e instanceof ContactConnectedEvent) {
|
||||||
|
setConnected(((ContactConnectedEvent) e).getContactId());
|
||||||
|
} else if (e instanceof ContactDisconnectedEvent) {
|
||||||
|
setConnected(((ContactDisconnectedEvent) e).getContactId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
private void setConnected(ContactId c) {
|
||||||
|
if (listener == null) throw new IllegalStateException();
|
||||||
|
if (contacts.contains(c)) {
|
||||||
|
int online = getOnlineCount();
|
||||||
|
listener.onSharingInfoUpdated(contacts.size(), online);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAll(Collection<ContactId> c) {
|
||||||
|
contacts.addAll(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(ContactId c) {
|
||||||
|
contacts.add(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove(ContactId c) {
|
||||||
|
contacts.remove(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOnlineCount() {
|
||||||
|
int online = 0;
|
||||||
|
for (ContactId c : contacts) {
|
||||||
|
if (connectionRegistry.isConnected(c)) online++;
|
||||||
|
}
|
||||||
|
return online;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTotalCount() {
|
||||||
|
return contacts.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
|
|||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
|
// TODO: we can probably switch to androidx DialogFragment here but need to
|
||||||
|
// test this properly
|
||||||
public class AliasDialogFragment extends AppCompatDialogFragment {
|
public class AliasDialogFragment extends AppCompatDialogFragment {
|
||||||
|
|
||||||
final static String TAG = AliasDialogFragment.class.getName();
|
final static String TAG = AliasDialogFragment.class.getName();
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import org.briarproject.briar.android.blog.BlogActivity;
|
|||||||
import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache;
|
import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache;
|
||||||
import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache;
|
import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache;
|
||||||
import org.briarproject.briar.android.forum.ForumActivity;
|
import org.briarproject.briar.android.forum.ForumActivity;
|
||||||
|
import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener;
|
||||||
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||||
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
||||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||||
@@ -59,8 +60,10 @@ import org.briarproject.briar.android.view.TextAttachmentController;
|
|||||||
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
|
import org.briarproject.briar.android.view.TextAttachmentController.AttachmentListener;
|
||||||
import org.briarproject.briar.android.view.TextInputView;
|
import org.briarproject.briar.android.view.TextInputView;
|
||||||
import org.briarproject.briar.android.view.TextSendController;
|
import org.briarproject.briar.android.view.TextSendController;
|
||||||
|
import org.briarproject.briar.android.view.TextSendController.SendState;
|
||||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||||
import org.briarproject.briar.api.attachment.AttachmentHeader;
|
import org.briarproject.briar.api.attachment.AttachmentHeader;
|
||||||
|
import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent;
|
||||||
import org.briarproject.briar.api.blog.BlogSharingManager;
|
import org.briarproject.briar.api.blog.BlogSharingManager;
|
||||||
import org.briarproject.briar.api.client.ProtocolStateException;
|
import org.briarproject.briar.api.client.ProtocolStateException;
|
||||||
import org.briarproject.briar.api.client.SessionId;
|
import org.briarproject.briar.api.client.SessionId;
|
||||||
@@ -138,12 +141,14 @@ import static org.briarproject.briar.android.util.UiUtils.observeOnce;
|
|||||||
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
|
import static org.briarproject.briar.android.view.AuthorView.setAvatar;
|
||||||
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
|
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_ATTACHMENTS_PER_MESSAGE;
|
||||||
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
|
import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_TEXT_LENGTH;
|
||||||
|
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_IMAGES_AUTO_DELETE;
|
||||||
|
import static org.briarproject.briar.api.messaging.PrivateMessageFormat.TEXT_ONLY;
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
public class ConversationActivity extends BriarActivity
|
public class ConversationActivity extends BriarActivity
|
||||||
implements EventListener, ConversationListener, TextCache,
|
implements BaseFragmentListener, EventListener, ConversationListener,
|
||||||
AttachmentCache, AttachmentListener, ActionMode.Callback {
|
TextCache, AttachmentCache, AttachmentListener, ActionMode.Callback {
|
||||||
|
|
||||||
public static final String CONTACT_ID = "briar.CONTACT_ID";
|
public static final String CONTACT_ID = "briar.CONTACT_ID";
|
||||||
|
|
||||||
@@ -268,15 +273,11 @@ public class ConversationActivity extends BriarActivity
|
|||||||
ImagePreview imagePreview = findViewById(R.id.imagePreview);
|
ImagePreview imagePreview = findViewById(R.id.imagePreview);
|
||||||
sendController = new TextAttachmentController(textInputView,
|
sendController = new TextAttachmentController(textInputView,
|
||||||
imagePreview, this, viewModel);
|
imagePreview, this, viewModel);
|
||||||
viewModel.hasImageSupport().observe(this, new Observer<Boolean>() {
|
observeOnce(viewModel.getPrivateMessageFormat(), this, format -> {
|
||||||
@Override
|
if (format != TEXT_ONLY) {
|
||||||
public void onChanged(@Nullable Boolean hasSupport) {
|
// TODO: remove cast when removing feature flag
|
||||||
if (hasSupport != null && hasSupport) {
|
((TextAttachmentController) sendController)
|
||||||
// TODO: remove cast when removing feature flag
|
.setImagesSupported();
|
||||||
((TextAttachmentController) sendController)
|
|
||||||
.setImagesSupported();
|
|
||||||
viewModel.hasImageSupport().removeObserver(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -286,6 +287,9 @@ public class ConversationActivity extends BriarActivity
|
|||||||
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
|
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
|
||||||
textInputView.setReady(false);
|
textInputView.setReady(false);
|
||||||
textInputView.setOnKeyboardShownListener(this::scrollToBottom);
|
textInputView.setOnKeyboardShownListener(this::scrollToBottom);
|
||||||
|
|
||||||
|
viewModel.getAutoDeleteTimer().observe(this, timer ->
|
||||||
|
sendController.setAutoDeleteTimer(timer));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scrollToBottom() {
|
private void scrollToBottom() {
|
||||||
@@ -369,6 +373,14 @@ public class ConversationActivity extends BriarActivity
|
|||||||
// enable alias action if available
|
// enable alias action if available
|
||||||
observeOnce(viewModel.getContactItem(), this, contact ->
|
observeOnce(viewModel.getContactItem(), this, contact ->
|
||||||
menu.findItem(R.id.action_set_alias).setEnabled(true));
|
menu.findItem(R.id.action_set_alias).setEnabled(true));
|
||||||
|
// Show auto-delete menu item if feature is enabled
|
||||||
|
if (featureFlags.shouldEnableDisappearingMessages()) {
|
||||||
|
MenuItem item = menu.findItem(R.id.action_conversation_settings);
|
||||||
|
item.setVisible(true);
|
||||||
|
// Enable menu item only if contact supports auto-delete
|
||||||
|
viewModel.getPrivateMessageFormat().observe(this, format ->
|
||||||
|
item.setEnabled(format == TEXT_IMAGES_AUTO_DELETE));
|
||||||
|
}
|
||||||
|
|
||||||
return super.onCreateOptionsMenu(menu);
|
return super.onCreateOptionsMenu(menu);
|
||||||
}
|
}
|
||||||
@@ -390,6 +402,10 @@ public class ConversationActivity extends BriarActivity
|
|||||||
AliasDialogFragment.newInstance().show(
|
AliasDialogFragment.newInstance().show(
|
||||||
getSupportFragmentManager(), AliasDialogFragment.TAG);
|
getSupportFragmentManager(), AliasDialogFragment.TAG);
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.action_conversation_settings:
|
||||||
|
if (contactId == null) return false;
|
||||||
|
onAutoDeleteTimerNoticeClicked();
|
||||||
|
return true;
|
||||||
case R.id.action_delete_all_messages:
|
case R.id.action_delete_all_messages:
|
||||||
askToDeleteAllMessages();
|
askToDeleteAllMessages();
|
||||||
return true;
|
return true;
|
||||||
@@ -559,7 +575,7 @@ public class ConversationActivity extends BriarActivity
|
|||||||
this::showImageOnboarding);
|
this::showImageOnboarding);
|
||||||
}
|
}
|
||||||
List<ConversationItem> items = createItems(headers);
|
List<ConversationItem> items = createItems(headers);
|
||||||
adapter.addAll(items);
|
adapter.replaceAll(items);
|
||||||
list.showData();
|
list.showData();
|
||||||
if (layoutManagerState == null) {
|
if (layoutManagerState == null) {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
@@ -640,8 +656,8 @@ public class ConversationActivity extends BriarActivity
|
|||||||
supportFinishAfterTransition();
|
supportFinishAfterTransition();
|
||||||
}
|
}
|
||||||
} else if (e instanceof ConversationMessageReceivedEvent) {
|
} else if (e instanceof ConversationMessageReceivedEvent) {
|
||||||
ConversationMessageReceivedEvent p =
|
ConversationMessageReceivedEvent<?> p =
|
||||||
(ConversationMessageReceivedEvent) e;
|
(ConversationMessageReceivedEvent<?>) e;
|
||||||
if (p.getContactId().equals(contactId)) {
|
if (p.getContactId().equals(contactId)) {
|
||||||
LOG.info("Message received, adding");
|
LOG.info("Message received, adding");
|
||||||
onNewConversationMessage(p.getMessageHeader());
|
onNewConversationMessage(p.getMessageHeader());
|
||||||
@@ -658,6 +674,13 @@ public class ConversationActivity extends BriarActivity
|
|||||||
LOG.info("Messages acked");
|
LOG.info("Messages acked");
|
||||||
markMessages(m.getMessageIds(), true, true);
|
markMessages(m.getMessageIds(), true, true);
|
||||||
}
|
}
|
||||||
|
} else if (e instanceof ConversationMessagesDeletedEvent) {
|
||||||
|
ConversationMessagesDeletedEvent m =
|
||||||
|
(ConversationMessagesDeletedEvent) e;
|
||||||
|
if (m.getContactId().equals(contactId)) {
|
||||||
|
LOG.info("Messages auto-deleted");
|
||||||
|
onConversationMessagesDeleted(m.getMessageIds());
|
||||||
|
}
|
||||||
} else if (e instanceof ContactConnectedEvent) {
|
} else if (e instanceof ContactConnectedEvent) {
|
||||||
ContactConnectedEvent c = (ContactConnectedEvent) e;
|
ContactConnectedEvent c = (ContactConnectedEvent) e;
|
||||||
if (c.getContactId().equals(contactId)) {
|
if (c.getContactId().equals(contactId)) {
|
||||||
@@ -705,6 +728,13 @@ public class ConversationActivity extends BriarActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
private void onConversationMessagesDeleted(
|
||||||
|
Collection<MessageId> messageIds) {
|
||||||
|
adapter.incrementRevision();
|
||||||
|
adapter.removeItems(messageIds);
|
||||||
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
private void markMessages(Collection<MessageId> messageIds, boolean sent,
|
private void markMessages(Collection<MessageId> messageIds, boolean sent,
|
||||||
boolean seen) {
|
boolean seen) {
|
||||||
@@ -735,20 +765,13 @@ public class ConversationActivity extends BriarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSendClick(@Nullable String text,
|
public LiveData<SendState> onSendClick(@Nullable String text,
|
||||||
List<AttachmentHeader> attachmentHeaders) {
|
List<AttachmentHeader> attachmentHeaders,
|
||||||
|
long expectedAutoDeleteTimer) {
|
||||||
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
|
if (isNullOrEmpty(text) && attachmentHeaders.isEmpty())
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
long timestamp = System.currentTimeMillis();
|
return viewModel
|
||||||
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
|
.sendMessage(text, attachmentHeaders, expectedAutoDeleteTimer);
|
||||||
viewModel.sendMessage(text, attachmentHeaders, timestamp);
|
|
||||||
textInputView.clearText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private long getMinTimestampForNewMessage() {
|
|
||||||
// Don't use an earlier timestamp than the newest message
|
|
||||||
ConversationItem item = adapter.getLastItem();
|
|
||||||
return item == null ? 0 : item.getTime() + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
|
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
|
||||||
@@ -823,10 +846,6 @@ public class ConversationActivity extends BriarActivity
|
|||||||
fails.add(getString(
|
fails.add(getString(
|
||||||
R.string.dialog_message_not_deleted_ongoing_invitations));
|
R.string.dialog_message_not_deleted_ongoing_invitations));
|
||||||
}
|
}
|
||||||
if (result.hasNotFullyDownloaded()) {
|
|
||||||
fails.add(getString(
|
|
||||||
R.string.dialog_message_not_deleted_partly_downloaded));
|
|
||||||
}
|
|
||||||
// add problems the user can resolve
|
// add problems the user can resolve
|
||||||
if (result.hasNotAllIntroductionSelected() &&
|
if (result.hasNotAllIntroductionSelected() &&
|
||||||
result.hasNotAllInvitationSelected()) {
|
result.hasNotAllInvitationSelected()) {
|
||||||
@@ -958,13 +977,11 @@ public class ConversationActivity extends BriarActivity
|
|||||||
adapter.notifyItemChanged(position, item);
|
adapter.notifyItemChanged(position, item);
|
||||||
}
|
}
|
||||||
runOnDbThread(() -> {
|
runOnDbThread(() -> {
|
||||||
long timestamp = System.currentTimeMillis();
|
|
||||||
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
|
|
||||||
try {
|
try {
|
||||||
switch (item.getRequestType()) {
|
switch (item.getRequestType()) {
|
||||||
case INTRODUCTION:
|
case INTRODUCTION:
|
||||||
respondToIntroductionRequest(item.getSessionId(),
|
respondToIntroductionRequest(item.getSessionId(),
|
||||||
accept, timestamp);
|
accept);
|
||||||
break;
|
break;
|
||||||
case FORUM:
|
case FORUM:
|
||||||
respondToForumRequest(item.getSessionId(), accept);
|
respondToForumRequest(item.getSessionId(), accept);
|
||||||
@@ -1038,11 +1055,18 @@ public class ConversationActivity extends BriarActivity
|
|||||||
ActivityCompat.startActivity(this, i, options.toBundle());
|
ActivityCompat.startActivity(this, i, options.toBundle());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAutoDeleteTimerNoticeClicked() {
|
||||||
|
ConversationSettingsDialog dialog =
|
||||||
|
ConversationSettingsDialog.newInstance(contactId);
|
||||||
|
dialog.show(getSupportFragmentManager(),
|
||||||
|
ConversationSettingsDialog.TAG);
|
||||||
|
}
|
||||||
|
|
||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
private void respondToIntroductionRequest(SessionId sessionId,
|
private void respondToIntroductionRequest(SessionId sessionId,
|
||||||
boolean accept, long time) throws DbException {
|
boolean accept) throws DbException {
|
||||||
introductionManager.respondToIntroduction(contactId, sessionId, time,
|
introductionManager.respondToIntroduction(contactId, sessionId, accept);
|
||||||
accept);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
|
|||||||
@@ -13,20 +13,26 @@ import org.briarproject.briar.R;
|
|||||||
import org.briarproject.briar.android.util.BriarAdapter;
|
import org.briarproject.briar.android.util.BriarAdapter;
|
||||||
import org.briarproject.briar.android.util.ItemReturningAdapter;
|
import org.briarproject.briar.android.util.ItemReturningAdapter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.annotation.LayoutRes;
|
import androidx.annotation.LayoutRes;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.UiThread;
|
||||||
import androidx.recyclerview.selection.SelectionTracker;
|
import androidx.recyclerview.selection.SelectionTracker;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
|
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool;
|
||||||
|
|
||||||
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
||||||
|
import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
|
||||||
|
|
||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
class ConversationAdapter
|
class ConversationAdapter
|
||||||
extends BriarAdapter<ConversationItem, ConversationItemViewHolder>
|
extends BriarAdapter<ConversationItem, ConversationItemViewHolder>
|
||||||
implements ItemReturningAdapter<ConversationItem> {
|
implements ItemReturningAdapter<ConversationItem> {
|
||||||
|
|
||||||
private ConversationListener listener;
|
private final ConversationListener listener;
|
||||||
private final RecycledViewPool imageViewPool;
|
private final RecycledViewPool imageViewPool;
|
||||||
private final ImageItemDecoration imageItemDecoration;
|
private final ImageItemDecoration imageItemDecoration;
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -65,22 +71,20 @@ class ConversationAdapter
|
|||||||
@LayoutRes int type) {
|
@LayoutRes int type) {
|
||||||
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
|
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
|
||||||
type, viewGroup, false);
|
type, viewGroup, false);
|
||||||
switch (type) {
|
if (type == R.layout.list_item_conversation_msg_in) {
|
||||||
case R.layout.list_item_conversation_msg_in:
|
return new ConversationMessageViewHolder(v, listener, true,
|
||||||
return new ConversationMessageViewHolder(v, listener, true,
|
imageViewPool, imageItemDecoration);
|
||||||
imageViewPool, imageItemDecoration);
|
} else if (type == R.layout.list_item_conversation_msg_out) {
|
||||||
case R.layout.list_item_conversation_msg_out:
|
return new ConversationMessageViewHolder(v, listener, false,
|
||||||
return new ConversationMessageViewHolder(v, listener, false,
|
imageViewPool, imageItemDecoration);
|
||||||
imageViewPool, imageItemDecoration);
|
} else if (type == R.layout.list_item_conversation_notice_in) {
|
||||||
case R.layout.list_item_conversation_notice_in:
|
return new ConversationNoticeViewHolder(v, listener, true);
|
||||||
return new ConversationNoticeViewHolder(v, listener, true);
|
} else if (type == R.layout.list_item_conversation_notice_out) {
|
||||||
case R.layout.list_item_conversation_notice_out:
|
return new ConversationNoticeViewHolder(v, listener, false);
|
||||||
return new ConversationNoticeViewHolder(v, listener, false);
|
} else if (type == R.layout.list_item_conversation_request) {
|
||||||
case R.layout.list_item_conversation_request:
|
return new ConversationRequestViewHolder(v, listener, true);
|
||||||
return new ConversationRequestViewHolder(v, listener, true);
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unknown ConversationItem");
|
|
||||||
}
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown ConversationItem");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -107,22 +111,65 @@ class ConversationAdapter
|
|||||||
return c1.equals(c2);
|
return c1.equals(c2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(ConversationItem item) {
|
||||||
|
items.beginBatchedUpdates();
|
||||||
|
items.add(item);
|
||||||
|
updateTimersInBatch();
|
||||||
|
items.endBatchedUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void replaceAll(Collection<ConversationItem> itemsToReplace) {
|
||||||
|
items.beginBatchedUpdates();
|
||||||
|
// there can be items already in the adapter
|
||||||
|
// SortedList takes care of duplicates and detecting changed items
|
||||||
|
items.replaceAll(itemsToReplace);
|
||||||
|
updateTimersInBatch();
|
||||||
|
items.endBatchedUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
void removeItems(Collection<MessageId> messageIds) {
|
||||||
|
// Collect all items to be deleted first
|
||||||
|
// and then delete them in one batched update.
|
||||||
|
// Deleting them right away would cause issues
|
||||||
|
// due to changing list positions.
|
||||||
|
List<ConversationItem> toRemove = new ArrayList<>(messageIds.size());
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
ConversationItem item = items.get(i);
|
||||||
|
if (messageIds.contains(item.getId())) toRemove.add(item);
|
||||||
|
}
|
||||||
|
items.beginBatchedUpdates();
|
||||||
|
for (ConversationItem item : toRemove) items.remove(item);
|
||||||
|
items.endBatchedUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTimersInBatch() {
|
||||||
|
long lastTimerIncoming = NO_AUTO_DELETE_TIMER;
|
||||||
|
long lastTimerOutgoing = NO_AUTO_DELETE_TIMER;
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
ConversationItem c = items.get(i);
|
||||||
|
boolean itemChanged;
|
||||||
|
boolean timerChanged;
|
||||||
|
if (c.isIncoming()) {
|
||||||
|
timerChanged = lastTimerIncoming != c.getAutoDeleteTimer();
|
||||||
|
lastTimerIncoming = c.getAutoDeleteTimer();
|
||||||
|
} else {
|
||||||
|
timerChanged = lastTimerOutgoing != c.getAutoDeleteTimer();
|
||||||
|
lastTimerOutgoing = c.getAutoDeleteTimer();
|
||||||
|
}
|
||||||
|
itemChanged = c.setTimerNoticeVisible(timerChanged);
|
||||||
|
if (itemChanged) items.updateItemAt(i, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void setSelectionTracker(SelectionTracker<String> tracker) {
|
void setSelectionTracker(SelectionTracker<String> tracker) {
|
||||||
this.tracker = tracker;
|
this.tracker = tracker;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
ConversationItem getLastItem() {
|
|
||||||
if (items.size() > 0) {
|
|
||||||
return items.get(items.size() - 1);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SparseArray<ConversationItem> getOutgoingMessages() {
|
SparseArray<ConversationItem> getOutgoingMessages() {
|
||||||
SparseArray<ConversationItem> messages = new SparseArray<>();
|
SparseArray<ConversationItem> messages = new SparseArray<>();
|
||||||
|
|
||||||
for (int i = 0; i < items.size(); i++) {
|
for (int i = 0; i < items.size(); i++) {
|
||||||
ConversationItem item = items.get(i);
|
ConversationItem item = items.get(i);
|
||||||
if (!item.isIncoming()) {
|
if (!item.isIncoming()) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user