mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 02:39:05 +01:00
Compare commits
57 Commits
beta-1.2.1
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a534b4503 | ||
|
|
e5b2275c82 | ||
|
|
5159593825 | ||
|
|
6fbc82ee27 | ||
|
|
885b03cfd7 | ||
|
|
f81bfcafeb | ||
|
|
f36f1cf3d4 | ||
|
|
7d6a63d866 | ||
|
|
15ebdf8dd5 | ||
|
|
db2c235283 | ||
|
|
e5bd43469e | ||
|
|
9366c184d8 | ||
|
|
73d2c964d4 | ||
|
|
fb2b4209cf | ||
|
|
a04b512497 | ||
|
|
3d9515e308 | ||
|
|
1b19b331b1 | ||
|
|
d151a2d7f7 | ||
|
|
9712a4b849 | ||
|
|
cf1ac5e3e5 | ||
|
|
cb859e998d | ||
|
|
0b9345f867 | ||
|
|
12988120d1 | ||
|
|
8d6c866e62 | ||
|
|
8f82cf3c73 | ||
|
|
21112ce092 | ||
|
|
21ee3ea00d | ||
|
|
bb964101b3 | ||
|
|
d796eff0f6 | ||
|
|
700ea2b387 | ||
|
|
6e3a7d8d0c | ||
|
|
166b5d4add | ||
|
|
0fd59a26f6 | ||
|
|
4162bf990a | ||
|
|
09cfadbf7e | ||
|
|
ae4a04bada | ||
|
|
d670179e30 | ||
|
|
998c435b13 | ||
|
|
4a0327a62b | ||
|
|
70532732c8 | ||
|
|
d69406dfe3 | ||
|
|
98619df867 | ||
|
|
f2eca0fdb6 | ||
|
|
c62a57e8b2 | ||
|
|
239c4a27ad | ||
|
|
e5d78a858d | ||
|
|
635008fb60 | ||
|
|
b78569119a | ||
|
|
8372bb01b2 | ||
|
|
766718e75c | ||
|
|
1c107a851b | ||
|
|
db53e79d1d | ||
|
|
21e56284fb | ||
|
|
d393b79ced | ||
|
|
6611d7c02e | ||
|
|
ab43dd4986 | ||
|
|
36a9174781 |
@@ -2,6 +2,7 @@ image: briar/ci-image-android:latest
|
||||
|
||||
stages:
|
||||
- test
|
||||
- optional_tests
|
||||
- check_reproducibility
|
||||
|
||||
test:
|
||||
@@ -31,3 +32,33 @@ test_reproducible:
|
||||
- "curl -X POST -F token=${RELEASE_CHECK_TOKEN} -F ref=master -F variables[RELEASE_TAG]=${CI_COMMIT_REF_NAME} https://code.briarproject.org/api/v4/projects/61/trigger/pipeline"
|
||||
only:
|
||||
- tags
|
||||
|
||||
.optional_tests:
|
||||
stage: optional_tests
|
||||
before_script:
|
||||
- set -e
|
||||
- export GRADLE_USER_HOME=$PWD/.gradle
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .gradle/wrapper
|
||||
- .gradle/caches
|
||||
|
||||
script:
|
||||
- OPTIONAL_TESTS=org.briarproject.bramble.plugin.tor.BridgeTest ./gradlew --info bramble-java:test --tests BridgeTest
|
||||
|
||||
after_script:
|
||||
# these file change every time but should not be cached
|
||||
- rm -f $GRADLE_USER_HOME/caches/modules-2/modules-2.lock
|
||||
- rm -fr $GRADLE_USER_HOME/caches/*/plugin-resolution/
|
||||
|
||||
manual_tests:
|
||||
extends: .optional_tests
|
||||
when: manual
|
||||
except:
|
||||
- tags
|
||||
|
||||
pre_release_tests:
|
||||
extends: .optional_tests
|
||||
only:
|
||||
- tags
|
||||
|
||||
14
.idea/dictionaries/briar.xml
generated
Normal file
14
.idea/dictionaries/briar.xml
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="briar">
|
||||
<words>
|
||||
<w>briar</w>
|
||||
<w>briarproject</w>
|
||||
<w>emoji</w>
|
||||
<w>encrypter</w>
|
||||
<w>identicon</w>
|
||||
<w>introducee</w>
|
||||
<w>introducer</w>
|
||||
<w>onboarding</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
</component>
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 10214
|
||||
versionName "1.2.14"
|
||||
versionCode 10216
|
||||
versionName "1.2.16"
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -38,8 +38,8 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':bramble-core', configuration: 'default')
|
||||
tor 'org.briarproject:tor-android:0.3.5.12@zip'
|
||||
tor 'org.briarproject:obfs4proxy-android:0.0.11-2@zip'
|
||||
tor 'org.briarproject:tor-android:0.3.5.13@zip'
|
||||
tor 'org.briarproject:obfs4proxy-android:0.0.12-dev-40245c4a@zip'
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public class AndroidUtils {
|
||||
private static final String FAKE_BLUETOOTH_ADDRESS = "02:00:00:00:00:00";
|
||||
|
||||
private static final String STORED_REPORTS = "dev-reports";
|
||||
private static final String STORED_LOGCAT = "dev-logcat";
|
||||
|
||||
public static Collection<String> getSupportedArchitectures() {
|
||||
List<String> abis = new ArrayList<>();
|
||||
@@ -107,6 +108,10 @@ public class AndroidUtils {
|
||||
return ctx.getDir(STORED_REPORTS, MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static File getLogcatFile(Context ctx) {
|
||||
return new File(ctx.getFilesDir(), STORED_LOGCAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of supported content types for image attachments.
|
||||
* GIFs can't be compressed on API < 24 so they're not supported.
|
||||
|
||||
@@ -75,8 +75,8 @@ dependencyVerification {
|
||||
'org.beanshell:bsh:1.3.0:bsh-1.3.0.jar:9b04edc75d19db54f1b4e8b5355e9364384c6cf71eb0a1b9724c159d779879f8',
|
||||
'org.bouncycastle:bcpkix-jdk15on:1.56:bcpkix-jdk15on-1.56.jar:7043dee4e9e7175e93e0b36f45b1ec1ecb893c5f755667e8b916eb8dd201c6ca',
|
||||
'org.bouncycastle:bcprov-jdk15on:1.56:bcprov-jdk15on-1.56.jar:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349',
|
||||
'org.briarproject:obfs4proxy-android:0.0.11-2:obfs4proxy-android-0.0.11-2.zip:57e55cbe87aa2aac210fdbb6cd8cdeafe15f825406a08ebf77a8b787aa2c6a8a',
|
||||
'org.briarproject:tor-android:0.3.5.12:tor-android-0.3.5.12.zip:db71fb3290acff79d572af0752570eaf6aad7c4d88c9b9aa0b4d5afe2b9ead9c',
|
||||
'org.briarproject:obfs4proxy-android:0.0.12-dev-40245c4a:obfs4proxy-android-0.0.12-dev-40245c4a.zip:8ab05a8f8391be2cb5ab2b665c281a06d9e3a756bd0f95a40a36ca927866ea82',
|
||||
'org.briarproject:tor-android:0.3.5.13:tor-android-0.3.5.13.zip:e0978db136731dae07774b722970cdae1e462fb5adc82845dd80a7e2d87f356c',
|
||||
'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.8.1:checker-qual-2.8.1.jar:9103499008bcecd4e948da29b17864abb64304e15706444ae209d17ebe0575df',
|
||||
|
||||
@@ -19,4 +19,10 @@ public interface StreamDecrypterFactory {
|
||||
*/
|
||||
StreamDecrypter createContactExchangeStreamDecrypter(InputStream in,
|
||||
SecretKey headerKey);
|
||||
|
||||
/**
|
||||
* Creates a {@link StreamDecrypter} for decrypting a log stream.
|
||||
*/
|
||||
StreamDecrypter createLogStreamDecrypter(InputStream in,
|
||||
SecretKey headerKey);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ public interface StreamEncrypterFactory {
|
||||
* Creates a {@link StreamEncrypter} for encrypting a contact exchange
|
||||
* stream.
|
||||
*/
|
||||
StreamEncrypter createContactExchangeStreamDecrypter(OutputStream out,
|
||||
StreamEncrypter createContactExchangeStreamEncrypter(OutputStream out,
|
||||
SecretKey headerKey);
|
||||
|
||||
/**
|
||||
* Creates a {@link StreamEncrypter} for encrypting a log stream.
|
||||
*/
|
||||
StreamEncrypter createLogStreamEncrypter(OutputStream out,
|
||||
SecretKey headerKey);
|
||||
}
|
||||
|
||||
@@ -13,4 +13,6 @@ public interface DevConfig {
|
||||
String getDevOnionAddress();
|
||||
|
||||
File getReportDir();
|
||||
|
||||
File getLogcatFile();
|
||||
}
|
||||
|
||||
@@ -16,8 +16,13 @@ public interface StreamReaderFactory {
|
||||
|
||||
/**
|
||||
* Creates an {@link InputStream InputStream} for reading from a contact
|
||||
* exchangestream.
|
||||
* exchange stream.
|
||||
*/
|
||||
InputStream createContactExchangeStreamReader(InputStream in,
|
||||
SecretKey headerKey);
|
||||
|
||||
/**
|
||||
* Creates an {@link InputStream} for reading from a log stream.
|
||||
*/
|
||||
InputStream createLogStreamReader(InputStream in, SecretKey headerKey);
|
||||
}
|
||||
|
||||
@@ -7,17 +7,19 @@ import java.io.OutputStream;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface StreamWriterFactory {
|
||||
|
||||
/**
|
||||
* Creates an {@link OutputStream OutputStream} for writing to a
|
||||
* transport stream
|
||||
* Creates a {@link StreamWriter} for writing to a transport stream.
|
||||
*/
|
||||
StreamWriter createStreamWriter(OutputStream out, StreamContext ctx);
|
||||
|
||||
/**
|
||||
* Creates an {@link OutputStream OutputStream} for writing to a contact
|
||||
* exchange stream.
|
||||
* Creates a {@link StreamWriter} for writing to a contact exchange stream.
|
||||
*/
|
||||
StreamWriter createContactExchangeStreamWriter(OutputStream out,
|
||||
SecretKey headerKey);
|
||||
|
||||
/**
|
||||
* Creates a {@link StreamWriter} for writing to a log stream.
|
||||
*/
|
||||
StreamWriter createLogStreamWriter(OutputStream out, SecretKey headerKey);
|
||||
}
|
||||
|
||||
@@ -9,14 +9,16 @@ import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
|
||||
class ScryptKdf implements PasswordBasedKdf {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ScryptKdf.class.getName());
|
||||
getLogger(ScryptKdf.class.getName());
|
||||
|
||||
private static final int MIN_COST = 256; // Min parameter N
|
||||
private static final int MAX_COST = 1024 * 1024; // Max parameter N
|
||||
@@ -33,10 +35,20 @@ class ScryptKdf implements PasswordBasedKdf {
|
||||
|
||||
@Override
|
||||
public int chooseCostParameter() {
|
||||
// Scrypt uses at least 128 * N * r bytes of memory. Don't use more
|
||||
// than half of the JVM's max heap size or we may run out of memory.
|
||||
// https://blog.filippo.io/the-scrypt-parameters/
|
||||
long maxMemory = Runtime.getRuntime().maxMemory();
|
||||
long maxCost = min(MAX_COST, maxMemory / BLOCK_SIZE / 256);
|
||||
if (LOG.isLoggable(INFO) && maxCost < MAX_COST) {
|
||||
LOG.info("Max cost capped at " + maxCost
|
||||
+ " due to max heap size " + maxMemory);
|
||||
}
|
||||
// Increase the cost from min to max while measuring performance
|
||||
int cost = MIN_COST;
|
||||
while (cost * 2 <= MAX_COST && measureDuration(cost) * 2 <= TARGET_MS)
|
||||
while (cost * 2 <= maxCost && measureDuration(cost) * 2 <= TARGET_MS) {
|
||||
cost *= 2;
|
||||
}
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("KDF cost parameter " + cost);
|
||||
return cost;
|
||||
|
||||
@@ -36,4 +36,10 @@ class StreamDecrypterFactoryImpl implements StreamDecrypterFactory {
|
||||
SecretKey headerKey) {
|
||||
return new StreamDecrypterImpl(in, cipherProvider.get(), 0, headerKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamDecrypter createLogStreamDecrypter(InputStream in,
|
||||
SecretKey headerKey) {
|
||||
return createContactExchangeStreamDecrypter(in, headerKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamEncrypter createContactExchangeStreamDecrypter(
|
||||
public StreamEncrypter createContactExchangeStreamEncrypter(
|
||||
OutputStream out, SecretKey headerKey) {
|
||||
AuthenticatedCipher cipher = cipherProvider.get();
|
||||
byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH];
|
||||
@@ -60,4 +60,10 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
|
||||
return new StreamEncrypterImpl(out, cipher, 0, null, streamHeaderNonce,
|
||||
headerKey, frameKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamEncrypter createLogStreamEncrypter(OutputStream out,
|
||||
SecretKey headerKey) {
|
||||
return createContactExchangeStreamEncrypter(out, headerKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,21 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
|
||||
|
||||
@Override
|
||||
public InputStream createStreamReader(InputStream in, StreamContext ctx) {
|
||||
return new StreamReaderImpl(
|
||||
streamDecrypterFactory.createStreamDecrypter(in, ctx));
|
||||
return new StreamReaderImpl(streamDecrypterFactory
|
||||
.createStreamDecrypter(in, ctx));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream createContactExchangeStreamReader(InputStream in,
|
||||
SecretKey headerKey) {
|
||||
return new StreamReaderImpl(
|
||||
streamDecrypterFactory.createContactExchangeStreamDecrypter(in,
|
||||
headerKey));
|
||||
return new StreamReaderImpl(streamDecrypterFactory
|
||||
.createContactExchangeStreamDecrypter(in, headerKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream createLogStreamReader(InputStream in,
|
||||
SecretKey headerKey) {
|
||||
return new StreamReaderImpl(streamDecrypterFactory
|
||||
.createLogStreamDecrypter(in, headerKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,21 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
|
||||
@Override
|
||||
public StreamWriter createStreamWriter(OutputStream out,
|
||||
StreamContext ctx) {
|
||||
return new StreamWriterImpl(
|
||||
streamEncrypterFactory.createStreamEncrypter(out, ctx));
|
||||
return new StreamWriterImpl(streamEncrypterFactory
|
||||
.createStreamEncrypter(out, ctx));
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamWriter createContactExchangeStreamWriter(OutputStream out,
|
||||
SecretKey headerKey) {
|
||||
return new StreamWriterImpl(
|
||||
streamEncrypterFactory.createContactExchangeStreamDecrypter(out,
|
||||
headerKey));
|
||||
return new StreamWriterImpl(streamEncrypterFactory
|
||||
.createContactExchangeStreamEncrypter(out, headerKey));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamWriter createLogStreamWriter(OutputStream out,
|
||||
SecretKey headerKey) {
|
||||
return new StreamWriterImpl(streamEncrypterFactory
|
||||
.createLogStreamEncrypter(out, headerKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: '*.jar')
|
||||
implementation 'net.java.dev.jna:jna:4.5.2'
|
||||
implementation 'net.java.dev.jna:jna-platform:4.5.2'
|
||||
tor 'org.briarproject:tor:0.3.5.12@zip'
|
||||
tor 'org.briarproject:obfs4proxy:0.0.7@zip'
|
||||
tor 'org.briarproject:tor:0.3.5.13@zip'
|
||||
tor 'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a@zip'
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public class BridgeTest extends BrambleTestCase {
|
||||
return component.getCircumventionProvider().getBridges(false);
|
||||
}
|
||||
|
||||
private final static long TIMEOUT = SECONDS.toMillis(30);
|
||||
private final static long TIMEOUT = SECONDS.toMillis(60);
|
||||
|
||||
private final static Logger LOG = getLogger(BridgeTest.class.getName());
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ dependencyVerification {
|
||||
'org.apache.ant:ant-launcher:1.9.4:ant-launcher-1.9.4.jar:7bccea20b41801ca17bcbc909a78c835d0f443f12d639c77bd6ae3d05861608d',
|
||||
'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.briarproject:obfs4proxy:0.0.7:obfs4proxy-0.0.7.zip:5b2f693262ce43a7e130f7cc7d5d1617925330640a2eb6d71085e95df8ee0642',
|
||||
'org.briarproject:tor:0.3.5.12:tor-0.3.5.12.zip:2f542c4befd216f2226bf7c76e3b8b2d99af6f146a8cb28bf727f42014587006',
|
||||
'org.briarproject:obfs4proxy:0.0.12-dev-40245c4a:obfs4proxy-0.0.12-dev-40245c4a.zip:172029e7058b3a83ac93ac4991a44bf76e16ce8d46f558f5836d57da3cb3a766',
|
||||
'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-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',
|
||||
|
||||
@@ -22,8 +22,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 10214
|
||||
versionName "1.2.14"
|
||||
versionCode 10216
|
||||
versionName "1.2.16"
|
||||
applicationId "org.briarproject.briar.android"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -136,6 +136,7 @@ dependencies {
|
||||
testImplementation "org.jmock:jmock:$jmockVersion"
|
||||
testImplementation "org.jmock:jmock-junit4:$jmockVersion"
|
||||
testImplementation "org.jmock:jmock-legacy:$jmockVersion"
|
||||
testAnnotationProcessor "com.google.dagger:dagger-compiler:2.24"
|
||||
|
||||
androidTestImplementation project(path: ':bramble-api', configuration: 'testOutput')
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.briarproject.briar.BriarCoreModule;
|
||||
import org.briarproject.briar.android.attachment.AttachmentModule;
|
||||
import org.briarproject.briar.android.attachment.media.MediaModule;
|
||||
import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
|
||||
import org.briarproject.briar.android.logging.CachingLogHandler;
|
||||
import org.briarproject.briar.android.login.SignInReminderReceiver;
|
||||
import org.briarproject.briar.android.view.EmojiTextInputView;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
@@ -179,6 +180,10 @@ public interface AndroidComponent
|
||||
|
||||
AndroidWakeLockManager wakeLockManager();
|
||||
|
||||
CachingLogHandler logHandler();
|
||||
|
||||
Thread.UncaughtExceptionHandler exceptionHandler();
|
||||
|
||||
void inject(SignInReminderReceiver briarService);
|
||||
|
||||
void inject(BriarService briarService);
|
||||
|
||||
@@ -33,11 +33,14 @@ import org.briarproject.briar.android.account.SetupModule;
|
||||
import org.briarproject.briar.android.contact.ContactListModule;
|
||||
import org.briarproject.briar.android.forum.ForumModule;
|
||||
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
|
||||
import org.briarproject.briar.android.logging.LoggingModule;
|
||||
import org.briarproject.briar.android.login.LoginModule;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerModule;
|
||||
import org.briarproject.briar.android.settings.SettingsModule;
|
||||
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
|
||||
import org.briarproject.briar.android.privategroup.list.GroupListModule;
|
||||
import org.briarproject.briar.android.reporting.DevReportModule;
|
||||
import org.briarproject.briar.android.settings.SettingsModule;
|
||||
import org.briarproject.briar.android.sharing.SharingModule;
|
||||
import org.briarproject.briar.android.test.TestAvatarCreatorImpl;
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelModule;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
@@ -72,6 +75,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
SetupModule.class,
|
||||
DozeHelperModule.class,
|
||||
ContactExchangeModule.class,
|
||||
LoggingModule.class,
|
||||
LoginModule.class,
|
||||
NavDrawerModule.class,
|
||||
ViewModelModule.class,
|
||||
@@ -79,8 +83,10 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
|
||||
DevReportModule.class,
|
||||
ContactListModule.class,
|
||||
// below need to be within same scope as ViewModelProvider.Factory
|
||||
ForumModule.BindsModule.class,
|
||||
ForumModule.class,
|
||||
GroupListModule.class,
|
||||
GroupConversationModule.class,
|
||||
SharingModule.class,
|
||||
})
|
||||
public class AppModule {
|
||||
|
||||
@@ -188,6 +194,11 @@ public class AppModule {
|
||||
public File getReportDir() {
|
||||
return AndroidUtils.getReportDir(app.getApplicationContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getLogcatFile() {
|
||||
return AndroidUtils.getLogcatFile(app.getApplicationContext());
|
||||
}
|
||||
};
|
||||
return devConfig;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,6 @@ import android.content.SharedPreferences;
|
||||
import org.briarproject.bramble.BrambleApplication;
|
||||
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
/**
|
||||
* This exists so that the Application object will not necessarily be cast
|
||||
* directly to the Briar application object.
|
||||
@@ -17,8 +14,6 @@ public interface BriarApplication extends BrambleApplication {
|
||||
|
||||
Class<? extends Activity> ENTRY_ACTIVITY = NavDrawerActivity.class;
|
||||
|
||||
Collection<LogRecord> getRecentLogRecords();
|
||||
|
||||
AndroidComponent getApplicationComponent();
|
||||
|
||||
SharedPreferences getDefaultSharedPreferences();
|
||||
|
||||
@@ -20,12 +20,10 @@ import org.briarproject.bramble.BrambleCoreEagerSingletons;
|
||||
import org.briarproject.briar.BriarCoreEagerSingletons;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.logging.CachingLogHandler;
|
||||
import org.briarproject.briar.android.reporting.BriarExceptionHandler;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -42,22 +40,18 @@ public class BriarApplicationImpl extends Application
|
||||
private static final Logger LOG =
|
||||
getLogger(BriarApplicationImpl.class.getName());
|
||||
|
||||
private final CachingLogHandler logHandler = new CachingLogHandler();
|
||||
private final BriarExceptionHandler exceptionHandler =
|
||||
new BriarExceptionHandler(this);
|
||||
|
||||
private AndroidComponent applicationComponent;
|
||||
private volatile SharedPreferences prefs;
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
|
||||
if (prefs == null)
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(base);
|
||||
// Loading the language needs to be done here.
|
||||
Localizer.initialize(prefs);
|
||||
super.attachBaseContext(
|
||||
Localizer.getInstance().setLocale(base));
|
||||
Localizer.getInstance().setLocale(this);
|
||||
setTheme(base, prefs);
|
||||
}
|
||||
|
||||
@@ -67,6 +61,11 @@ public class BriarApplicationImpl extends Application
|
||||
|
||||
if (IS_DEBUG_BUILD) enableStrictMode();
|
||||
|
||||
applicationComponent = createApplicationComponent();
|
||||
UncaughtExceptionHandler exceptionHandler =
|
||||
applicationComponent.exceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
|
||||
|
||||
Logger rootLogger = getLogger("");
|
||||
Handler[] handlers = rootLogger.getHandlers();
|
||||
// Disable the Android logger for release builds
|
||||
@@ -78,12 +77,12 @@ public class BriarApplicationImpl extends Application
|
||||
// Restore the default handlers after the level raising handler
|
||||
for (Handler handler : handlers) rootLogger.addHandler(handler);
|
||||
}
|
||||
CachingLogHandler logHandler = applicationComponent.logHandler();
|
||||
rootLogger.addHandler(logHandler);
|
||||
rootLogger.setLevel(IS_DEBUG_BUILD ? FINE : INFO);
|
||||
|
||||
LOG.info("Created");
|
||||
|
||||
applicationComponent = createApplicationComponent();
|
||||
EmojiManager.install(new GoogleEmojiProvider());
|
||||
}
|
||||
|
||||
@@ -136,11 +135,6 @@ public class BriarApplicationImpl extends Application
|
||||
return applicationComponent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LogRecord> getRecentLogRecords() {
|
||||
return logHandler.getRecentLogRecords();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AndroidComponent getApplicationComponent() {
|
||||
return applicationComponent;
|
||||
|
||||
@@ -37,7 +37,7 @@ import javax.inject.Inject;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
|
||||
import static android.app.NotificationManager.IMPORTANCE_NONE;
|
||||
import static android.app.NotificationManager.IMPORTANCE_LOW;
|
||||
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
import static android.content.Intent.ACTION_SHUTDOWN;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
|
||||
@@ -56,6 +56,7 @@ import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.FAILURE_CHANNEL_ID;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.FAILURE_NOTIFICATION_ID;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_ID;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_CHANNEL_OLD_ID;
|
||||
import static org.briarproject.briar.api.android.AndroidNotificationManager.ONGOING_NOTIFICATION_ID;
|
||||
import static org.briarproject.briar.api.android.LockManager.ACTION_LOCK;
|
||||
|
||||
@@ -120,11 +121,17 @@ public class BriarService extends Service {
|
||||
if (SDK_INT >= 26) {
|
||||
NotificationManager nm = (NotificationManager)
|
||||
requireNonNull(getSystemService(NOTIFICATION_SERVICE));
|
||||
// Delete the old notification channel, which had
|
||||
// IMPORTANCE_NONE and showed a badge
|
||||
nm.deleteNotificationChannel(ONGOING_CHANNEL_OLD_ID);
|
||||
// Use IMPORTANCE_LOW so the system doesn't show its own
|
||||
// notification on API 26-27
|
||||
NotificationChannel ongoingChannel = new NotificationChannel(
|
||||
ONGOING_CHANNEL_ID,
|
||||
getString(R.string.ongoing_notification_title),
|
||||
IMPORTANCE_NONE);
|
||||
IMPORTANCE_LOW);
|
||||
ongoingChannel.setLockscreenVisibility(VISIBILITY_SECRET);
|
||||
ongoingChannel.setShowBadge(false);
|
||||
nm.createNotificationChannel(ongoingChannel);
|
||||
NotificationChannel failureChannel = new NotificationChannel(
|
||||
FAILURE_CHANNEL_ID,
|
||||
@@ -170,6 +177,7 @@ public class BriarService extends Service {
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(Localizer.getInstance().setLocale(base));
|
||||
Localizer.getInstance().setLocale(this);
|
||||
}
|
||||
|
||||
private void showStartupFailureNotification(StartResult result) {
|
||||
|
||||
@@ -68,7 +68,21 @@ public class Localizer {
|
||||
return new Locale(tag);
|
||||
}
|
||||
|
||||
// Returns the localized version of context
|
||||
/*
|
||||
* Apply localization to the specified context.
|
||||
*
|
||||
* It updates the configuration of the context's resources object but can
|
||||
* also return a new context derived from the context parameter. Hence
|
||||
* make sure to work with the return value of this method instead of
|
||||
* the context you passed as a parameter.
|
||||
*
|
||||
* This method also has side-effects as it calls Locale#setDefault().
|
||||
*
|
||||
* When using this in attachBaseContext() of Application, Service or
|
||||
* Activity subclasses, it is important to not only apply this method to the
|
||||
* base Context parameter received in that method, but also apply it on the
|
||||
* class itself which also extends Context.
|
||||
*/
|
||||
public Context setLocale(Context context) {
|
||||
Resources res = context.getResources();
|
||||
Configuration conf = res.getConfiguration();
|
||||
@@ -82,7 +96,7 @@ public class Localizer {
|
||||
Locale.setDefault(locale);
|
||||
if (SDK_INT >= 17) {
|
||||
conf.setLocale(locale);
|
||||
context.createConfigurationContext(conf);
|
||||
context = context.createConfigurationContext(conf);
|
||||
} else
|
||||
conf.locale = locale;
|
||||
//noinspection deprecation
|
||||
|
||||
@@ -32,7 +32,6 @@ import org.briarproject.briar.android.conversation.ImageFragment;
|
||||
import org.briarproject.briar.android.forum.CreateForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumActivity;
|
||||
import org.briarproject.briar.android.forum.ForumListFragment;
|
||||
import org.briarproject.briar.android.forum.ForumModule;
|
||||
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
||||
import org.briarproject.briar.android.introduction.ContactChooserFragment;
|
||||
import org.briarproject.briar.android.introduction.IntroductionActivity;
|
||||
@@ -50,7 +49,6 @@ import org.briarproject.briar.android.navdrawer.TransportsActivity;
|
||||
import org.briarproject.briar.android.panic.PanicPreferencesActivity;
|
||||
import org.briarproject.briar.android.panic.PanicResponderActivity;
|
||||
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
|
||||
import org.briarproject.briar.android.privategroup.conversation.GroupConversationModule;
|
||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupActivity;
|
||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment;
|
||||
import org.briarproject.briar.android.privategroup.creation.CreateGroupModule;
|
||||
@@ -89,12 +87,10 @@ import dagger.Component;
|
||||
ActivityModule.class,
|
||||
BlogModule.class,
|
||||
CreateGroupModule.class,
|
||||
ForumModule.class,
|
||||
GroupInvitationModule.class,
|
||||
GroupConversationModule.class,
|
||||
GroupMemberModule.class,
|
||||
GroupRevealModule.class,
|
||||
SharingModule.class
|
||||
SharingModule.SharingLegacyModule.class
|
||||
}, dependencies = AndroidComponent.class)
|
||||
public interface ActivityComponent {
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.briarproject.briar.android.BriarApplication;
|
||||
import org.briarproject.briar.android.DestroyableContext;
|
||||
import org.briarproject.briar.android.Localizer;
|
||||
import org.briarproject.briar.android.controller.ActivityLifecycleController;
|
||||
import org.briarproject.briar.android.forum.ForumModule;
|
||||
import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
@@ -87,7 +86,6 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
activityComponent = DaggerActivityComponent.builder()
|
||||
.androidComponent(applicationComponent)
|
||||
.activityModule(getActivityModule())
|
||||
.forumModule(getForumModule())
|
||||
.build();
|
||||
injectActivity(activityComponent);
|
||||
super.onCreate(state);
|
||||
@@ -111,6 +109,7 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(
|
||||
Localizer.getInstance().setLocale(base));
|
||||
Localizer.getInstance().setLocale(this);
|
||||
}
|
||||
|
||||
public ActivityComponent getActivityComponent() {
|
||||
@@ -122,11 +121,6 @@ public abstract class BaseActivity extends AppCompatActivity
|
||||
return new ActivityModule(this);
|
||||
}
|
||||
|
||||
// TODO use a test module where this is used in tests
|
||||
protected ForumModule getForumModule() {
|
||||
return new ForumModule();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
@@ -160,7 +160,6 @@ public abstract class BriarActivity extends BaseActivity {
|
||||
* @param ownLayout true if the custom toolbar brings its own layout
|
||||
* @return the Toolbar object or null if content view did not contain one
|
||||
*/
|
||||
@Nullable
|
||||
protected Toolbar setUpCustomToolbar(boolean ownLayout) {
|
||||
// Custom Toolbar
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
|
||||
@@ -124,7 +124,7 @@ public class AddContactViewModel extends DbViewModel {
|
||||
return addContactResult;
|
||||
}
|
||||
|
||||
public void updatePendingContact(String name, PendingContact p) {
|
||||
void updatePendingContact(String name, PendingContact p) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
contactManager.removePendingContact(p.getId());
|
||||
|
||||
@@ -29,11 +29,11 @@ import org.briarproject.briar.android.fragment.BaseFragment;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog.Builder;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import static android.view.View.INVISIBLE;
|
||||
import static android.view.View.VISIBLE;
|
||||
@@ -48,6 +48,7 @@ import static org.briarproject.briar.android.util.UiUtils.getDialogIcon;
|
||||
public class NicknameFragment extends BaseFragment {
|
||||
|
||||
private static final String TAG = NicknameFragment.class.getName();
|
||||
private static final String SAVED_LINK = "savedLink";
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
@@ -67,6 +68,20 @@ public class NicknameFragment extends BaseFragment {
|
||||
@Override
|
||||
public void injectFragment(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
|
||||
.get(AddContactViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
// When the activity (and the ViewModel) get destroyed,
|
||||
// the link will not be available anymore and needs to be restored.
|
||||
// TODO migrate to SavedStateViewModelFactory (once we can use it)
|
||||
String savedLink = savedInstanceState.getString(SAVED_LINK);
|
||||
if (savedLink != null) viewModel.setRemoteHandshakeLink(savedLink);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -74,14 +89,9 @@ public class NicknameFragment extends BaseFragment {
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
if (getActivity() == null || getContext() == null) return null;
|
||||
|
||||
View v = inflater.inflate(R.layout.fragment_nickname,
|
||||
container, false);
|
||||
|
||||
viewModel = ViewModelProviders.of(getActivity(), viewModelFactory)
|
||||
.get(AddContactViewModel.class);
|
||||
|
||||
contactNameLayout = v.findViewById(R.id.contactNameLayout);
|
||||
contactNameInput = v.findViewById(R.id.contactNameInput);
|
||||
|
||||
@@ -93,6 +103,12 @@ public class NicknameFragment extends BaseFragment {
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(SAVED_LINK, viewModel.getRemoteHandshakeLink());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getNicknameOrNull() {
|
||||
Editable text = contactNameInput.getText();
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.Collection;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@Deprecated
|
||||
@NotNullByDefault
|
||||
public interface SharingController {
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@Deprecated
|
||||
@NotNullByDefault
|
||||
public class SharingControllerImpl implements SharingController, EventListener {
|
||||
|
||||
|
||||
@@ -6,52 +6,54 @@ import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityComponent;
|
||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.briar.android.forum.ForumController.ForumListener;
|
||||
import org.briarproject.briar.android.sharing.ForumSharingStatusActivity;
|
||||
import org.briarproject.briar.android.sharing.ShareForumActivity;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.briar.android.threaded.ThreadListActivity;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
import org.briarproject.briar.android.threaded.ThreadListViewModel;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SHARE_FORUM;
|
||||
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
|
||||
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class ForumActivity extends
|
||||
ThreadListActivity<Forum, ForumPostItem, ThreadItemAdapter<ForumPostItem>>
|
||||
implements ForumListener {
|
||||
ThreadListActivity<ForumPostItem, ThreadItemAdapter<ForumPostItem>> {
|
||||
|
||||
@Inject
|
||||
ForumController forumController;
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private ForumViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||
.get(ForumViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreadListController<Forum, ForumPostItem> getController() {
|
||||
return forumController;
|
||||
protected ThreadListViewModel<ForumPostItem> getViewModel() {
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreadItemAdapter<ForumPostItem> createAdapter() {
|
||||
return new ThreadItemAdapter<>(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,36 +61,27 @@ public class ForumActivity extends
|
||||
super.onCreate(state);
|
||||
|
||||
Toolbar toolbar = setUpCustomToolbar(false);
|
||||
|
||||
Intent i = getIntent();
|
||||
String groupName = i.getStringExtra(GROUP_NAME);
|
||||
if (groupName != null) setTitle(groupName);
|
||||
else loadNamedGroup();
|
||||
|
||||
// Open member list on Toolbar click
|
||||
if (toolbar != null) {
|
||||
toolbar.setOnClickListener(v -> {
|
||||
Intent i1 = new Intent(ForumActivity.this,
|
||||
ForumSharingStatusActivity.class);
|
||||
i1.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i1);
|
||||
});
|
||||
toolbar.setOnClickListener(v -> {
|
||||
Intent i = new Intent(ForumActivity.this,
|
||||
ForumSharingStatusActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i);
|
||||
});
|
||||
|
||||
String groupName = getIntent().getStringExtra(GROUP_NAME);
|
||||
if (groupName != null) {
|
||||
setTitle(groupName);
|
||||
} else {
|
||||
observeOnce(viewModel.loadForum(), this, forum ->
|
||||
setTitle(forum.getName())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNamedGroupLoaded(Forum forum) {
|
||||
setTitle(forum.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreadItemAdapter<ForumPostItem> createAdapter(
|
||||
LinearLayoutManager layoutManager) {
|
||||
return new ThreadItemAdapter<>(this, layoutManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int request, int result, Intent data) {
|
||||
protected void onActivityResult(int request, int result,
|
||||
@Nullable Intent data) {
|
||||
super.onActivityResult(request, result, data);
|
||||
|
||||
if (request == REQUEST_SHARE_FORUM && result == RESULT_OK) {
|
||||
@@ -101,32 +94,31 @@ public class ForumActivity extends
|
||||
// Inflate the menu items for use in the action bar
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.forum_actions, menu);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle presses on the action bar items
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_forum_share:
|
||||
Intent i2 = new Intent(this, ShareForumActivity.class);
|
||||
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||
i2.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivityForResult(i2, REQUEST_SHARE_FORUM);
|
||||
return true;
|
||||
case R.id.action_forum_sharing_status:
|
||||
Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
|
||||
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||
i3.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i3);
|
||||
return true;
|
||||
case R.id.action_forum_delete:
|
||||
showUnsubscribeDialog();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_forum_share) {
|
||||
Intent i = new Intent(this, ShareForumActivity.class);
|
||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivityForResult(i, REQUEST_SHARE_FORUM);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_forum_sharing_status) {
|
||||
Intent i = new Intent(this, ForumSharingStatusActivity.class);
|
||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_forum_delete) {
|
||||
showUnsubscribeDialog();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,7 +127,7 @@ public class ForumActivity extends
|
||||
}
|
||||
|
||||
private void showUnsubscribeDialog() {
|
||||
OnClickListener okListener = (dialog, which) -> deleteForum();
|
||||
OnClickListener okListener = (dialog, which) -> viewModel.deleteForum();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this,
|
||||
R.style.BriarDialogTheme);
|
||||
builder.setTitle(getString(R.string.dialog_title_leave_forum));
|
||||
@@ -145,27 +137,4 @@ public class ForumActivity extends
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void deleteForum() {
|
||||
forumController.deleteNamedGroup(
|
||||
new UiResultExceptionHandler<Void, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(Void v) {
|
||||
Toast.makeText(ForumActivity.this,
|
||||
R.string.forum_left_toast, LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onForumLeft(ContactId c) {
|
||||
sharingController.remove(c);
|
||||
setToolbarSubTitle(sharingController.getTotalCount(),
|
||||
sharingController.getOnlineCount());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@NotNullByDefault
|
||||
interface ForumController extends ThreadListController<Forum, ForumPostItem> {
|
||||
|
||||
interface ForumListener extends ThreadListListener<ForumPostItem> {
|
||||
@UiThread
|
||||
void onForumLeft(ContactId c);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
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.MessageId;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.briar.android.forum.ForumController.ForumListener;
|
||||
import org.briarproject.briar.android.threaded.ThreadListControllerImpl;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
import org.briarproject.briar.api.forum.ForumInvitationResponse;
|
||||
import org.briarproject.briar.api.forum.ForumManager;
|
||||
import org.briarproject.briar.api.forum.ForumPost;
|
||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||
import org.briarproject.briar.api.forum.ForumSharingManager;
|
||||
import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent;
|
||||
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
|
||||
import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
class ForumControllerImpl extends
|
||||
ThreadListControllerImpl<Forum, ForumPostItem, ForumPostHeader, ForumPost, ForumListener>
|
||||
implements ForumController {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ForumControllerImpl.class.getName());
|
||||
|
||||
private final ForumManager forumManager;
|
||||
private final ForumSharingManager forumSharingManager;
|
||||
|
||||
@Inject
|
||||
ForumControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
ForumManager forumManager, ForumSharingManager forumSharingManager,
|
||||
EventBus eventBus, Clock clock, MessageTracker messageTracker,
|
||||
AndroidNotificationManager notificationManager) {
|
||||
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
||||
eventBus, clock, notificationManager, messageTracker);
|
||||
this.forumManager = forumManager;
|
||||
this.forumSharingManager = forumSharingManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStart() {
|
||||
super.onActivityStart();
|
||||
notificationManager.clearForumPostNotification(getGroupId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
super.eventOccurred(e);
|
||||
|
||||
if (e instanceof ForumPostReceivedEvent) {
|
||||
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
|
||||
if (f.getGroupId().equals(getGroupId())) {
|
||||
LOG.info("Forum post received, adding...");
|
||||
listener.onItemReceived(buildItem(f.getHeader(), f.getText()));
|
||||
}
|
||||
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
|
||||
ForumInvitationResponseReceivedEvent f =
|
||||
(ForumInvitationResponseReceivedEvent) e;
|
||||
ForumInvitationResponse r = f.getMessageHeader();
|
||||
if (r.getShareableId().equals(getGroupId()) && r.wasAccepted()) {
|
||||
LOG.info("Forum invitation was accepted");
|
||||
listener.onInvitationAccepted(f.getContactId());
|
||||
}
|
||||
} else if (e instanceof ContactLeftShareableEvent) {
|
||||
ContactLeftShareableEvent c = (ContactLeftShareableEvent) e;
|
||||
if (c.getGroupId().equals(getGroupId())) {
|
||||
LOG.info("Forum left by contact");
|
||||
listener.onForumLeft(c.getContactId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Forum loadNamedGroup() throws DbException {
|
||||
return forumManager.getForum(getGroupId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<ForumPostHeader> loadHeaders() throws DbException {
|
||||
return forumManager.getPostHeaders(getGroupId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String loadMessageText(ForumPostHeader h) throws DbException {
|
||||
return forumManager.getPostText(h.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markRead(MessageId id) throws DbException {
|
||||
forumManager.setReadFlag(getGroupId(), id, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSharingContacts(
|
||||
ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Collection<Contact> contacts =
|
||||
forumSharingManager.getSharedWith(getGroupId());
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAndStoreMessage(String text,
|
||||
@Nullable ForumPostItem parentItem,
|
||||
ResultExceptionHandler<ForumPostItem, DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
GroupCount count = forumManager.getGroupCount(getGroupId());
|
||||
long timestamp = max(count.getLatestMsgTime() + 1,
|
||||
clock.currentTimeMillis());
|
||||
MessageId parentId = parentItem != null ?
|
||||
parentItem.getId() : null;
|
||||
createMessage(text, timestamp, parentId, author, handler);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createMessage(String text, long timestamp,
|
||||
@Nullable MessageId parentId, LocalAuthor author,
|
||||
ResultExceptionHandler<ForumPostItem, DbException> handler) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
LOG.info("Creating forum post...");
|
||||
ForumPost msg = forumManager.createLocalPost(getGroupId(), text,
|
||||
timestamp, parentId, author);
|
||||
storePost(msg, text, handler);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ForumPostHeader addLocalMessage(ForumPost p) throws DbException {
|
||||
return forumManager.addLocalPost(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteNamedGroup(Forum forum) throws DbException {
|
||||
forumManager.removeForum(forum);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ForumPostItem buildItem(ForumPostHeader header, String text) {
|
||||
return new ForumPostItem(header, text);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +1,23 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import org.briarproject.briar.android.activity.ActivityScope;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
@Module
|
||||
public class ForumModule {
|
||||
public interface ForumModule {
|
||||
|
||||
@Module
|
||||
public interface BindsModule {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ForumListViewModel.class)
|
||||
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
|
||||
}
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ForumListViewModel.class)
|
||||
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
ForumController provideForumController(BaseActivity activity,
|
||||
ForumControllerImpl forumController) {
|
||||
activity.addLifecycleController(forumController);
|
||||
return forumController;
|
||||
}
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ForumViewModel.class)
|
||||
ViewModel bindForumViewModel(ForumViewModel forumViewModel);
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.android.threaded.ThreadItem;
|
||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
@NotThreadSafe
|
||||
@@ -17,9 +16,4 @@ class ForumPostItem extends ThreadItem {
|
||||
h.getAuthorInfo(), h.isRead());
|
||||
}
|
||||
|
||||
ForumPostItem(MessageId messageId, @Nullable MessageId parentId,
|
||||
String text, long timestamp, Author author, AuthorInfo authorInfo) {
|
||||
super(messageId, parentId, text, timestamp, author, authorInfo, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import android.app.Application;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
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.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.MessageId;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.sharing.SharingController;
|
||||
import org.briarproject.briar.android.threaded.ThreadListViewModel;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.forum.Forum;
|
||||
import org.briarproject.briar.api.forum.ForumInvitationResponse;
|
||||
import org.briarproject.briar.api.forum.ForumManager;
|
||||
import org.briarproject.briar.api.forum.ForumPost;
|
||||
import org.briarproject.briar.api.forum.ForumPostHeader;
|
||||
import org.briarproject.briar.api.forum.ForumSharingManager;
|
||||
import org.briarproject.briar.api.forum.event.ForumInvitationResponseReceivedEvent;
|
||||
import org.briarproject.briar.api.forum.event.ForumPostReceivedEvent;
|
||||
import org.briarproject.briar.api.sharing.event.ContactLeftShareableEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.lang.Math.max;
|
||||
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;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class ForumViewModel extends ThreadListViewModel<ForumPostItem> {
|
||||
|
||||
private static final Logger LOG = getLogger(ForumViewModel.class.getName());
|
||||
|
||||
private final ForumManager forumManager;
|
||||
private final ForumSharingManager forumSharingManager;
|
||||
|
||||
@Inject
|
||||
ForumViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
IdentityManager identityManager,
|
||||
AndroidNotificationManager notificationManager,
|
||||
SharingController sharingController,
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
Clock clock,
|
||||
MessageTracker messageTracker,
|
||||
EventBus eventBus,
|
||||
ForumManager forumManager,
|
||||
ForumSharingManager forumSharingManager) {
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
|
||||
identityManager, notificationManager, sharingController,
|
||||
cryptoExecutor, clock, messageTracker, eventBus);
|
||||
this.forumManager = forumManager;
|
||||
this.forumSharingManager = forumSharingManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof ForumPostReceivedEvent) {
|
||||
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
|
||||
if (f.getGroupId().equals(groupId)) {
|
||||
LOG.info("Forum post received, adding...");
|
||||
ForumPostItem item =
|
||||
new ForumPostItem(f.getHeader(), f.getText());
|
||||
addItem(item, false);
|
||||
}
|
||||
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
|
||||
ForumInvitationResponseReceivedEvent f =
|
||||
(ForumInvitationResponseReceivedEvent) e;
|
||||
ForumInvitationResponse r = f.getMessageHeader();
|
||||
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
|
||||
LOG.info("Forum invitation was accepted");
|
||||
sharingController.add(f.getContactId());
|
||||
}
|
||||
} else if (e instanceof ContactLeftShareableEvent) {
|
||||
ContactLeftShareableEvent c = (ContactLeftShareableEvent) e;
|
||||
if (c.getGroupId().equals(groupId)) {
|
||||
LOG.info("Forum left by contact");
|
||||
sharingController.remove(c.getContactId());
|
||||
}
|
||||
} else {
|
||||
super.eventOccurred(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearNotifications() {
|
||||
notificationManager.clearForumPostNotification(groupId);
|
||||
}
|
||||
|
||||
LiveData<Forum> loadForum() {
|
||||
MutableLiveData<Forum> forum = new MutableLiveData<>();
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Forum f = forumManager.getForum(groupId);
|
||||
forum.postValue(f);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
return forum;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadItems() {
|
||||
loadList(txn -> {
|
||||
long start = now();
|
||||
List<ForumPostHeader> headers =
|
||||
forumManager.getPostHeaders(txn, groupId);
|
||||
logDuration(LOG, "Loading headers", start);
|
||||
start = now();
|
||||
List<ForumPostItem> items = new ArrayList<>();
|
||||
for (ForumPostHeader header : headers) {
|
||||
items.add(loadItem(txn, header));
|
||||
}
|
||||
logDuration(LOG, "Loading bodies and creating items", start);
|
||||
return items;
|
||||
}, this::setItems);
|
||||
}
|
||||
|
||||
private ForumPostItem loadItem(Transaction txn, ForumPostHeader header)
|
||||
throws DbException {
|
||||
String text = forumManager.getPostText(txn, header.getId());
|
||||
return new ForumPostItem(header, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAndStoreMessage(String text,
|
||||
@Nullable MessageId parentId) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
GroupCount count = forumManager.getGroupCount(groupId);
|
||||
long timestamp = max(count.getLatestMsgTime() + 1,
|
||||
clock.currentTimeMillis());
|
||||
createMessage(text, timestamp, parentId, author);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createMessage(String text, long timestamp,
|
||||
@Nullable MessageId parentId, LocalAuthor author) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
LOG.info("Creating forum post...");
|
||||
ForumPost msg = forumManager.createLocalPost(groupId, text,
|
||||
timestamp, parentId, author);
|
||||
storePost(msg, text);
|
||||
});
|
||||
}
|
||||
|
||||
private void storePost(ForumPost msg, String text) {
|
||||
runOnDbThread(false, txn -> {
|
||||
long start = now();
|
||||
ForumPostHeader header = forumManager.addLocalPost(txn, msg);
|
||||
logDuration(LOG, "Storing forum post", start);
|
||||
txn.attach(() -> {
|
||||
ForumPostItem item = new ForumPostItem(header, text);
|
||||
addItem(item, true);
|
||||
});
|
||||
}, e -> logException(LOG, WARNING, e));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markItemRead(ForumPostItem item) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
forumManager.setReadFlag(groupId, item.getId(), true);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadSharingContacts() {
|
||||
runOnDbThread(true, txn -> {
|
||||
Collection<Contact> contacts =
|
||||
forumSharingManager.getSharedWith(txn, groupId);
|
||||
Collection<ContactId> contactIds = new ArrayList<>(contacts.size());
|
||||
for (Contact c : contacts) contactIds.add(c.getId());
|
||||
txn.attach(() -> sharingController.addAll(contactIds));
|
||||
}, e -> logException(LOG, WARNING, e));
|
||||
}
|
||||
|
||||
void deleteForum() {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Forum f = forumManager.getForum(groupId);
|
||||
forumManager.removeForum(f);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
Toast.makeText(getApplication(), R.string.forum_left_toast,
|
||||
LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
import java.util.logging.Formatter;
|
||||
@@ -17,6 +18,16 @@ import static java.util.Locale.US;
|
||||
@NotNullByDefault
|
||||
public class BriefLogFormatter extends Formatter {
|
||||
|
||||
public static String formatLog(Formatter formatter,
|
||||
Collection<LogRecord> logRecords) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (LogRecord record : logRecords) {
|
||||
String formatted = formatter.format(record);
|
||||
sb.append(formatted).append('\n');
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private final Object lock = new Object();
|
||||
private final DateFormat dateFormat; // Locking: lock
|
||||
private final Date date; // Locking: lock
|
||||
|
||||
@@ -21,6 +21,10 @@ public class CachingLogHandler extends Handler {
|
||||
// Locking: lock
|
||||
private final Queue<LogRecord> recent = new LinkedList<>();
|
||||
|
||||
// package-private constructor
|
||||
CachingLogHandler() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(LogRecord record) {
|
||||
synchronized (lock) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.briarproject.briar.android.logging;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface LogDecrypter {
|
||||
/**
|
||||
* Returns decrypted log records from {@link AndroidUtils#getLogcatFile}
|
||||
* or null if there was an error reading the logs.
|
||||
*/
|
||||
@Nullable
|
||||
String decryptLogs(@Nullable byte[] logKey);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.briarproject.briar.android.logging;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.reporting.DevConfig;
|
||||
import org.briarproject.bramble.api.transport.StreamReaderFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Scanner;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
class LogDecrypterImpl implements LogDecrypter {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(LogDecrypterImpl.class.getName());
|
||||
|
||||
private final DevConfig devConfig;
|
||||
private final StreamReaderFactory streamReaderFactory;
|
||||
|
||||
@Inject
|
||||
LogDecrypterImpl(DevConfig devConfig,
|
||||
StreamReaderFactory streamReaderFactory) {
|
||||
this.devConfig = devConfig;
|
||||
this.streamReaderFactory = streamReaderFactory;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String decryptLogs(@Nullable byte[] logKey) {
|
||||
if (logKey == null) return null;
|
||||
SecretKey key = new SecretKey(logKey);
|
||||
File logFile = devConfig.getLogcatFile();
|
||||
try (InputStream in = new FileInputStream(logFile)) {
|
||||
InputStream reader =
|
||||
streamReaderFactory.createLogStreamReader(in, key);
|
||||
Scanner s = new Scanner(reader);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (s.hasNextLine()) sb.append(s.nextLine()).append("\n");
|
||||
s.close();
|
||||
return sb.toString();
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return null;
|
||||
} finally {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
logFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.briarproject.briar.android.logging;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface LogEncrypter {
|
||||
/**
|
||||
* Writes encrypted log records to {@link AndroidUtils#getLogcatFile}
|
||||
* and returns the encryption key if everything went fine.
|
||||
*/
|
||||
@Nullable
|
||||
byte[] encryptLogs();
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.briarproject.briar.android.logging;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.SecretKey;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.reporting.DevConfig;
|
||||
import org.briarproject.bramble.api.transport.StreamWriter;
|
||||
import org.briarproject.bramble.api.transport.StreamWriterFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.LogRecord;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@NotNullByDefault
|
||||
class LogEncrypterImpl implements LogEncrypter {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(LogEncrypterImpl.class.getName());
|
||||
|
||||
private final DevConfig devConfig;
|
||||
private final CachingLogHandler logHandler;
|
||||
private final CryptoComponent crypto;
|
||||
private final StreamWriterFactory streamWriterFactory;
|
||||
|
||||
@Inject
|
||||
LogEncrypterImpl(DevConfig devConfig,
|
||||
CachingLogHandler logHandler,
|
||||
CryptoComponent crypto,
|
||||
StreamWriterFactory streamWriterFactory) {
|
||||
this.devConfig = devConfig;
|
||||
this.logHandler = logHandler;
|
||||
this.crypto = crypto;
|
||||
this.streamWriterFactory = streamWriterFactory;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public byte[] encryptLogs() {
|
||||
SecretKey logKey = crypto.generateSecretKey();
|
||||
File logFile = devConfig.getLogcatFile();
|
||||
try (OutputStream out = new FileOutputStream(logFile)) {
|
||||
StreamWriter streamWriter =
|
||||
streamWriterFactory.createLogStreamWriter(out, logKey);
|
||||
Writer writer =
|
||||
new OutputStreamWriter(streamWriter.getOutputStream());
|
||||
writeLogString(writer);
|
||||
writer.close();
|
||||
return logKey.getBytes();
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLogString(Writer writer) throws IOException {
|
||||
Formatter formatter = new BriefLogFormatter();
|
||||
for (LogRecord record : logHandler.getRecentLogRecords()) {
|
||||
String formatted = formatter.format(record);
|
||||
writer.append(formatted).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.briarproject.briar.android.logging;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module
|
||||
public class LoggingModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
CachingLogHandler provideCachingLogHandler() {
|
||||
return new CachingLogHandler();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
LogEncrypter provideLogEncrypter(LogEncrypterImpl logEncrypter) {
|
||||
return logEncrypter;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
LogDecrypter provideLogDecrypter(LogDecrypterImpl logDecrypter) {
|
||||
return logDecrypter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -68,6 +68,7 @@ import static android.view.View.VISIBLE;
|
||||
import static androidx.core.view.GravityCompat.START;
|
||||
import static androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
|
||||
import static androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE;
|
||||
import static androidx.lifecycle.Lifecycle.State.STARTED;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
|
||||
@@ -297,6 +298,12 @@ public class NavDrawerActivity extends BriarActivity implements
|
||||
finish();
|
||||
} else if (fm.getBackStackEntryCount() == 0
|
||||
&& fm.findFragmentByTag(ContactListFragment.TAG) == null) {
|
||||
// don't start fragments in the wrong part of lifecycle (#1904)
|
||||
if (!getLifecycle().getCurrentState().isAtLeast(STARTED)) {
|
||||
LOG.warning("Tried to start contacts fragment in state " +
|
||||
getLifecycle().getCurrentState().name());
|
||||
return;
|
||||
}
|
||||
/*
|
||||
* This makes sure that the first fragment (ContactListFragment) the
|
||||
* user sees is the same as the last fragment the user sees before
|
||||
|
||||
@@ -1,67 +1,60 @@
|
||||
package org.briarproject.briar.android.privategroup.conversation;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
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.privategroup.conversation.GroupController.GroupListener;
|
||||
import org.briarproject.briar.android.privategroup.creation.GroupInviteActivity;
|
||||
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity;
|
||||
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
|
||||
import org.briarproject.briar.android.threaded.ThreadListActivity;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.briar.api.privategroup.Visibility;
|
||||
import org.briarproject.briar.android.threaded.ThreadListViewModel;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
|
||||
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_GROUP_INVITE;
|
||||
import static org.briarproject.briar.android.util.UiUtils.observeOnce;
|
||||
import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_TEXT_LENGTH;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class GroupActivity extends
|
||||
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageAdapter>
|
||||
implements GroupListener, OnClickListener {
|
||||
ThreadListActivity<GroupMessageItem, GroupMessageAdapter> {
|
||||
|
||||
@Inject
|
||||
GroupController controller;
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
@Nullable
|
||||
private Boolean isCreator = null;
|
||||
private boolean isDissolved = false;
|
||||
private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem,
|
||||
dissolveMenuItem;
|
||||
private GroupViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
viewModel = new ViewModelProvider(this, viewModelFactory)
|
||||
.get(GroupViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThreadListController<PrivateGroup, GroupMessageItem> getController() {
|
||||
return controller;
|
||||
protected ThreadListViewModel<GroupMessageItem> getViewModel() {
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GroupMessageAdapter createAdapter() {
|
||||
return new GroupMessageAdapter(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,65 +62,28 @@ public class GroupActivity extends
|
||||
super.onCreate(state);
|
||||
|
||||
Toolbar toolbar = setUpCustomToolbar(false);
|
||||
|
||||
Intent i = getIntent();
|
||||
String groupName = i.getStringExtra(GROUP_NAME);
|
||||
if (groupName != null) setTitle(groupName);
|
||||
loadNamedGroup();
|
||||
|
||||
// Open member list on Toolbar click
|
||||
if (toolbar != null) {
|
||||
toolbar.setOnClickListener(v -> {
|
||||
Intent i1 = new Intent(GroupActivity.this,
|
||||
GroupMemberListActivity.class);
|
||||
i1.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i1);
|
||||
});
|
||||
}
|
||||
toolbar.setOnClickListener(v -> {
|
||||
Intent i = new Intent(GroupActivity.this,
|
||||
GroupMemberListActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i);
|
||||
});
|
||||
|
||||
String groupName = getIntent().getStringExtra(GROUP_NAME);
|
||||
if (groupName != null) setTitle(groupName);
|
||||
observeOnce(viewModel.getPrivateGroup(), this, privateGroup ->
|
||||
setTitle(privateGroup.getName())
|
||||
);
|
||||
observeOnce(viewModel.isCreator(), this, adapter::setIsCreator);
|
||||
|
||||
// start with group disabled and enable when not dissolved
|
||||
setGroupEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GroupMessageAdapter createAdapter(
|
||||
LinearLayoutManager layoutManager) {
|
||||
return new GroupMessageAdapter(this, layoutManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadItems() {
|
||||
controller.isDissolved(
|
||||
new UiResultExceptionHandler<Boolean, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(Boolean isDissolved) {
|
||||
setGroupEnabled(!isDissolved);
|
||||
GroupActivity.super.loadItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNamedGroupLoaded(PrivateGroup group) {
|
||||
setTitle(group.getName());
|
||||
controller.loadLocalAuthor(
|
||||
new UiResultExceptionHandler<LocalAuthor, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(LocalAuthor author) {
|
||||
isCreator = group.getCreator().equals(author);
|
||||
adapter.setPerspective(isCreator);
|
||||
showMenuItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
viewModel.isDissolved().observe(this, dissolved -> {
|
||||
setGroupEnabled(!dissolved);
|
||||
// only show dialog when no prior state
|
||||
if (dissolved && state == null) onGroupDissolved();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,74 +92,61 @@ public class GroupActivity extends
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.group_actions, menu);
|
||||
|
||||
revealMenuItem = menu.findItem(R.id.action_group_reveal);
|
||||
inviteMenuItem = menu.findItem(R.id.action_group_invite);
|
||||
leaveMenuItem = menu.findItem(R.id.action_group_leave);
|
||||
dissolveMenuItem = menu.findItem(R.id.action_group_dissolve);
|
||||
|
||||
// all role-dependent items are invisible until we know our role
|
||||
revealMenuItem.setVisible(false);
|
||||
inviteMenuItem.setVisible(false);
|
||||
leaveMenuItem.setVisible(false);
|
||||
dissolveMenuItem.setVisible(false);
|
||||
|
||||
// show items based on role
|
||||
showMenuItems();
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
// show items based on role (which will not change, so observe once)
|
||||
observeOnce(viewModel.isCreator(), this, isCreator -> {
|
||||
menu.findItem(R.id.action_group_reveal).setVisible(!isCreator);
|
||||
menu.findItem(R.id.action_group_invite).setVisible(isCreator);
|
||||
menu.findItem(R.id.action_group_leave).setVisible(!isCreator);
|
||||
menu.findItem(R.id.action_group_dissolve).setVisible(isCreator);
|
||||
});
|
||||
super.onCreateOptionsMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_group_member_list:
|
||||
Intent i1 = new Intent(this, GroupMemberListActivity.class);
|
||||
i1.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i1);
|
||||
return true;
|
||||
case R.id.action_group_reveal:
|
||||
if (isCreator == null || isCreator)
|
||||
throw new IllegalStateException();
|
||||
Intent i2 = new Intent(this, RevealContactsActivity.class);
|
||||
i2.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i2);
|
||||
return true;
|
||||
case R.id.action_group_invite:
|
||||
if (isCreator == null || !isCreator)
|
||||
throw new IllegalStateException();
|
||||
Intent i3 = new Intent(this, GroupInviteActivity.class);
|
||||
i3.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivityForResult(i3, REQUEST_GROUP_INVITE);
|
||||
return true;
|
||||
case R.id.action_group_leave:
|
||||
if (isCreator == null || isCreator)
|
||||
throw new IllegalStateException();
|
||||
showLeaveGroupDialog();
|
||||
return true;
|
||||
case R.id.action_group_dissolve:
|
||||
if (isCreator == null || !isCreator)
|
||||
throw new IllegalStateException();
|
||||
showDissolveGroupDialog();
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_group_member_list) {
|
||||
Intent i = new Intent(this, GroupMemberListActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_group_reveal) {
|
||||
if (requireNonNull(viewModel.isCreator().getValue()))
|
||||
throw new IllegalStateException();
|
||||
Intent i = new Intent(this, RevealContactsActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivity(i);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_group_invite) {
|
||||
if (!requireNonNull(viewModel.isCreator().getValue()))
|
||||
throw new IllegalStateException();
|
||||
Intent i = new Intent(this, GroupInviteActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
startActivityForResult(i, REQUEST_GROUP_INVITE);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_group_leave) {
|
||||
if (requireNonNull(viewModel.isCreator().getValue()))
|
||||
throw new IllegalStateException();
|
||||
showLeaveGroupDialog();
|
||||
return true;
|
||||
} else if (itemId == R.id.action_group_dissolve) {
|
||||
if (!requireNonNull(viewModel.isCreator().getValue()))
|
||||
throw new IllegalStateException();
|
||||
showDissolveGroupDialog();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int request, int result, Intent data) {
|
||||
protected void onActivityResult(int request, int result,
|
||||
@Nullable Intent data) {
|
||||
if (request == REQUEST_GROUP_INVITE && result == RESULT_OK) {
|
||||
displaySnackbar(R.string.groups_invitation_sent);
|
||||
} else super.onActivityResult(request, result, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemReceived(GroupMessageItem item) {
|
||||
super.onItemReceived(item);
|
||||
if (item instanceof JoinMessageItem) {
|
||||
if (((JoinMessageItem) item).isInitial()) loadSharingContacts();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMaxTextLength() {
|
||||
return MAX_GROUP_POST_TEXT_LENGTH;
|
||||
@@ -211,11 +154,11 @@ public class GroupActivity extends
|
||||
|
||||
@Override
|
||||
public void onReplyClick(GroupMessageItem item) {
|
||||
if (!isDissolved) super.onReplyClick(item);
|
||||
Boolean isDissolved = viewModel.isDissolved().getValue();
|
||||
if (isDissolved != null && !isDissolved) super.onReplyClick(item);
|
||||
}
|
||||
|
||||
private void setGroupEnabled(boolean enabled) {
|
||||
isDissolved = !enabled;
|
||||
sendController.setReady(enabled);
|
||||
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
|
||||
|
||||
@@ -227,21 +170,13 @@ public class GroupActivity extends
|
||||
}
|
||||
}
|
||||
|
||||
private void showMenuItems() {
|
||||
// we need to have the menu items and know if we are the creator
|
||||
if (leaveMenuItem == null || isCreator == null) return;
|
||||
revealMenuItem.setVisible(!isCreator);
|
||||
inviteMenuItem.setVisible(isCreator);
|
||||
leaveMenuItem.setVisible(!isCreator);
|
||||
dissolveMenuItem.setVisible(isCreator);
|
||||
}
|
||||
|
||||
private void showLeaveGroupDialog() {
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
|
||||
builder.setTitle(getString(R.string.groups_leave_dialog_title));
|
||||
builder.setMessage(getString(R.string.groups_leave_dialog_message));
|
||||
builder.setNegativeButton(R.string.dialog_button_leave, this);
|
||||
builder.setNegativeButton(R.string.dialog_button_leave,
|
||||
(d, w) -> deleteGroup());
|
||||
builder.setPositiveButton(R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
@@ -251,37 +186,19 @@ public class GroupActivity extends
|
||||
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
|
||||
builder.setTitle(getString(R.string.groups_dissolve_dialog_title));
|
||||
builder.setMessage(getString(R.string.groups_dissolve_dialog_message));
|
||||
builder.setNegativeButton(R.string.groups_dissolve_button, this);
|
||||
builder.setNegativeButton(R.string.groups_dissolve_button,
|
||||
(d, w) -> deleteGroup());
|
||||
builder.setPositiveButton(R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
controller.deleteNamedGroup(
|
||||
new UiExceptionHandler<DbException>(this) {
|
||||
// The activity is going to be destroyed by the
|
||||
// GroupRemovedEvent being fired
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
private void deleteGroup() {
|
||||
// The activity is going to be destroyed by the
|
||||
// GroupRemovedEvent being fired
|
||||
viewModel.deletePrivateGroup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContactRelationshipRevealed(AuthorId memberId, ContactId c,
|
||||
Visibility v) {
|
||||
adapter.updateVisibility(memberId, v);
|
||||
|
||||
sharingController.add(c);
|
||||
setToolbarSubTitle(sharingController.getTotalCount(),
|
||||
sharingController.getOnlineCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGroupDissolved() {
|
||||
setGroupEnabled(false);
|
||||
private void onGroupDissolved() {
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(this, R.style.BriarDialogTheme);
|
||||
builder.setTitle(getString(R.string.groups_dissolved_dialog_title));
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.briarproject.briar.android.privategroup.conversation;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.briar.api.privategroup.Visibility;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
public interface GroupController
|
||||
extends ThreadListController<PrivateGroup, GroupMessageItem> {
|
||||
|
||||
void loadLocalAuthor(
|
||||
ResultExceptionHandler<LocalAuthor, DbException> handler);
|
||||
|
||||
void isDissolved(
|
||||
ResultExceptionHandler<Boolean, DbException> handler);
|
||||
|
||||
interface GroupListener extends ThreadListListener<GroupMessageItem> {
|
||||
|
||||
@UiThread
|
||||
void onContactRelationshipRevealed(AuthorId memberId,
|
||||
ContactId contactId, Visibility v);
|
||||
|
||||
@UiThread
|
||||
void onGroupDissolved();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package org.briarproject.briar.android.privategroup.conversation;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
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.MessageId;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.briar.android.privategroup.conversation.GroupController.GroupListener;
|
||||
import org.briarproject.briar.android.threaded.ThreadListControllerImpl;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.privategroup.GroupMember;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessage;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.JoinMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.briar.api.privategroup.event.ContactRelationshipRevealedEvent;
|
||||
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
|
||||
import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent;
|
||||
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
|
||||
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class GroupControllerImpl extends
|
||||
ThreadListControllerImpl<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessage, GroupListener>
|
||||
implements GroupController {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(GroupControllerImpl.class.getName());
|
||||
|
||||
private final PrivateGroupManager privateGroupManager;
|
||||
private final GroupMessageFactory groupMessageFactory;
|
||||
|
||||
@Inject
|
||||
GroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
GroupMessageFactory groupMessageFactory, EventBus eventBus,
|
||||
MessageTracker messageTracker, Clock clock,
|
||||
AndroidNotificationManager notificationManager) {
|
||||
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
||||
eventBus, clock, notificationManager, messageTracker);
|
||||
this.privateGroupManager = privateGroupManager;
|
||||
this.groupMessageFactory = groupMessageFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStart() {
|
||||
super.onActivityStart();
|
||||
notificationManager.clearGroupMessageNotification(getGroupId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
super.eventOccurred(e);
|
||||
|
||||
if (e instanceof GroupMessageAddedEvent) {
|
||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||
if (!g.isLocal() && g.getGroupId().equals(getGroupId())) {
|
||||
LOG.info("Group message received, adding...");
|
||||
listener.onItemReceived(buildItem(g.getHeader(), g.getText()));
|
||||
}
|
||||
} else if (e instanceof ContactRelationshipRevealedEvent) {
|
||||
ContactRelationshipRevealedEvent c =
|
||||
(ContactRelationshipRevealedEvent) e;
|
||||
if (getGroupId().equals(c.getGroupId())) {
|
||||
listener.onContactRelationshipRevealed(c.getMemberId(),
|
||||
c.getContactId(), c.getVisibility());
|
||||
}
|
||||
} else if (e instanceof GroupInvitationResponseReceivedEvent) {
|
||||
GroupInvitationResponseReceivedEvent g =
|
||||
(GroupInvitationResponseReceivedEvent) e;
|
||||
GroupInvitationResponse r = g.getMessageHeader();
|
||||
if (getGroupId().equals(r.getShareableId()) && r.wasAccepted()) {
|
||||
listener.onInvitationAccepted(g.getContactId());
|
||||
}
|
||||
} else if (e instanceof GroupDissolvedEvent) {
|
||||
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
|
||||
if (getGroupId().equals(g.getGroupId())) {
|
||||
listener.onGroupDissolved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PrivateGroup loadNamedGroup() throws DbException {
|
||||
return privateGroupManager.getPrivateGroup(getGroupId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<GroupMessageHeader> loadHeaders() throws DbException {
|
||||
return privateGroupManager.getHeaders(getGroupId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String loadMessageText(GroupMessageHeader header)
|
||||
throws DbException {
|
||||
if (header instanceof JoinMessageHeader) {
|
||||
// will be looked up later
|
||||
return "";
|
||||
}
|
||||
return privateGroupManager.getMessageText(header.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markRead(MessageId id) throws DbException {
|
||||
privateGroupManager.setReadFlag(getGroupId(), id, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadSharingContacts(
|
||||
ResultExceptionHandler<Collection<ContactId>, DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
Collection<GroupMember> members =
|
||||
privateGroupManager.getMembers(getGroupId());
|
||||
Collection<ContactId> contactIds = new ArrayList<>();
|
||||
for (GroupMember m : members) {
|
||||
if (m.getContactId() != null)
|
||||
contactIds.add(m.getContactId());
|
||||
}
|
||||
handler.onResult(contactIds);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAndStoreMessage(String text,
|
||||
@Nullable GroupMessageItem parentItem,
|
||||
ResultExceptionHandler<GroupMessageItem, DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
MessageId parentId = null;
|
||||
MessageId previousMsgId =
|
||||
privateGroupManager.getPreviousMsgId(getGroupId());
|
||||
GroupCount count =
|
||||
privateGroupManager.getGroupCount(getGroupId());
|
||||
long timestamp = count.getLatestMsgTime();
|
||||
if (parentItem != null) parentId = parentItem.getId();
|
||||
timestamp = max(clock.currentTimeMillis(), timestamp + 1);
|
||||
createMessage(text, timestamp, parentId, author, previousMsgId,
|
||||
handler);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createMessage(String text, long timestamp,
|
||||
@Nullable MessageId parentId, LocalAuthor author,
|
||||
MessageId previousMsgId,
|
||||
ResultExceptionHandler<GroupMessageItem, DbException> handler) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
LOG.info("Creating group message...");
|
||||
GroupMessage msg = groupMessageFactory
|
||||
.createGroupMessage(getGroupId(), timestamp,
|
||||
parentId, author, text, previousMsgId);
|
||||
storePost(msg, text, handler);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GroupMessageHeader addLocalMessage(GroupMessage message)
|
||||
throws DbException {
|
||||
return privateGroupManager.addLocalMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteNamedGroup(PrivateGroup group) throws DbException {
|
||||
privateGroupManager.removePrivateGroup(group.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GroupMessageItem buildItem(GroupMessageHeader header,
|
||||
String text) {
|
||||
if (header instanceof JoinMessageHeader) {
|
||||
return new JoinMessageItem((JoinMessageHeader) header, text);
|
||||
}
|
||||
return new GroupMessageItem(header, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadLocalAuthor(
|
||||
ResultExceptionHandler<LocalAuthor, DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
handler.onResult(author);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void isDissolved(
|
||||
ResultExceptionHandler<Boolean, DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
boolean isDissolved =
|
||||
privateGroupManager.isDissolved(getGroupId());
|
||||
handler.onResult(isDissolved);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
package org.briarproject.briar.android.privategroup.conversation;
|
||||
|
||||
import org.briarproject.briar.android.activity.ActivityScope;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.viewmodel.ViewModelKey;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
@Module
|
||||
public class GroupConversationModule {
|
||||
public interface GroupConversationModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(GroupViewModel.class)
|
||||
ViewModel bindGroupViewModel(GroupViewModel groupViewModel);
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
GroupController provideGroupController(BaseActivity activity,
|
||||
GroupControllerImpl groupController) {
|
||||
activity.addLifecycleController(groupController);
|
||||
return groupController;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,14 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.threaded.BaseThreadItemViewHolder;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.briar.android.threaded.ThreadPostViewHolder;
|
||||
import org.briarproject.briar.api.privategroup.Visibility;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
@@ -24,15 +19,14 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
||||
|
||||
private boolean isCreator = false;
|
||||
|
||||
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener,
|
||||
LinearLayoutManager layoutManager) {
|
||||
super(listener, layoutManager);
|
||||
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener) {
|
||||
super(listener);
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
GroupMessageItem item = items.get(position);
|
||||
GroupMessageItem item = getItem(position);
|
||||
return item.getLayout();
|
||||
}
|
||||
|
||||
@@ -47,30 +41,9 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
||||
return new ThreadPostViewHolder<>(v);
|
||||
}
|
||||
|
||||
void setPerspective(boolean isCreator) {
|
||||
void setIsCreator(boolean isCreator) {
|
||||
this.isCreator = isCreator;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void updateVisibility(AuthorId memberId, Visibility v) {
|
||||
int position = findItemPosition(memberId);
|
||||
if (position != NO_POSITION) {
|
||||
GroupMessageItem item = items.get(position);
|
||||
if (item instanceof JoinMessageItem) {
|
||||
((JoinMessageItem) item).setVisibility(v);
|
||||
notifyItemChanged(findItemPosition(item), item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int findItemPosition(AuthorId a) {
|
||||
int count = items.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
GroupMessageItem item = items.get(i);
|
||||
if (item.getAuthor().getId().equals(a))
|
||||
return i;
|
||||
}
|
||||
return NO_POSITION; // Not found
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
package org.briarproject.briar.android.privategroup.conversation;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
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.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.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.briar.android.sharing.SharingController;
|
||||
import org.briarproject.briar.android.threaded.ThreadListViewModel;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
import org.briarproject.briar.api.privategroup.GroupMember;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessage;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.briar.api.privategroup.GroupMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.JoinMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.briar.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.briar.api.privategroup.event.ContactRelationshipRevealedEvent;
|
||||
import org.briarproject.briar.api.privategroup.event.GroupDissolvedEvent;
|
||||
import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent;
|
||||
import org.briarproject.briar.api.privategroup.event.GroupMessageAddedEvent;
|
||||
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
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;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
class GroupViewModel extends ThreadListViewModel<GroupMessageItem> {
|
||||
|
||||
private static final Logger LOG = getLogger(GroupViewModel.class.getName());
|
||||
|
||||
private final PrivateGroupManager privateGroupManager;
|
||||
private final GroupMessageFactory groupMessageFactory;
|
||||
|
||||
private final MutableLiveData<PrivateGroup> privateGroup =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> isCreator = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> isDissolved =
|
||||
new MutableLiveData<>();
|
||||
|
||||
@Inject
|
||||
GroupViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
EventBus eventBus,
|
||||
IdentityManager identityManager,
|
||||
AndroidNotificationManager notificationManager,
|
||||
SharingController sharingController,
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
Clock clock,
|
||||
MessageTracker messageTracker,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
GroupMessageFactory groupMessageFactory) {
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor,
|
||||
identityManager, notificationManager, sharingController,
|
||||
cryptoExecutor, clock, messageTracker, eventBus);
|
||||
this.privateGroupManager = privateGroupManager;
|
||||
this.groupMessageFactory = groupMessageFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof GroupMessageAddedEvent) {
|
||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||
// only act on non-local messages in this group
|
||||
if (!g.isLocal() && g.getGroupId().equals(groupId)) {
|
||||
LOG.info("Group message received, adding...");
|
||||
GroupMessageItem item = buildItem(g.getHeader(), g.getText());
|
||||
addItem(item, false);
|
||||
// In case the join message comes from the creator,
|
||||
// we need to reload the sharing contacts
|
||||
// in case it was delayed and the sharing count is wrong (#850).
|
||||
if (item instanceof JoinMessageItem &&
|
||||
(((JoinMessageItem) item).isInitial())) {
|
||||
loadSharingContacts();
|
||||
}
|
||||
}
|
||||
} else if (e instanceof GroupInvitationResponseReceivedEvent) {
|
||||
GroupInvitationResponseReceivedEvent g =
|
||||
(GroupInvitationResponseReceivedEvent) e;
|
||||
GroupInvitationResponse r = g.getMessageHeader();
|
||||
if (r.getShareableId().equals(groupId) && r.wasAccepted()) {
|
||||
sharingController.add(g.getContactId());
|
||||
}
|
||||
} else if (e instanceof ContactRelationshipRevealedEvent) {
|
||||
ContactRelationshipRevealedEvent c =
|
||||
(ContactRelationshipRevealedEvent) e;
|
||||
if (c.getGroupId().equals(groupId)) {
|
||||
sharingController.add(c.getContactId());
|
||||
}
|
||||
} else if (e instanceof GroupDissolvedEvent) {
|
||||
GroupDissolvedEvent g = (GroupDissolvedEvent) e;
|
||||
if (g.getGroupId().equals(groupId)) {
|
||||
isDissolved.setValue(true);
|
||||
}
|
||||
} else {
|
||||
super.eventOccurred(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void performInitialLoad() {
|
||||
super.performInitialLoad();
|
||||
loadPrivateGroup(groupId);
|
||||
}
|
||||
|
||||
protected void clearNotifications() {
|
||||
notificationManager.clearGroupMessageNotification(groupId);
|
||||
}
|
||||
|
||||
private void loadPrivateGroup(GroupId groupId) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
PrivateGroup g = privateGroupManager.getPrivateGroup(groupId);
|
||||
privateGroup.postValue(g);
|
||||
Author author = identityManager.getLocalAuthor();
|
||||
isCreator.postValue(g.getCreator().equals(author));
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadItems() {
|
||||
loadList(txn -> {
|
||||
// check first if group is dissolved
|
||||
isDissolved
|
||||
.postValue(privateGroupManager.isDissolved(txn, groupId));
|
||||
// now continue to load the items
|
||||
long start = now();
|
||||
List<GroupMessageHeader> headers =
|
||||
privateGroupManager.getHeaders(txn, groupId);
|
||||
logDuration(LOG, "Loading headers", start);
|
||||
start = now();
|
||||
List<GroupMessageItem> items = new ArrayList<>();
|
||||
for (GroupMessageHeader header : headers) {
|
||||
items.add(loadItem(txn, header));
|
||||
}
|
||||
logDuration(LOG, "Loading bodies and creating items", start);
|
||||
return items;
|
||||
}, this::setItems);
|
||||
}
|
||||
|
||||
private GroupMessageItem loadItem(Transaction txn,
|
||||
GroupMessageHeader header) throws DbException {
|
||||
String text;
|
||||
if (header instanceof JoinMessageHeader) {
|
||||
// will be looked up later
|
||||
text = "";
|
||||
} else {
|
||||
text = privateGroupManager.getMessageText(txn, header.getId());
|
||||
}
|
||||
return buildItem(header, text);
|
||||
}
|
||||
|
||||
private GroupMessageItem buildItem(GroupMessageHeader header, String text) {
|
||||
if (header instanceof JoinMessageHeader) {
|
||||
return new JoinMessageItem((JoinMessageHeader) header, text);
|
||||
}
|
||||
return new GroupMessageItem(header, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createAndStoreMessage(String text,
|
||||
@Nullable MessageId parentId) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
LocalAuthor author = identityManager.getLocalAuthor();
|
||||
MessageId previousMsgId =
|
||||
privateGroupManager.getPreviousMsgId(groupId);
|
||||
GroupCount count = privateGroupManager.getGroupCount(groupId);
|
||||
long timestamp = count.getLatestMsgTime();
|
||||
timestamp = max(clock.currentTimeMillis(), timestamp + 1);
|
||||
createMessage(text, timestamp, parentId, author, previousMsgId);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createMessage(String text, long timestamp,
|
||||
@Nullable MessageId parentId, LocalAuthor author,
|
||||
MessageId previousMsgId) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
LOG.info("Creating group message...");
|
||||
GroupMessage msg = groupMessageFactory.createGroupMessage(groupId,
|
||||
timestamp, parentId, author, text, previousMsgId);
|
||||
storePost(msg, text);
|
||||
});
|
||||
}
|
||||
|
||||
private void storePost(GroupMessage msg, String text) {
|
||||
runOnDbThread(false, txn -> {
|
||||
long start = now();
|
||||
GroupMessageHeader header =
|
||||
privateGroupManager.addLocalMessage(txn, msg);
|
||||
logDuration(LOG, "Storing group message", start);
|
||||
txn.attach(() ->
|
||||
addItem(buildItem(header, text), true)
|
||||
);
|
||||
}, e -> logException(LOG, WARNING, e));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markItemRead(GroupMessageItem item) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
privateGroupManager.setReadFlag(groupId, item.getId(), true);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void loadSharingContacts() {
|
||||
runOnDbThread(true, txn -> {
|
||||
Collection<GroupMember> members =
|
||||
privateGroupManager.getMembers(txn, groupId);
|
||||
Collection<ContactId> contactIds = new ArrayList<>();
|
||||
for (GroupMember m : members) {
|
||||
if (m.getContactId() != null)
|
||||
contactIds.add(m.getContactId());
|
||||
}
|
||||
txn.attach(() -> sharingController.addAll(contactIds));
|
||||
}, e -> logException(LOG, WARNING, e));
|
||||
}
|
||||
|
||||
void deletePrivateGroup() {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
privateGroupManager.removePrivateGroup(groupId);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
LiveData<PrivateGroup> getPrivateGroup() {
|
||||
return privateGroup;
|
||||
}
|
||||
|
||||
LiveData<Boolean> isCreator() {
|
||||
return isCreator;
|
||||
}
|
||||
|
||||
LiveData<Boolean> isDissolved() {
|
||||
return isDissolved;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package org.briarproject.briar.android.privategroup.conversation;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.api.privategroup.JoinMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.Visibility;
|
||||
|
||||
import javax.annotation.concurrent.NotThreadSafe;
|
||||
|
||||
@@ -13,13 +12,11 @@ import androidx.annotation.UiThread;
|
||||
@NotThreadSafe
|
||||
class JoinMessageItem extends GroupMessageItem {
|
||||
|
||||
private Visibility visibility;
|
||||
private final boolean isInitial;
|
||||
|
||||
JoinMessageItem(JoinMessageHeader h, String text) {
|
||||
super(h, text);
|
||||
this.visibility = h.getVisibility();
|
||||
this.isInitial = h.isInitial();
|
||||
isInitial = h.isInitial();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,14 +30,6 @@ class JoinMessageItem extends GroupMessageItem {
|
||||
return R.layout.list_item_group_join_notice;
|
||||
}
|
||||
|
||||
Visibility getVisibility() {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
void setVisibility(Visibility visibility) {
|
||||
this.visibility = visibility;
|
||||
}
|
||||
|
||||
boolean isInitial() {
|
||||
return isInitial;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
package org.briarproject.briar.android.reporting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.app.Application;
|
||||
import android.os.Process;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.android.logging.LogEncrypter;
|
||||
|
||||
import java.lang.Thread.UncaughtExceptionHandler;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.briar.android.util.UiUtils.startDevReportActivity;
|
||||
|
||||
@NotNullByDefault
|
||||
public class BriarExceptionHandler implements UncaughtExceptionHandler {
|
||||
class BriarExceptionHandler implements UncaughtExceptionHandler {
|
||||
|
||||
private final Context ctx;
|
||||
private final Application app;
|
||||
private final LogEncrypter logEncrypter;
|
||||
private final long appStartTime;
|
||||
|
||||
public BriarExceptionHandler(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
this.appStartTime = System.currentTimeMillis();
|
||||
@Inject
|
||||
BriarExceptionHandler(Application app, LogEncrypter logEncrypter) {
|
||||
this.app = app;
|
||||
this.logEncrypter = logEncrypter;
|
||||
appStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
// encrypt logs to disk before handing over to new process
|
||||
// the intent has limited space, so we can't reliably store them there.
|
||||
byte[] logKey = logEncrypter.encryptLogs();
|
||||
|
||||
// activity runs in its own process, so we can kill the old one
|
||||
startDevReportActivity(ctx, CrashReportActivity.class, e, appStartTime);
|
||||
startDevReportActivity(app.getApplicationContext(),
|
||||
CrashReportActivity.class, e, appStartTime, logKey);
|
||||
Process.killProcess(Process.myPid());
|
||||
System.exit(10);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.BuildConfig;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.BriarApplication;
|
||||
import org.briarproject.briar.android.logging.BriefLogFormatter;
|
||||
import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo;
|
||||
import org.briarproject.briar.android.reporting.ReportData.ReportItem;
|
||||
import org.briarproject.briar.android.reporting.ReportData.SingleReportInfo;
|
||||
@@ -41,8 +39,6 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@@ -74,8 +70,8 @@ class BriarReportCollector {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
public ReportData collectReportData(@Nullable Throwable t,
|
||||
long appStartTime) {
|
||||
ReportData collectReportData(@Nullable Throwable t, long appStartTime,
|
||||
String logs) {
|
||||
ReportData reportData = new ReportData()
|
||||
.add(getBasicInfo(t))
|
||||
.add(getDeviceInfo());
|
||||
@@ -86,7 +82,7 @@ class BriarReportCollector {
|
||||
.add(getStorage())
|
||||
.add(getConnectivity())
|
||||
.add(getBuildConfig())
|
||||
.add(getLogcat())
|
||||
.add(getLogcat(logs))
|
||||
.add(getDeviceFeatures());
|
||||
}
|
||||
|
||||
@@ -313,15 +309,8 @@ class BriarReportCollector {
|
||||
buildConfig);
|
||||
}
|
||||
|
||||
private ReportItem getLogcat() {
|
||||
BriarApplication app = (BriarApplication) ctx.getApplicationContext();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Formatter formatter = new BriefLogFormatter();
|
||||
for (LogRecord record : app.getRecentLogRecords()) {
|
||||
sb.append(formatter.format(record)).append('\n');
|
||||
}
|
||||
return new ReportItem("Logcat", R.string.dev_report_logcat,
|
||||
sb.toString());
|
||||
private ReportItem getLogcat(String logs) {
|
||||
return new ReportItem("Logcat", R.string.dev_report_logcat, logs);
|
||||
}
|
||||
|
||||
private ReportItem getDeviceFeatures() {
|
||||
|
||||
@@ -35,6 +35,7 @@ public class CrashReportActivity extends BaseActivity
|
||||
|
||||
public static final String EXTRA_THROWABLE = "throwable";
|
||||
public static final String EXTRA_APP_START_TIME = "appStartTime";
|
||||
public static final String EXTRA_APP_LOGCAT = "logcat";
|
||||
|
||||
@Inject
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
@@ -56,7 +57,8 @@ public class CrashReportActivity extends BaseActivity
|
||||
Intent intent = getIntent();
|
||||
Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE);
|
||||
long appStartTime = intent.getLongExtra(EXTRA_APP_START_TIME, -1);
|
||||
viewModel.init(t, appStartTime);
|
||||
byte[] logKey = intent.getByteArrayExtra(EXTRA_APP_LOGCAT);
|
||||
viewModel.init(t, appStartTime, logKey);
|
||||
viewModel.getShowReport().observeEvent(this, show -> {
|
||||
if (show) displayFragment(true);
|
||||
});
|
||||
|
||||
@@ -15,4 +15,8 @@ public abstract class DevReportModule {
|
||||
@ViewModelKey(ReportViewModel.class)
|
||||
abstract ViewModel bindReportViewModel(ReportViewModel reportViewModel);
|
||||
|
||||
@Binds
|
||||
abstract Thread.UncaughtExceptionHandler bindUncaughtExceptionHandler(
|
||||
BriarExceptionHandler handler);
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ import org.briarproject.bramble.api.plugin.TorConstants;
|
||||
import org.briarproject.bramble.api.reporting.DevReporter;
|
||||
import org.briarproject.bramble.util.AndroidUtils;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.logging.BriefLogFormatter;
|
||||
import org.briarproject.briar.android.logging.CachingLogHandler;
|
||||
import org.briarproject.briar.android.logging.LogDecrypter;
|
||||
import org.briarproject.briar.android.reporting.ReportData.MultiReportInfo;
|
||||
import org.briarproject.briar.android.viewmodel.LiveEvent;
|
||||
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
|
||||
@@ -19,6 +22,7 @@ import org.json.JSONException;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Formatter;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -36,13 +40,16 @@ import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
import static org.briarproject.briar.android.logging.BriefLogFormatter.formatLog;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ReportViewModel extends AndroidViewModel {
|
||||
class ReportViewModel extends AndroidViewModel {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ReportViewModel.class.getName());
|
||||
|
||||
private final CachingLogHandler logHandler;
|
||||
private final LogDecrypter logDecrypter;
|
||||
private final BriarReportCollector collector;
|
||||
private final DevReporter reporter;
|
||||
private final PluginManager pluginManager;
|
||||
@@ -58,18 +65,39 @@ public class ReportViewModel extends AndroidViewModel {
|
||||
private boolean isFeedback;
|
||||
|
||||
@Inject
|
||||
public ReportViewModel(@NonNull Application application,
|
||||
DevReporter reporter, PluginManager pluginManager) {
|
||||
ReportViewModel(@NonNull Application application,
|
||||
CachingLogHandler logHandler,
|
||||
LogDecrypter logDecrypter,
|
||||
DevReporter reporter,
|
||||
PluginManager pluginManager) {
|
||||
super(application);
|
||||
this.collector = new BriarReportCollector(application);
|
||||
collector = new BriarReportCollector(application);
|
||||
this.logHandler = logHandler;
|
||||
this.logDecrypter = logDecrypter;
|
||||
this.reporter = reporter;
|
||||
this.pluginManager = pluginManager;
|
||||
}
|
||||
|
||||
void init(@Nullable Throwable t, long appStartTime) {
|
||||
void init(@Nullable Throwable t, long appStartTime,
|
||||
@Nullable byte[] logKey) {
|
||||
isFeedback = t == null;
|
||||
if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> {
|
||||
ReportData data = collector.collectReportData(t, appStartTime);
|
||||
String decryptedLogs;
|
||||
if (isFeedback) {
|
||||
Formatter formatter = new BriefLogFormatter();
|
||||
decryptedLogs =
|
||||
formatLog(formatter, logHandler.getRecentLogRecords());
|
||||
} else {
|
||||
decryptedLogs = logDecrypter.decryptLogs(logKey);
|
||||
if (decryptedLogs == null) {
|
||||
// error decrypting logs, get logs from this process
|
||||
Formatter formatter = new BriefLogFormatter();
|
||||
decryptedLogs = formatLog(formatter,
|
||||
logHandler.getRecentLogRecords());
|
||||
}
|
||||
}
|
||||
ReportData data =
|
||||
collector.collectReportData(t, appStartTime, decryptedLogs);
|
||||
reportData.postValue(data);
|
||||
}).start();
|
||||
}
|
||||
@@ -110,8 +138,8 @@ public class ReportViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* The content of the report
|
||||
* that will be loaded after {@link #init(Throwable, long)} was called.
|
||||
* The content of the report that will be loaded after
|
||||
* {@link #init(Throwable, long, byte[])} was called.
|
||||
*/
|
||||
LiveData<ReportData> getReportData() {
|
||||
return reportData;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.briarproject.briar.android.sharing;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface SharingController {
|
||||
|
||||
/**
|
||||
* Call this when the owning ViewModel gets cleared,
|
||||
* so the {@link EventBus} can get unregistered.
|
||||
*/
|
||||
void onCleared();
|
||||
|
||||
/**
|
||||
* 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 total number of contacts that have been added.
|
||||
*/
|
||||
LiveData<SharingInfo> getSharingInfo();
|
||||
|
||||
class SharingInfo {
|
||||
public final int total, online;
|
||||
|
||||
SharingInfo(int total, int online) {
|
||||
this.total = total;
|
||||
this.online = online;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package org.briarproject.briar.android.sharing;
|
||||
|
||||
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.inject.Inject;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
@NotNullByDefault
|
||||
public class SharingControllerImpl implements SharingController, EventListener {
|
||||
|
||||
private final EventBus eventBus;
|
||||
private final ConnectionRegistry connectionRegistry;
|
||||
|
||||
// UI thread
|
||||
private final Set<ContactId> contacts = new HashSet<>();
|
||||
private final MutableLiveData<SharingInfo> sharingInfo =
|
||||
new MutableLiveData<>();
|
||||
|
||||
@Inject
|
||||
SharingControllerImpl(EventBus eventBus,
|
||||
ConnectionRegistry connectionRegistry) {
|
||||
this.eventBus = eventBus;
|
||||
this.connectionRegistry = connectionRegistry;
|
||||
eventBus.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCleared() {
|
||||
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 (contacts.contains(c)) {
|
||||
updateLiveData();
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void updateLiveData() {
|
||||
int online = getOnlineCount();
|
||||
sharingInfo.setValue(new SharingInfo(contacts.size(), online));
|
||||
}
|
||||
|
||||
private int getOnlineCount() {
|
||||
int online = 0;
|
||||
for (ContactId c : contacts) {
|
||||
if (connectionRegistry.isConnected(c)) online++;
|
||||
}
|
||||
return online;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void addAll(Collection<ContactId> c) {
|
||||
contacts.addAll(c);
|
||||
updateLiveData();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void add(ContactId c) {
|
||||
contacts.add(c);
|
||||
updateLiveData();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Override
|
||||
public void remove(ContactId c) {
|
||||
contacts.remove(c);
|
||||
updateLiveData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<SharingInfo> getSharingInfo() {
|
||||
return sharingInfo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,36 +9,47 @@ import dagger.Provides;
|
||||
@Module
|
||||
public class SharingModule {
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
ShareForumController provideShareForumController(
|
||||
ShareForumControllerImpl shareForumController) {
|
||||
return shareForumController;
|
||||
@Module
|
||||
@Deprecated
|
||||
public static class SharingLegacyModule {
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
ShareForumController provideShareForumController(
|
||||
ShareForumControllerImpl shareForumController) {
|
||||
return shareForumController;
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
BlogInvitationController provideInvitationBlogController(
|
||||
BaseActivity activity,
|
||||
BlogInvitationControllerImpl blogInvitationController) {
|
||||
activity.addLifecycleController(blogInvitationController);
|
||||
return blogInvitationController;
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
ForumInvitationController provideInvitationForumController(
|
||||
BaseActivity activity,
|
||||
ForumInvitationControllerImpl forumInvitationController) {
|
||||
activity.addLifecycleController(forumInvitationController);
|
||||
return forumInvitationController;
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
ShareBlogController provideShareBlogController(
|
||||
ShareBlogControllerImpl shareBlogController) {
|
||||
return shareBlogController;
|
||||
}
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
BlogInvitationController provideInvitationBlogController(
|
||||
BaseActivity activity,
|
||||
BlogInvitationControllerImpl blogInvitationController) {
|
||||
activity.addLifecycleController(blogInvitationController);
|
||||
return blogInvitationController;
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
ForumInvitationController provideInvitationForumController(
|
||||
BaseActivity activity,
|
||||
ForumInvitationControllerImpl forumInvitationController) {
|
||||
activity.addLifecycleController(forumInvitationController);
|
||||
return forumInvitationController;
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
ShareBlogController provideShareBlogController(
|
||||
ShareBlogControllerImpl shareBlogController) {
|
||||
return shareBlogController;
|
||||
SharingController provideSharingController(
|
||||
SharingControllerImpl sharingController) {
|
||||
return sharingController;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.briar.android.splash;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -7,6 +8,7 @@ import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.Localizer;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
@@ -27,6 +29,13 @@ public class ExpiredActivity extends AppCompatActivity
|
||||
findViewById(R.id.download_briar_button).setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(
|
||||
Localizer.getInstance().setLocale(base));
|
||||
Localizer.getInstance().setLocale(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Uri uri = Uri.parse("https://briarproject.org/download.html");
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package org.briarproject.briar.android.threaded;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.api.client.MessageTree;
|
||||
import org.briarproject.briar.api.client.MessageTree.MessageNode;
|
||||
import org.briarproject.briar.client.MessageTreeImpl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
public class NestedTreeList<T extends MessageNode> implements Iterable<T> {
|
||||
|
||||
private final MessageTree<T> tree = new MessageTreeImpl<>();
|
||||
private List<T> depthFirstCollection = new ArrayList<>();
|
||||
|
||||
public void addAll(Collection<T> collection) {
|
||||
tree.add(collection);
|
||||
depthFirstCollection = new ArrayList<>(tree.depthFirstOrder());
|
||||
}
|
||||
|
||||
public void add(T elem) {
|
||||
tree.add(elem);
|
||||
depthFirstCollection = new ArrayList<>(tree.depthFirstOrder());
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
tree.clear();
|
||||
depthFirstCollection.clear();
|
||||
}
|
||||
|
||||
public T get(int index) {
|
||||
return depthFirstCollection.get(index);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return depthFirstCollection.size();
|
||||
}
|
||||
|
||||
public boolean contains(MessageId m) {
|
||||
return tree.contains(m);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return depthFirstCollection.iterator();
|
||||
}
|
||||
}
|
||||
@@ -99,4 +99,14 @@ public abstract class ThreadItem implements MessageNode {
|
||||
return highlighted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return messageId.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
return o instanceof ThreadItem &&
|
||||
messageId.equals(((ThreadItem) o).messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,39 +4,45 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.util.ItemReturningAdapter;
|
||||
import org.briarproject.briar.android.util.VersionedAdapter;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
|
||||
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
|
||||
|
||||
@UiThread
|
||||
@NotNullByDefault
|
||||
public class ThreadItemAdapter<I extends ThreadItem>
|
||||
extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
|
||||
implements VersionedAdapter, ItemReturningAdapter<I> {
|
||||
extends ListAdapter<I, BaseThreadItemViewHolder<I>>
|
||||
implements ItemReturningAdapter<I> {
|
||||
|
||||
static final int UNDEFINED = -1;
|
||||
|
||||
protected final NestedTreeList<I> items = new NestedTreeList<>();
|
||||
private final ThreadItemListener<I> listener;
|
||||
private final LinearLayoutManager layoutManager;
|
||||
|
||||
private volatile int revision = 0;
|
||||
public ThreadItemAdapter(ThreadItemListener<I> listener) {
|
||||
super(new DiffUtil.ItemCallback<I>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(I a, I b) {
|
||||
return a.equals(b);
|
||||
}
|
||||
|
||||
public ThreadItemAdapter(ThreadItemListener<I> listener,
|
||||
LinearLayoutManager layoutManager) {
|
||||
@Override
|
||||
public boolean areContentsTheSame(I a, I b) {
|
||||
return a.isHighlighted() == b.isHighlighted() &&
|
||||
a.isRead() == b.isRead();
|
||||
}
|
||||
});
|
||||
this.listener = listener;
|
||||
this.layoutManager = layoutManager;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -51,76 +57,27 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull BaseThreadItemViewHolder<I> ui,
|
||||
int position) {
|
||||
I item = items.get(position);
|
||||
I item = getItem(position);
|
||||
ui.bind(item, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incrementRevision() {
|
||||
revision++;
|
||||
}
|
||||
|
||||
void setItemWithIdVisible(MessageId messageId) {
|
||||
int pos = 0;
|
||||
for (I item : items) {
|
||||
if (item.getId().equals(messageId)) {
|
||||
layoutManager.scrollToPosition(pos);
|
||||
break;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
public void setItems(Collection<I> items) {
|
||||
this.items.clear();
|
||||
this.items.addAll(items);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void add(I item) {
|
||||
items.add(item);
|
||||
notifyItemInserted(findItemPosition(item));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public I getItemAt(int position) {
|
||||
if (position == NO_POSITION || position >= items.size()) {
|
||||
return null;
|
||||
}
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
protected int findItemPosition(@Nullable I item) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (items.get(i).equals(item)) return i;
|
||||
int findItemPosition(MessageId id) {
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
if (id.equals(getItem(i).getId())) return i;
|
||||
}
|
||||
return NO_POSITION; // Not found
|
||||
}
|
||||
|
||||
boolean contains(MessageId m) {
|
||||
return items.contains(m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights the item with the given {@link MessageId}
|
||||
* and disables the highlight for a previously highlighted item, if any.
|
||||
*
|
||||
* <p>
|
||||
* Only one item can be highlighted at a time.
|
||||
*/
|
||||
void setHighlightedItem(@Nullable MessageId id) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
I item = items.get(i);
|
||||
if (id != null && item.getId().equals(id)) {
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
I item = getItem(i);
|
||||
if (item.getId().equals(id)) {
|
||||
item.setHighlighted(true);
|
||||
notifyItemChanged(i, item);
|
||||
} else if (item.isHighlighted()) {
|
||||
@@ -132,20 +89,27 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
||||
|
||||
@Nullable
|
||||
I getHighlightedItem() {
|
||||
for (I i : items) {
|
||||
if (i.isHighlighted()) return i;
|
||||
for (I item : getCurrentList()) {
|
||||
if (item.isHighlighted()) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MessageId getFirstVisibleMessageId(LinearLayoutManager layoutManager) {
|
||||
int position = layoutManager.findFirstVisibleItemPosition();
|
||||
if (position == NO_POSITION) return null;
|
||||
return getItemAt(position).getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of the first unread item below the current viewport
|
||||
*/
|
||||
int getVisibleUnreadPosBottom() {
|
||||
int getVisibleUnreadPosBottom(LinearLayoutManager layoutManager) {
|
||||
int positionBottom = layoutManager.findLastVisibleItemPosition();
|
||||
if (positionBottom == NO_POSITION) return NO_POSITION;
|
||||
for (int i = positionBottom + 1; i < items.size(); i++) {
|
||||
if (!items.get(i).isRead()) return i;
|
||||
for (int i = positionBottom + 1; i < getItemCount(); i++) {
|
||||
if (!getItem(i).isRead()) return i;
|
||||
}
|
||||
return NO_POSITION;
|
||||
}
|
||||
@@ -153,11 +117,11 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
||||
/**
|
||||
* Returns the position of the first unread item above the current viewport
|
||||
*/
|
||||
int getVisibleUnreadPosTop() {
|
||||
int getVisibleUnreadPosTop(LinearLayoutManager layoutManager) {
|
||||
int positionTop = layoutManager.findFirstVisibleItemPosition();
|
||||
int position = NO_POSITION;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
if (i < positionTop && !items.get(i).isRead()) {
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
if (i < positionTop && !getItem(i).isRead()) {
|
||||
position = i;
|
||||
} else if (i >= positionTop) {
|
||||
return position;
|
||||
@@ -166,6 +130,11 @@ public class ThreadItemAdapter<I extends ThreadItem>
|
||||
return NO_POSITION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public I getItemAt(int position) {
|
||||
return getItem(position);
|
||||
}
|
||||
|
||||
public interface ThreadItemListener<I> {
|
||||
void onReplyClick(I item);
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package org.briarproject.briar.android.threaded;
|
||||
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public interface ThreadItemList<I extends ThreadItem> extends List<I> {
|
||||
|
||||
@Nullable
|
||||
MessageId getFirstVisibleItemId();
|
||||
|
||||
void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package org.briarproject.briar.android.threaded;
|
||||
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ThreadItemListImpl<I extends ThreadItem> extends ArrayList<I>
|
||||
implements ThreadItemList<I> {
|
||||
|
||||
private MessageId bottomVisibleItemId;
|
||||
|
||||
@Override
|
||||
public MessageId getFirstVisibleItemId() {
|
||||
return bottomVisibleItemId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFirstVisibleId(@Nullable MessageId bottomVisibleItemId) {
|
||||
this.bottomVisibleItemId = bottomVisibleItemId;
|
||||
}
|
||||
}
|
||||
@@ -2,25 +2,18 @@ package org.briarproject.briar.android.threaded;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
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.R;
|
||||
import org.briarproject.briar.android.activity.BriarActivity;
|
||||
import org.briarproject.briar.android.controller.SharingController;
|
||||
import org.briarproject.briar.android.controller.SharingController.SharingListener;
|
||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.briar.android.sharing.SharingController.SharingInfo;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDataSource;
|
||||
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
|
||||
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
|
||||
import org.briarproject.briar.android.view.BriarRecyclerView;
|
||||
import org.briarproject.briar.android.view.TextInputView;
|
||||
@@ -28,18 +21,13 @@ import org.briarproject.briar.android.view.TextSendController;
|
||||
import org.briarproject.briar.android.view.TextSendController.SendListener;
|
||||
import org.briarproject.briar.android.view.UnreadMessageButton;
|
||||
import org.briarproject.briar.api.attachment.AttachmentHeader;
|
||||
import org.briarproject.briar.api.client.NamedGroup;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
@@ -48,32 +36,19 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, A extends ThreadItemAdapter<I>>
|
||||
extends BriarActivity
|
||||
implements ThreadListListener<I>, SendListener, SharingListener,
|
||||
ThreadItemListener<I>, ThreadListDataSource {
|
||||
public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadItemAdapter<I>>
|
||||
extends BriarActivity implements SendListener, ThreadItemListener<I> {
|
||||
|
||||
protected static final String KEY_REPLY_ID = "replyId";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ThreadListActivity.class.getName());
|
||||
|
||||
protected A adapter;
|
||||
private ThreadScrollListener<I> scrollListener;
|
||||
protected final A adapter = createAdapter();
|
||||
protected abstract ThreadListViewModel<I> getViewModel();
|
||||
protected abstract A createAdapter();
|
||||
protected BriarRecyclerView list;
|
||||
private LinearLayoutManager layoutManager;
|
||||
protected TextInputView textInput;
|
||||
protected TextSendController sendController;
|
||||
protected GroupId groupId;
|
||||
@Nullable
|
||||
private Parcelable layoutManagerState;
|
||||
@Nullable
|
||||
private MessageId replyId;
|
||||
|
||||
protected abstract ThreadListController<G, I> getController();
|
||||
|
||||
@Inject
|
||||
protected SharingController sharingController;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private ThreadScrollListener<I> scrollListener;
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
@@ -86,7 +61,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException("No GroupId in intent.");
|
||||
groupId = new GroupId(b);
|
||||
getController().setGroupId(groupId);
|
||||
ThreadListViewModel<I> viewModel = getViewModel();
|
||||
viewModel.setGroupId(groupId);
|
||||
|
||||
textInput = findViewById(R.id.text_input_container);
|
||||
sendController = new TextSendController(textInput, this, false);
|
||||
@@ -100,131 +76,41 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
list = findViewById(R.id.list);
|
||||
layoutManager = new LinearLayoutManager(this);
|
||||
list.setLayoutManager(layoutManager);
|
||||
adapter = createAdapter(layoutManager);
|
||||
list.setAdapter(adapter);
|
||||
scrollListener = new ThreadScrollListener<>(adapter, getController(),
|
||||
scrollListener = new ThreadScrollListener<>(adapter, viewModel,
|
||||
upButton, downButton);
|
||||
list.getRecyclerView().addOnScrollListener(scrollListener);
|
||||
|
||||
upButton.setOnClickListener(v -> {
|
||||
int position = adapter.getVisibleUnreadPosTop();
|
||||
int position = adapter.getVisibleUnreadPosTop(layoutManager);
|
||||
if (position != NO_POSITION) {
|
||||
list.getRecyclerView().scrollToPosition(position);
|
||||
}
|
||||
});
|
||||
downButton.setOnClickListener(v -> {
|
||||
int position = adapter.getVisibleUnreadPosBottom();
|
||||
int position = adapter.getVisibleUnreadPosBottom(layoutManager);
|
||||
if (position != NO_POSITION) {
|
||||
list.getRecyclerView().scrollToPosition(position);
|
||||
}
|
||||
});
|
||||
|
||||
if (state != null) {
|
||||
byte[] replyIdBytes = state.getByteArray(KEY_REPLY_ID);
|
||||
if (replyIdBytes != null) replyId = new MessageId(replyIdBytes);
|
||||
}
|
||||
viewModel.getItems().observe(this, result -> result
|
||||
.onError(this::handleException)
|
||||
.onSuccess(this::displayItems)
|
||||
);
|
||||
|
||||
sharingController.setSharingListener(this);
|
||||
loadSharingContacts();
|
||||
}
|
||||
viewModel.getSharingInfo().observe(this, this::setToolbarSubTitle);
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MessageId getFirstVisibleMessageId() {
|
||||
if (layoutManager != null && adapter != null) {
|
||||
int position =
|
||||
layoutManager.findFirstVisibleItemPosition();
|
||||
I i = adapter.getItemAt(position);
|
||||
return i == null ? null : i.getId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract A createAdapter(LinearLayoutManager layoutManager);
|
||||
|
||||
protected void loadNamedGroup() {
|
||||
getController().loadNamedGroup(
|
||||
new UiResultExceptionHandler<G, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(G groupItem) {
|
||||
onNamedGroupLoaded(groupItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@UiThread
|
||||
protected abstract void onNamedGroupLoaded(G groupItem);
|
||||
|
||||
protected void loadItems() {
|
||||
int revision = adapter.getRevision();
|
||||
getController().loadItems(
|
||||
new UiResultExceptionHandler<ThreadItemList<I>, DbException>(
|
||||
this) {
|
||||
@Override
|
||||
public void onResultUi(ThreadItemList<I> items) {
|
||||
if (revision == adapter.getRevision()) {
|
||||
adapter.incrementRevision();
|
||||
if (items.isEmpty()) {
|
||||
list.showData();
|
||||
} else {
|
||||
displayItems(items);
|
||||
updateTextInput();
|
||||
}
|
||||
} else {
|
||||
LOG.info("Concurrent update, reloading");
|
||||
loadItems();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayItems(ThreadItemList<I> items) {
|
||||
adapter.setItems(items);
|
||||
MessageId messageId = items.getFirstVisibleItemId();
|
||||
if (messageId != null)
|
||||
adapter.setItemWithIdVisible(messageId);
|
||||
list.showData();
|
||||
if (layoutManagerState == null) {
|
||||
list.scrollToPosition(0); // Scroll to the top
|
||||
} else {
|
||||
layoutManager.onRestoreInstanceState(layoutManagerState);
|
||||
}
|
||||
}
|
||||
|
||||
protected void loadSharingContacts() {
|
||||
getController().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);
|
||||
}
|
||||
});
|
||||
viewModel.getGroupRemoved().observe(this, removed -> {
|
||||
if (removed) supportFinishAfterTransition();
|
||||
});
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
sharingController.onStart();
|
||||
loadItems();
|
||||
getViewModel().blockAndClearNotifications();
|
||||
list.startPeriodicUpdate();
|
||||
}
|
||||
|
||||
@@ -232,91 +118,98 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
sharingController.onStop();
|
||||
getViewModel().unblockNotifications();
|
||||
list.stopPeriodicUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (layoutManager != null) {
|
||||
layoutManagerState = layoutManager.onSaveInstanceState();
|
||||
outState.putParcelable("layoutManager", layoutManagerState);
|
||||
}
|
||||
if (replyId != null) {
|
||||
outState.putByteArray(KEY_REPLY_ID, replyId.getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
layoutManagerState = savedInstanceState.getParcelable("layoutManager");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
supportFinishAfterTransition();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
supportFinishAfterTransition();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (adapter.getHighlightedItem() != null) {
|
||||
textInput.clearText();
|
||||
replyId = null;
|
||||
getViewModel().setReplyId(null);
|
||||
updateTextInput();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// store list position, so we can restore it when coming back here
|
||||
if (layoutManager != null && adapter != null) {
|
||||
MessageId id = adapter.getFirstVisibleMessageId(layoutManager);
|
||||
getViewModel().storeMessageId(id);
|
||||
}
|
||||
}
|
||||
|
||||
protected void displayItems(List<I> items) {
|
||||
if (items.isEmpty()) {
|
||||
list.showData();
|
||||
} else {
|
||||
adapter.submitList(items, () -> {
|
||||
// do stuff *after* list had been updated
|
||||
scrollAfterListCommitted();
|
||||
updateTextInput();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to the first visible item last time the activity was open,
|
||||
* if one exists and this is the first time, the list gets displayed.
|
||||
* Or scrolls to a locally added item that has just been added to the list.
|
||||
*/
|
||||
private void scrollAfterListCommitted() {
|
||||
MessageId restoredFirstVisibleItemId =
|
||||
getViewModel().getAndResetRestoredMessageId();
|
||||
MessageId scrollToItem =
|
||||
getViewModel().getAndResetScrollToItem();
|
||||
if (restoredFirstVisibleItemId != null) {
|
||||
scrollToItemAtTop(restoredFirstVisibleItemId);
|
||||
} else if (scrollToItem != null) {
|
||||
scrollToItemAtTop(scrollToItem);
|
||||
}
|
||||
scrollListener.updateUnreadButtons(layoutManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReplyClick(I item) {
|
||||
replyId = item.getId();
|
||||
getViewModel().setReplyId(item.getId());
|
||||
updateTextInput();
|
||||
// FIXME This does not work for a hardware keyboard
|
||||
if (textInput.isKeyboardOpen()) {
|
||||
scrollToItemAtTop(item);
|
||||
scrollToItemAtTop(item.getId());
|
||||
} else {
|
||||
// wait with scrolling until keyboard opened
|
||||
textInput.setOnKeyboardShownListener(() -> {
|
||||
scrollToItemAtTop(item);
|
||||
scrollToItemAtTop(item.getId());
|
||||
textInput.setOnKeyboardShownListener(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharingInfoUpdated(int total, int online) {
|
||||
setToolbarSubTitle(total, online);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvitationAccepted(ContactId c) {
|
||||
sharingController.add(c);
|
||||
setToolbarSubTitle(sharingController.getTotalCount(),
|
||||
sharingController.getOnlineCount());
|
||||
}
|
||||
|
||||
protected void setToolbarSubTitle(int total, int online) {
|
||||
protected void setToolbarSubTitle(SharingInfo sharingInfo) {
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setSubtitle(
|
||||
getString(R.string.shared_with, total, online));
|
||||
actionBar.setSubtitle(getString(R.string.shared_with,
|
||||
sharingInfo.total, sharingInfo.online));
|
||||
}
|
||||
}
|
||||
|
||||
private void scrollToItemAtTop(I item) {
|
||||
int position = adapter.findItemPosition(item);
|
||||
private void scrollToItemAtTop(MessageId messageId) {
|
||||
int position = adapter.findItemPosition(messageId);
|
||||
if (position != NO_POSITION) {
|
||||
layoutManager
|
||||
.scrollToPositionWithOffset(position, 0);
|
||||
layoutManager.scrollToPositionWithOffset(position, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +220,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
}
|
||||
|
||||
private void updateTextInput() {
|
||||
MessageId replyId = getViewModel().getReplyId();
|
||||
if (replyId != null) {
|
||||
textInput.setHint(R.string.forum_message_reply_hint);
|
||||
textInput.showSoftKeyboard();
|
||||
@@ -341,54 +235,14 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
||||
List<AttachmentHeader> headers) {
|
||||
if (isNullOrEmpty(text)) throw new AssertionError();
|
||||
|
||||
I replyItem = adapter.getHighlightedItem();
|
||||
UiResultExceptionHandler<I, DbException> handler =
|
||||
new UiResultExceptionHandler<I, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(I result) {
|
||||
addItem(result, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExceptionUi(DbException exception) {
|
||||
handleException(exception);
|
||||
}
|
||||
};
|
||||
getController().createAndStoreMessage(text, replyItem, handler);
|
||||
MessageId replyId = getViewModel().getReplyId();
|
||||
getViewModel().createAndStoreMessage(text, replyId);
|
||||
textInput.hideSoftKeyboard();
|
||||
textInput.clearText();
|
||||
replyId = null;
|
||||
getViewModel().setReplyId(null);
|
||||
updateTextInput();
|
||||
}
|
||||
|
||||
protected abstract int getMaxTextLength();
|
||||
|
||||
@Override
|
||||
public void onItemReceived(I item) {
|
||||
addItem(item, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGroupRemoved() {
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
private void addItem(I item, boolean isLocal) {
|
||||
adapter.incrementRevision();
|
||||
MessageId parent = item.getParentId();
|
||||
if (parent != null && !adapter.contains(parent)) {
|
||||
// We've incremented the adapter's revision, so the item will be
|
||||
// loaded when its parent has been loaded
|
||||
LOG.info("Ignoring item with missing parent");
|
||||
return;
|
||||
}
|
||||
adapter.add(item);
|
||||
|
||||
if (isLocal) {
|
||||
scrollToItemAtTop(item);
|
||||
} else {
|
||||
scrollListener.updateUnreadButtons(layoutManager);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package org.briarproject.briar.android.threaded;
|
||||
|
||||
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.ActivityLifecycleController;
|
||||
import org.briarproject.briar.android.controller.handler.ExceptionHandler;
|
||||
import org.briarproject.briar.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.briar.api.client.NamedGroup;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface ThreadListController<G extends NamedGroup, I extends ThreadItem>
|
||||
extends ActivityLifecycleController {
|
||||
|
||||
void setGroupId(GroupId groupId);
|
||||
|
||||
void loadNamedGroup(ResultExceptionHandler<G, DbException> handler);
|
||||
|
||||
void loadSharingContacts(
|
||||
ResultExceptionHandler<Collection<ContactId>, DbException> handler);
|
||||
|
||||
void loadItems(
|
||||
ResultExceptionHandler<ThreadItemList<I>, DbException> handler);
|
||||
|
||||
void markItemRead(I item);
|
||||
|
||||
void markItemsRead(Collection<I> items);
|
||||
|
||||
void createAndStoreMessage(String text, @Nullable I parentItem,
|
||||
ResultExceptionHandler<I, DbException> handler);
|
||||
|
||||
void deleteNamedGroup(ExceptionHandler<DbException> handler);
|
||||
|
||||
interface ThreadListListener<I> extends ThreadListDataSource {
|
||||
|
||||
@UiThread
|
||||
void onItemReceived(I item);
|
||||
|
||||
@UiThread
|
||||
void onGroupRemoved();
|
||||
|
||||
@UiThread
|
||||
void onInvitationAccepted(ContactId c);
|
||||
}
|
||||
|
||||
interface ThreadListDataSource {
|
||||
|
||||
@UiThread @Nullable
|
||||
MessageId getFirstVisibleMessageId();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
package org.briarproject.briar.android.threaded;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
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.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.GroupId;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
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.android.threaded.ThreadListController.ThreadListListener;
|
||||
import org.briarproject.briar.api.android.AndroidNotificationManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.NamedGroup;
|
||||
import org.briarproject.briar.api.client.PostHeader;
|
||||
import org.briarproject.briar.api.client.ThreadedMessage;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
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;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends ThreadItem, H extends PostHeader, M extends ThreadedMessage, L extends ThreadListListener<I>>
|
||||
extends DbControllerImpl
|
||||
implements ThreadListController<G, I>, EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ThreadListControllerImpl.class.getName());
|
||||
|
||||
private final EventBus eventBus;
|
||||
private final MessageTracker messageTracker;
|
||||
private final Map<MessageId, String> textCache = new ConcurrentHashMap<>();
|
||||
|
||||
private volatile GroupId groupId;
|
||||
|
||||
protected final IdentityManager identityManager;
|
||||
protected final AndroidNotificationManager notificationManager;
|
||||
protected final Executor cryptoExecutor;
|
||||
protected final Clock clock;
|
||||
|
||||
// UI thread
|
||||
protected L listener;
|
||||
|
||||
protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||
@CryptoExecutor Executor cryptoExecutor, EventBus eventBus,
|
||||
Clock clock, AndroidNotificationManager notificationManager,
|
||||
MessageTracker messageTracker) {
|
||||
super(dbExecutor, lifecycleManager);
|
||||
this.identityManager = identityManager;
|
||||
this.cryptoExecutor = cryptoExecutor;
|
||||
this.notificationManager = notificationManager;
|
||||
this.clock = clock;
|
||||
this.eventBus = eventBus;
|
||||
this.messageTracker = messageTracker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGroupId(GroupId groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onActivityCreate(Activity activity) {
|
||||
listener = (L) activity;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onActivityStart() {
|
||||
notificationManager.blockNotification(getGroupId());
|
||||
eventBus.addListener(this);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onActivityStop() {
|
||||
notificationManager.unblockNotification(getGroupId());
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroy() {
|
||||
MessageId messageId = listener.getFirstVisibleMessageId();
|
||||
if (messageId != null) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
messageTracker.storeMessageId(groupId, messageId);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent s = (GroupRemovedEvent) e;
|
||||
if (s.getGroup().getId().equals(getGroupId())) {
|
||||
LOG.info("Group removed");
|
||||
listener.onGroupRemoved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadNamedGroup(
|
||||
ResultExceptionHandler<G, DbException> handler) {
|
||||
checkGroupId();
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
G groupItem = loadNamedGroup();
|
||||
logDuration(LOG, "Loading group", start);
|
||||
handler.onResult(groupItem);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
protected abstract G loadNamedGroup() throws DbException;
|
||||
|
||||
@Override
|
||||
public void loadItems(
|
||||
ResultExceptionHandler<ThreadItemList<I>, DbException> handler) {
|
||||
checkGroupId();
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
// Load headers
|
||||
long start = now();
|
||||
Collection<H> headers = loadHeaders();
|
||||
logDuration(LOG, "Loading headers", start);
|
||||
|
||||
// Load bodies into cache
|
||||
start = now();
|
||||
for (H header : headers) {
|
||||
if (!textCache.containsKey(header.getId())) {
|
||||
textCache.put(header.getId(),
|
||||
loadMessageText(header));
|
||||
}
|
||||
}
|
||||
logDuration(LOG, "Loading bodies", start);
|
||||
|
||||
// Build and hand over items
|
||||
handler.onResult(buildItems(headers));
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
protected abstract Collection<H> loadHeaders() throws DbException;
|
||||
|
||||
@DatabaseExecutor
|
||||
protected abstract String loadMessageText(H header) throws DbException;
|
||||
|
||||
@Override
|
||||
public void markItemRead(I item) {
|
||||
markItemsRead(Collections.singletonList(item));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markItemsRead(Collection<I> items) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
for (I i : items) {
|
||||
markRead(i.getId());
|
||||
}
|
||||
logDuration(LOG, "Marking read", start);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
protected abstract void markRead(MessageId id) throws DbException;
|
||||
|
||||
protected void storePost(M msg, String text,
|
||||
ResultExceptionHandler<I, DbException> resultHandler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
H header = addLocalMessage(msg);
|
||||
textCache.put(msg.getMessage().getId(), text);
|
||||
logDuration(LOG, "Storing message", start);
|
||||
resultHandler.onResult(buildItem(header, text));
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
resultHandler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
protected abstract H addLocalMessage(M message) throws DbException;
|
||||
|
||||
@Override
|
||||
public void deleteNamedGroup(ExceptionHandler<DbException> handler) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
G groupItem = loadNamedGroup();
|
||||
deleteNamedGroup(groupItem);
|
||||
logDuration(LOG, "Removing group", start);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
handler.onException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@DatabaseExecutor
|
||||
protected abstract void deleteNamedGroup(G groupItem) throws DbException;
|
||||
|
||||
private ThreadItemList<I> buildItems(Collection<H> headers)
|
||||
throws DbException {
|
||||
ThreadItemList<I> items = new ThreadItemListImpl<>();
|
||||
for (H h : headers) {
|
||||
items.add(buildItem(h, textCache.get(h.getId())));
|
||||
}
|
||||
MessageId msgId = messageTracker.loadStoredMessageId(groupId);
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loaded last top visible message id " + msgId);
|
||||
items.setFirstVisibleId(msgId);
|
||||
return items;
|
||||
}
|
||||
|
||||
protected abstract I buildItem(H header, String text);
|
||||
|
||||
protected GroupId getGroupId() {
|
||||
checkGroupId();
|
||||
return groupId;
|
||||
}
|
||||
|
||||
private void checkGroupId() {
|
||||
if (groupId == null) throw new IllegalStateException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package org.briarproject.briar.android.threaded;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
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.EventBus;
|
||||
import org.briarproject.bramble.api.event.EventListener;
|
||||
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.GroupId;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.event.GroupRemovedEvent;
|
||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.briar.android.sharing.SharingController;
|
||||
import org.briarproject.briar.android.sharing.SharingController.SharingInfo;
|
||||
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.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.MessageTree;
|
||||
import org.briarproject.briar.client.MessageTreeImpl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
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.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public abstract class ThreadListViewModel<I extends ThreadItem>
|
||||
extends DbViewModel implements EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
getLogger(ThreadListViewModel.class.getName());
|
||||
|
||||
protected final IdentityManager identityManager;
|
||||
protected final AndroidNotificationManager notificationManager;
|
||||
protected final SharingController sharingController;
|
||||
protected final Executor cryptoExecutor;
|
||||
protected final Clock clock;
|
||||
private final MessageTracker messageTracker;
|
||||
private final EventBus eventBus;
|
||||
|
||||
// UIThread
|
||||
private final MessageTree<I> messageTree = new MessageTreeImpl<>();
|
||||
private final MutableLiveData<LiveResult<List<I>>> items =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> groupRemoved =
|
||||
new MutableLiveData<>();
|
||||
private final AtomicReference<MessageId> scrollToItem =
|
||||
new AtomicReference<>();
|
||||
|
||||
protected volatile GroupId groupId;
|
||||
@Nullable
|
||||
private MessageId replyId;
|
||||
/**
|
||||
* Stored list position. Needs to be loaded and set before the list itself.
|
||||
*/
|
||||
private final AtomicReference<MessageId> storedMessageId =
|
||||
new AtomicReference<>();
|
||||
|
||||
public ThreadListViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
LifecycleManager lifecycleManager,
|
||||
TransactionManager db,
|
||||
AndroidExecutor androidExecutor,
|
||||
IdentityManager identityManager,
|
||||
AndroidNotificationManager notificationManager,
|
||||
SharingController sharingController,
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
Clock clock,
|
||||
MessageTracker messageTracker,
|
||||
EventBus eventBus) {
|
||||
super(application, dbExecutor, lifecycleManager, db, androidExecutor);
|
||||
this.identityManager = identityManager;
|
||||
this.notificationManager = notificationManager;
|
||||
this.cryptoExecutor = cryptoExecutor;
|
||||
this.clock = clock;
|
||||
this.sharingController = sharingController;
|
||||
this.messageTracker = messageTracker;
|
||||
this.eventBus = eventBus;
|
||||
this.eventBus.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
eventBus.removeListener(this);
|
||||
sharingController.onCleared();
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be called right after initialization,
|
||||
* before calling any other methods.
|
||||
*/
|
||||
public final void setGroupId(GroupId groupId) {
|
||||
boolean needsInitialLoad = this.groupId == null;
|
||||
this.groupId = groupId;
|
||||
if (needsInitialLoad) performInitialLoad();
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected void performInitialLoad() {
|
||||
// load stored MessageId (last list position) before the list itself
|
||||
loadStoredMessageId();
|
||||
loadItems();
|
||||
loadSharingContacts();
|
||||
}
|
||||
|
||||
protected abstract void clearNotifications();
|
||||
|
||||
void blockAndClearNotifications() {
|
||||
notificationManager.blockNotification(groupId);
|
||||
clearNotifications();
|
||||
}
|
||||
|
||||
void unblockNotifications() {
|
||||
notificationManager.unblockNotification(groupId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent s = (GroupRemovedEvent) e;
|
||||
if (s.getGroup().getId().equals(groupId)) {
|
||||
LOG.info("Group removed");
|
||||
groupRemoved.setValue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadStoredMessageId() {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
storedMessageId
|
||||
.set(messageTracker.loadStoredMessageId(groupId));
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Loaded last top visible message id " +
|
||||
storedMessageId);
|
||||
}
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public abstract void loadItems();
|
||||
|
||||
public abstract void createAndStoreMessage(String text,
|
||||
@Nullable MessageId parentMessageId);
|
||||
|
||||
/**
|
||||
* Loads the ContactIds of all contacts the group is shared with
|
||||
* and adds them to {@link SharingController}.
|
||||
*/
|
||||
protected abstract void loadSharingContacts();
|
||||
|
||||
@UiThread
|
||||
protected void setItems(LiveResult<List<I>> items) {
|
||||
if (items.hasError()) {
|
||||
this.items.setValue(items);
|
||||
} else {
|
||||
messageTree.clear();
|
||||
// not null, because hasError() is false
|
||||
messageTree.add(requireNonNull(items.getResultOrNull()));
|
||||
LiveResult<List<I>> result =
|
||||
new LiveResult<>(messageTree.depthFirstOrder());
|
||||
this.items.setValue(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a remote item on the UI thread.
|
||||
*
|
||||
* @param scrollToItem whether the list will scroll to the newly added item
|
||||
*/
|
||||
@UiThread
|
||||
protected void addItem(I item, boolean scrollToItem) {
|
||||
// If items haven't loaded, we need to wait until they have.
|
||||
// Since this was a R/W DB transaction, the load will pick up this item.
|
||||
if (items.getValue() == null) return;
|
||||
|
||||
messageTree.add(item);
|
||||
if (scrollToItem) this.scrollToItem.set(item.getId());
|
||||
items.setValue(new LiveResult<>(messageTree.depthFirstOrder()));
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void setReplyId(@Nullable MessageId id) {
|
||||
replyId = id;
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@Nullable
|
||||
MessageId getReplyId() {
|
||||
return replyId;
|
||||
}
|
||||
|
||||
void storeMessageId(@Nullable MessageId messageId) {
|
||||
if (messageId != null) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
messageTracker.storeMessageId(groupId, messageId);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void markItemRead(I item);
|
||||
|
||||
/**
|
||||
* Returns the {@link MessageId} of the item that was at the top of the
|
||||
* list last time or null if there has been nothing stored, yet.
|
||||
*/
|
||||
@Nullable
|
||||
MessageId getAndResetRestoredMessageId() {
|
||||
return storedMessageId.getAndSet(null);
|
||||
}
|
||||
|
||||
LiveData<LiveResult<List<I>>> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
LiveData<SharingInfo> getSharingInfo() {
|
||||
return sharingController.getSharingInfo();
|
||||
}
|
||||
|
||||
LiveData<Boolean> getGroupRemoved() {
|
||||
return groupRemoved;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MessageId getAndResetScrollToItem() {
|
||||
return scrollToItem.getAndSet(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,15 +20,15 @@ class ThreadScrollListener<I extends ThreadItem>
|
||||
private static final Logger LOG =
|
||||
getLogger(ThreadScrollListener.class.getName());
|
||||
|
||||
private final ThreadListController<?, I> controller;
|
||||
private final ThreadListViewModel<I> viewModel;
|
||||
private final UnreadMessageButton upButton, downButton;
|
||||
|
||||
ThreadScrollListener(ThreadItemAdapter<I> adapter,
|
||||
ThreadListController<?, I> controller,
|
||||
ThreadListViewModel<I> viewModel,
|
||||
UnreadMessageButton upButton,
|
||||
UnreadMessageButton downButton) {
|
||||
super(adapter);
|
||||
this.controller = controller;
|
||||
this.viewModel = viewModel;
|
||||
this.upButton = upButton;
|
||||
this.downButton = downButton;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ class ThreadScrollListener<I extends ThreadItem>
|
||||
protected void onItemVisible(I item) {
|
||||
if (!item.isRead()) {
|
||||
item.setRead(true);
|
||||
controller.markItemRead(item);
|
||||
viewModel.markItemRead(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ import static java.util.concurrent.TimeUnit.DAYS;
|
||||
import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes;
|
||||
import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
|
||||
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_LOGCAT;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_APP_START_TIME;
|
||||
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
|
||||
|
||||
@@ -357,16 +358,17 @@ public class UiUtils {
|
||||
}
|
||||
|
||||
public static void triggerFeedback(Context ctx) {
|
||||
startDevReportActivity(ctx, FeedbackActivity.class, null, null);
|
||||
startDevReportActivity(ctx, FeedbackActivity.class, null, null, null);
|
||||
}
|
||||
|
||||
public static void startDevReportActivity(Context ctx,
|
||||
Class<? extends FragmentActivity> activity, @Nullable Throwable t,
|
||||
@Nullable Long appStartTime) {
|
||||
@Nullable Long appStartTime, @Nullable byte[] logKey) {
|
||||
final Intent dialogIntent = new Intent(ctx, activity);
|
||||
dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
dialogIntent.putExtra(EXTRA_THROWABLE, t);
|
||||
dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime);
|
||||
dialogIntent.putExtra(EXTRA_APP_LOGCAT, logKey);
|
||||
ctx.startActivity(dialogIntent);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.Application;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbCallable;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.DbRunnable;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.db.TransactionManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
@@ -23,6 +24,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -57,8 +59,8 @@ public abstract class DbViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given task on the {@link DatabaseExecutor}
|
||||
* and waits for the DB to open.
|
||||
* Waits for the DB to open and runs the given task on the
|
||||
* {@link DatabaseExecutor}.
|
||||
* <p>
|
||||
* If you need a list of items to be displayed in a
|
||||
* {@link RecyclerView.Adapter},
|
||||
@@ -76,6 +78,29 @@ public abstract class DbViewModel extends AndroidViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the DB to open and runs the given task on the
|
||||
* {@link DatabaseExecutor}.
|
||||
* <p>
|
||||
* If you need a list of items to be displayed in a
|
||||
* {@link RecyclerView.Adapter},
|
||||
* use {@link #loadList(DbCallable, UiConsumer)} instead.
|
||||
*/
|
||||
protected void runOnDbThread(boolean readOnly,
|
||||
DbRunnable<Exception> task, Consumer<Exception> err) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
lifecycleManager.waitForDatabase();
|
||||
db.transaction(readOnly, task);
|
||||
} catch (InterruptedException e) {
|
||||
LOG.warning("Interrupted while waiting for database");
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
err.accept(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a list of items on the {@link DatabaseExecutor} within a single
|
||||
* {@link Transaction} and publishes it as a {@link LiveResult}
|
||||
|
||||
@@ -39,6 +39,17 @@ public class LiveEvent<T> extends LiveData<LiveEvent.ConsumableEvent<T>> {
|
||||
super.observeForever(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last value of the event (even if already consumed)
|
||||
* or null if there hasn't been any value so far.
|
||||
*/
|
||||
@Nullable
|
||||
public T getLastValue() {
|
||||
ConsumableEvent<T> event = getValue();
|
||||
if (event == null) return null;
|
||||
return event.content;
|
||||
}
|
||||
|
||||
static class ConsumableEvent<T> {
|
||||
|
||||
private final T content;
|
||||
|
||||
@@ -39,7 +39,8 @@ public interface AndroidNotificationManager {
|
||||
String BLOG_CHANNEL_ID = "blogs";
|
||||
// Channels are sorted by channel ID in the Settings app, so use IDs
|
||||
// that will sort below the main channels such as contacts
|
||||
String ONGOING_CHANNEL_ID = "zForegroundService";
|
||||
String ONGOING_CHANNEL_OLD_ID = "zForegroundService";
|
||||
String ONGOING_CHANNEL_ID = "zForegroundService2";
|
||||
String FAILURE_CHANNEL_ID = "zStartupFailure";
|
||||
String REMINDER_CHANNEL_ID = "zSignInReminder";
|
||||
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
android:textColor="@color/briar_primary"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="18sp"
|
||||
android:typeface="monospace"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/yourLinkIcon"
|
||||
|
||||
@@ -139,10 +139,10 @@
|
||||
<Space
|
||||
android:id="@+id/space"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/addButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_default="wrap"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/contactNameLayout" />
|
||||
|
||||
@@ -173,4 +173,4 @@
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_invite"
|
||||
android:icon="@drawable/social_share_white"
|
||||
android:title="@string/groups_invite_members"
|
||||
app:showAsAction="ifRoom"/>
|
||||
android:visible="false"
|
||||
app:showAsAction="ifRoom"
|
||||
tools:visible="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_member_list"
|
||||
android:icon="@drawable/ic_group_white"
|
||||
android:title="@string/groups_member_list"
|
||||
app:showAsAction="never"/>
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_reveal"
|
||||
android:icon="@drawable/ic_visibility_white"
|
||||
android:title="@string/groups_reveal_contacts"
|
||||
app:showAsAction="never"/>
|
||||
android:visible="false"
|
||||
app:showAsAction="never"
|
||||
tools:visible="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_leave"
|
||||
android:icon="@drawable/action_delete_white"
|
||||
android:title="@string/groups_leave"
|
||||
app:showAsAction="never"/>
|
||||
android:visible="false"
|
||||
app:showAsAction="never"
|
||||
tools:visible="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_group_dissolve"
|
||||
android:icon="@drawable/action_delete_white"
|
||||
android:title="@string/groups_dissolve"
|
||||
app:showAsAction="never"/>
|
||||
android:visible="false"
|
||||
app:showAsAction="never"
|
||||
tools:visible="true" />
|
||||
|
||||
</menu>
|
||||
@@ -67,8 +67,11 @@
|
||||
<string name="lock_button">قفل کردن برنامه</string>
|
||||
<string name="settings_button">تنظیمات</string>
|
||||
<string name="sign_out_button">خروج</string>
|
||||
<string name="transports_onboarding_text">برای کنترل چگونگی اتصال Briar (برایر) به مخاطبین خود، اینجا را لمس کنید.</string>
|
||||
<!--Transports: Tor-->
|
||||
<string name="transport_tor">اینترنت</string>
|
||||
<string name="tor_device_status_online_wifi">تلفن شما از طریق Wi-Fi به اینترنت دسترسی دارد.</string>
|
||||
<string name="tor_device_status_online_mobile">تلفن شما از طریق دیتا سیمکارت به اینترنت دسترسی دارد.</string>
|
||||
<string name="tor_device_status_offline">تلفن شما دارای دسترسی اینترنتی نیست</string>
|
||||
<string name="tor_plugin_status_enabling">Briar در حال اتصال به اینترنت می باشد</string>
|
||||
<string name="tor_plugin_status_active">Briar به اینترنت متصل شد</string>
|
||||
@@ -462,6 +465,10 @@
|
||||
برای وارد کردن خوراک روی آیکون + ضربه بزنید</string>
|
||||
<string name="blogs_rss_feeds_manage_error">مشکلی با بارگذاری فیدهای شما وجود داشت. لطفا بعدا امتحان کنید.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="change_profile_picture">برای تغییر تصویر نمایه خود اینجا را لمس کنید.</string>
|
||||
<string name="dialog_confirm_profile_picture_title">تغییر تصویر نمایه</string>
|
||||
<string name="dialog_confirm_profile_picture_remark">تنها مخاطبین شما میتوانند تصویر نمایه شما را مشاهده کنند.</string>
|
||||
<string name="change_profile_picture_failed_message">تاسفیم اما هنگام بروزرسانی تصویر نمایه شما مشکلی رخ داد.</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">زبان و منطقه</string>
|
||||
<string name="pref_language_changed">این تنظیمات زمانی که Briar (برایر) را ری استارت کنید تاثیر خود را می گذارند. لطفا خارج شوید و Briar (برایر) را دوباره راه اندازی کنید.</string>
|
||||
@@ -571,17 +578,20 @@
|
||||
<string name="include_debug_report_feedback">قرار دادن داده های ناشناس درباره این دستگاه</string>
|
||||
<string name="dev_report_basic_info">اطلاعات پایه</string>
|
||||
<string name="dev_report_device_info">اطلاعات دستگاه</string>
|
||||
<string name="dev_report_stacktrace">Stacktrace</string>
|
||||
<string name="dev_report_time_info">اطلاعات زمانی</string>
|
||||
<string name="dev_report_memory">حافظه</string>
|
||||
<string name="dev_report_storage">حافظه</string>
|
||||
<string name="dev_report_connectivity">اتصال</string>
|
||||
<string name="dev_report_build_config">پیکربندی ساخت</string>
|
||||
<string name="dev_report_logcat">لاگ برنامه</string>
|
||||
<string name="dev_report_device_features">ویژگیهای دستگاه</string>
|
||||
<string name="send_report">ارسال گزارش</string>
|
||||
<string name="close">بستن</string>
|
||||
<string name="dev_report_sending">در حال فرستادن نظر...</string>
|
||||
<string name="dev_report_sent">بازخورد ارسال شد</string>
|
||||
<string name="dev_report_saved">گزارش ذخیره شد. دفعه بعدی که وارد Briar (برایر) شدید فرستاده خواهد شد.</string>
|
||||
<string name="dev_report_error">خطا در ارسال گزارش</string>
|
||||
<!--Sign Out-->
|
||||
<string name="progress_title_logout">خروج از Briar (برایر)...</string>
|
||||
<!--Screen Filters & Tapjacking-->
|
||||
@@ -591,7 +601,9 @@
|
||||
این برنامه ها ممکن است روی Briar (برایر) قرار گرفته باشند:
|
||||
|
||||
%1$s</string>
|
||||
<string name="screen_filter_body_api_30">برنامه دیگری بر روی برنامه Briar (برایر) قرار دارد. برای محافظت از امنیت شما، Briar (برایر) هنگامی که برنامه دیگری روی آن باز است، به لمس پاسخ نخواهد داد. \n\nبرای یافتن برنامه مذکور، برنامههای زیر را بررسی کنید.</string>
|
||||
<string name="screen_filter_allow">به این برنامه ها اجازه بده تا روی Briar (برایر) قرار بگیرند</string>
|
||||
<string name="screen_filter_review_apps">بررسی برنامهها</string>
|
||||
<!--Permission Requests-->
|
||||
<string name="permission_camera_title">دسترسی به دوربین</string>
|
||||
<string name="permission_camera_request_body">برای اسکن کردن کد کیوآر دسترسی به دوربین لازم است.</string>
|
||||
@@ -608,6 +620,7 @@ Briar (برایر) موقعیت شما را ذخیره نمیکند و آن
|
||||
<string name="permission_camera_denied_body">شما دسترسی به دوربین را رد کرده اید، اما افزودن مخاطب نیاز به دوربین دارد.
|
||||
|
||||
لطفا اجازه دسترسی را بدهید.</string>
|
||||
<string name="permission_location_denied_body">شما دسترسی به موقعیت خود را ندادهاید اما Briar (برایر) برای یافتن دستگاههای بلوتوث نیاز به این دسترسی دارد.\n\nلطفا این دسترسی را فراهم کنید.</string>
|
||||
<string name="qr_code">کد کیوآر</string>
|
||||
<string name="show_qr_code_fullscreen">نمایش کد کیوآر به صورت فول اسکرین</string>
|
||||
<!--App Locking-->
|
||||
@@ -618,6 +631,7 @@ Briar (برایر) موقعیت شما را ذخیره نمیکند و آن
|
||||
<string name="lock_is_locked">Briar (برایر) قفل می باشد</string>
|
||||
<string name="lock_tap_to_unlock">برای آنلاک کردن کلیک کنید</string>
|
||||
<!--Connections Screen-->
|
||||
<string name="transports_help_text">Briar (برایر) میتواند از طریق اینترنت، Wi-Fi و یا بلوتوث به مخاطبین شما متصل گردد.\n\nارتباط با اینترنت از طریق شبکهی تور صورت میپذیرد.\n\nاگر دسترسی به مخاطب شما از روشهای مختلفی ممکن باشد، Briar (برایر) به صورت موازی از آنها استفاده خواهد کرد.</string>
|
||||
<!--Screenshots-->
|
||||
<!--This is a name to be used in screenshots. Feel free to change it to a local name.-->
|
||||
<string name="screenshot_alice">آلیس</string>
|
||||
|
||||
@@ -425,6 +425,10 @@
|
||||
<string name="blogs_rss_feeds_manage_empty_state">Sen fontes RSS que mostrar\n\nToque na icona + para importar unha fonte</string>
|
||||
<string name="blogs_rss_feeds_manage_error">Aconteceu un problema ao cargar as súas fontes. Por favor, inténteo máis tarde.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="change_profile_picture">Toca para cambiar a túa imaxe de perfil</string>
|
||||
<string name="dialog_confirm_profile_picture_title">Mudar imaxe de perfil</string>
|
||||
<string name="dialog_confirm_profile_picture_remark">Só os teus contactos poden ver a túa imaxe de perfil</string>
|
||||
<string name="change_profile_picture_failed_message">Lamentámolo, pero algo fallou cando intentamos actualizar a túa imaxe de pefil</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">Idioma & rexión</string>
|
||||
<string name="pref_language_changed">Este axuste terá efecto cando reinicie Briar. Por favor desconecte e volte a iniciar Briar.</string>
|
||||
|
||||
@@ -449,6 +449,10 @@
|
||||
<string name="blogs_rss_feeds_manage_empty_state">אין הזנות RSS להראות\n\nהקש על הצלמית + כדי לייבא הזנה</string>
|
||||
<string name="blogs_rss_feeds_manage_error">הייתה בעיה בטעינת ההזנות שלך. אנא נסה שוב מאוחר יותר.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="change_profile_picture">הקש כדי לשנות את תמונת הפרופיל שלך</string>
|
||||
<string name="dialog_confirm_profile_picture_title">שנה תמונת פרופיל</string>
|
||||
<string name="dialog_confirm_profile_picture_remark">רק אנשי הקשר שלך יכולים לראות את תמונת הפרופיל שלך</string>
|
||||
<string name="change_profile_picture_failed_message">אנו מצטערים משהו השתבש בעת עדכון תמונת הפרופיל שלך</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">שפה ואזור</string>
|
||||
<string name="pref_language_changed">הגדרה זו תיכנס לתוקף כשתפעיל מחדש את Briar. אנא התנתק והפעל מחדש את Briar.</string>
|
||||
@@ -558,6 +562,7 @@
|
||||
<string name="include_debug_report_feedback">כלול נתונים אלמוניים לגבי מכשיר זה</string>
|
||||
<string name="dev_report_basic_info">מידע בסיסי</string>
|
||||
<string name="dev_report_device_info">מידע מכשיר</string>
|
||||
<string name="dev_report_stacktrace">מחסנית עקיבה (Stacktrace)</string>
|
||||
<string name="dev_report_time_info">מידע זמן</string>
|
||||
<string name="dev_report_memory">זיכרון</string>
|
||||
<string name="dev_report_storage">אחסון</string>
|
||||
|
||||
@@ -425,6 +425,10 @@
|
||||
<string name="blogs_rss_feeds_manage_empty_state">Engin RSS-streymi til að birta\n\nÝttu á + táknið til að flytja inn streymi</string>
|
||||
<string name="blogs_rss_feeds_manage_error">Vandamál hefur komið upp með að hlaða inn streymunum þínum. Reyndu aftur síðar.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="change_profile_picture">Ýttu til að skipta um auðkennismyndina þína</string>
|
||||
<string name="dialog_confirm_profile_picture_title">Skipta um auðkennismynd</string>
|
||||
<string name="dialog_confirm_profile_picture_remark">Einungis tengiliðirnir þínir geta séð auðkennismyndina þína</string>
|
||||
<string name="change_profile_picture_failed_message">Því miður, eitthvað fór úrskeiðis við að uppfæra auðkennismyndina þína.</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">Tungumál og landsvæði</string>
|
||||
<string name="pref_language_changed">Þessi stilling tekur gildi í næst þegar þú skráir þig inn í Briar. Skráðu þig út og endurræstu Briar.</string>
|
||||
|
||||
@@ -425,6 +425,10 @@
|
||||
<string name="blogs_rss_feeds_manage_empty_state">Nessun feed RSS da mostrare\n\nClicca l\'icona + per importare un feed</string>
|
||||
<string name="blogs_rss_feeds_manage_error">C\'è stato un problema nel caricare i tuoi feeds. Per favore riprova fra poco.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="change_profile_picture">Tocca per cambiare l\'immagine del profilo</string>
|
||||
<string name="dialog_confirm_profile_picture_title">Cambia immagine profilo</string>
|
||||
<string name="dialog_confirm_profile_picture_remark">Solo i tuoi contatti possono vedere l\'immagine del profilo</string>
|
||||
<string name="change_profile_picture_failed_message">Spiacenti, qualcosa è andato storto aggiornando la tua foto del profilo.</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">Lingua & regione</string>
|
||||
<string name="pref_language_changed">Questa impostazione avrà effetto quando riavvierai Briar. Per favore, esci e riavvia Briar.</string>
|
||||
|
||||
@@ -425,6 +425,10 @@
|
||||
<string name="blogs_rss_feeds_manage_empty_state">Geen RSS-feeds om te tonen\n\nTik op het +-icoon om een feed te importeren</string>
|
||||
<string name="blogs_rss_feeds_manage_error">Er was een probleem met het laden van je feeds. Probeer het alsjeblieft later nog een keer.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="change_profile_picture">Tik om je profielfoto te wijzigen</string>
|
||||
<string name="dialog_confirm_profile_picture_title">Wijzig profielfoto</string>
|
||||
<string name="dialog_confirm_profile_picture_remark">Alleen je contacten kunnen je profielfoto zien</string>
|
||||
<string name="change_profile_picture_failed_message">Excuses, maar er is iets misgegaan met het updaten van je profielfoto</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">Taal & regio</string>
|
||||
<string name="pref_language_changed">Deze instelling zal werken wanneer u Briar opnieuw opstart. Gelieve uit te loggen en Briar opnieuw te starten.</string>
|
||||
@@ -564,7 +568,7 @@
|
||||
<string name="permission_camera_location_title">Camera en locatie</string>
|
||||
<string name="permission_camera_location_request_body">Om de QR-code in te scannen heeft Briar toegang nodig tot de camera.\n\nOm bluetoothapparaten te ontdekken heeft Briar toestemming nodig tot je locatie.\n\nBriar slaat je locatie niet op en deelt het met niemand.</string>
|
||||
<string name="permission_camera_denied_body">Je hebt toegang tot de camera niet vrijgegeven, terwijl het toevoegen van contacten de camera nodig heeft.\n\nOverweeg alsjeblieft toegang vrij te geven.</string>
|
||||
<string name="permission_location_denied_body">Je hebt geen toegang tot je locatie gegeven, maar Briar heeft deze rechten nodig om apparaten via bleutooth te vinden.\n\nOverweeg a.u.b. deze rechten te geven.</string>
|
||||
<string name="permission_location_denied_body">Je hebt geen toegang tot je locatie gegeven, maar Briar heeft deze rechten nodig om apparaten via bluetooth te vinden.\n\nOverweeg a.u.b. deze rechten te geven.</string>
|
||||
<string name="qr_code">QR-code</string>
|
||||
<string name="show_qr_code_fullscreen">Toon QR-code op volledig scherm</string>
|
||||
<!--App Locking-->
|
||||
|
||||
@@ -435,6 +435,10 @@
|
||||
<string name="blogs_rss_feeds_manage_empty_state">Nici un flux RSS de arătat\n\nAtingeți iconița + pentru a adăuga un flux</string>
|
||||
<string name="blogs_rss_feeds_manage_error">A apărut o eroare la încărcarea fluxurilor dumneavoastră. Vă rugăm să încercați din nou mai târziu.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="change_profile_picture">Atingeți pentru a vă schimba poza de profil</string>
|
||||
<string name="dialog_confirm_profile_picture_title">Schimbare poză de profil</string>
|
||||
<string name="dialog_confirm_profile_picture_remark">Doar contactele vor vedea poza de contact</string>
|
||||
<string name="change_profile_picture_failed_message">Ne pare rău, dar ceva nu a funcționat cum trebuie la actualizarea pozei de profil</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">Limbă & Regiune</string>
|
||||
<string name="pref_language_changed">Această setare va avea efect după repornirea Briar. Vă rugăm să ieșiți din Briar și să reporniți aplicația.</string>
|
||||
|
||||
@@ -447,6 +447,10 @@
|
||||
<string name="blogs_rss_feeds_manage_empty_state">Нет RSS-лент для отображения\n\nКоснитесь значка + для импорта ленты</string>
|
||||
<string name="blogs_rss_feeds_manage_error">Ошибка при загрузке вашей ленты. Повторите попытку позже.</string>
|
||||
<!--Settings Profile Picture-->
|
||||
<string name="change_profile_picture">Нажмите, чтобы изменить изображение вашего профиля </string>
|
||||
<string name="dialog_confirm_profile_picture_title">Изменить изображение профиля</string>
|
||||
<string name="dialog_confirm_profile_picture_remark">Только ваши контакты могут видеть изображение вашего профиля</string>
|
||||
<string name="change_profile_picture_failed_message">Нам очень жаль, но что-то пошло не так во время обновления изображения вашего профиля.</string>
|
||||
<!--Settings Display-->
|
||||
<string name="pref_language_title">Язык и регион</string>
|
||||
<string name="pref_language_changed">Этот параметр вступит в силу после перезапуска Briar. Пожалуйста, выйдите и перезапустите Briar.</string>
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.briar.api.identity.AuthorInfo;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemList;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemListImpl;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static org.briarproject.briar.api.identity.AuthorInfo.Status.UNKNOWN;
|
||||
import static org.briarproject.bramble.test.TestUtils.getAuthor;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 21)
|
||||
public class ForumActivityTest {
|
||||
|
||||
private final static MessageId[] MESSAGE_IDS = new MessageId[6];
|
||||
|
||||
static {
|
||||
for (int i = 0; i < MESSAGE_IDS.length; i++)
|
||||
MESSAGE_IDS[i] = new MessageId(getRandomId());
|
||||
}
|
||||
|
||||
private final static MessageId[] PARENT_IDS = {
|
||||
null,
|
||||
MESSAGE_IDS[0],
|
||||
MESSAGE_IDS[1],
|
||||
MESSAGE_IDS[2],
|
||||
MESSAGE_IDS[0],
|
||||
null
|
||||
};
|
||||
|
||||
/*
|
||||
1
|
||||
-> 2
|
||||
-> 3
|
||||
-> 4
|
||||
5
|
||||
6
|
||||
*/
|
||||
private final static int[] LEVELS = {
|
||||
0, 1, 2, 3, 1, 0
|
||||
};
|
||||
|
||||
private TestForumActivity forumActivity;
|
||||
@Captor
|
||||
private ArgumentCaptor<UiResultExceptionHandler<ThreadItemList<ForumPostItem>, DbException>>
|
||||
rc;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("briar.GROUP_ID", getRandomId());
|
||||
forumActivity = Robolectric.buildActivity(TestForumActivity.class,
|
||||
intent).create().start().resume().get();
|
||||
}
|
||||
|
||||
private ThreadItemList<ForumPostItem> getDummyData() {
|
||||
ForumPostItem[] forumPostItems = new ForumPostItem[6];
|
||||
for (int i = 0; i < forumPostItems.length; i++) {
|
||||
Author author = getAuthor();
|
||||
String text = getRandomString(MAX_FORUM_POST_TEXT_LENGTH);
|
||||
forumPostItems[i] = new ForumPostItem(MESSAGE_IDS[i], PARENT_IDS[i],
|
||||
text, System.currentTimeMillis(), author,
|
||||
new AuthorInfo(UNKNOWN));
|
||||
forumPostItems[i].setLevel(LEVELS[i]);
|
||||
}
|
||||
ThreadItemList<ForumPostItem> list = new ThreadItemListImpl<>();
|
||||
list.addAll(Arrays.asList(forumPostItems));
|
||||
return list;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedEntries() {
|
||||
ForumController mc = forumActivity.getController();
|
||||
ThreadItemList<ForumPostItem> dummyData = getDummyData();
|
||||
verify(mc, times(1)).loadItems(rc.capture());
|
||||
rc.getValue().onResult(dummyData);
|
||||
ThreadItemAdapter<ForumPostItem> adapter = forumActivity.getAdapter();
|
||||
Assert.assertNotNull(adapter);
|
||||
assertEquals(6, adapter.getItemCount());
|
||||
assertEquals(dummyData.get(0).getText(),
|
||||
adapter.getItemAt(0).getText());
|
||||
assertEquals(dummyData.get(1).getText(),
|
||||
adapter.getItemAt(1).getText());
|
||||
assertEquals(dummyData.get(2).getText(),
|
||||
adapter.getItemAt(2).getText());
|
||||
assertEquals(dummyData.get(3).getText(),
|
||||
adapter.getItemAt(3).getText());
|
||||
assertEquals(dummyData.get(4).getText(),
|
||||
adapter.getItemAt(4).getText());
|
||||
assertEquals(dummyData.get(5).getText(),
|
||||
adapter.getItemAt(5).getText());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package org.briarproject.briar.android.forum;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||
import org.briarproject.briar.R;
|
||||
import org.briarproject.briar.android.activity.ActivityModule;
|
||||
import org.briarproject.briar.android.activity.BaseActivity;
|
||||
import org.briarproject.briar.android.controller.BriarController;
|
||||
import org.briarproject.briar.android.controller.BriarControllerImpl;
|
||||
import org.briarproject.briar.android.threaded.ThreadItemAdapter;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class exposes the ForumController and offers the possibility to
|
||||
* override it.
|
||||
*/
|
||||
@MethodsNotNullByDefault
|
||||
@ParametersNotNullByDefault
|
||||
public class TestForumActivity extends ForumActivity {
|
||||
|
||||
@Override
|
||||
public ForumController getController() {
|
||||
return forumController;
|
||||
}
|
||||
|
||||
public ThreadItemAdapter<ForumPostItem> getAdapter() {
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle state) {
|
||||
setTheme(R.style.BriarTheme_NoActionBar);
|
||||
super.onCreate(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ActivityModule getActivityModule() {
|
||||
return new ActivityModule(this) {
|
||||
|
||||
@Override
|
||||
protected BriarController provideBriarController(
|
||||
BriarControllerImpl briarController) {
|
||||
BriarController c = Mockito.mock(BriarController.class);
|
||||
Mockito.when(c.accountSignedIn()).thenReturn(true);
|
||||
return c;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ForumModule getForumModule() {
|
||||
return new ForumModule() {
|
||||
@Override
|
||||
ForumController provideForumController(BaseActivity activity,
|
||||
ForumControllerImpl forumController) {
|
||||
return Mockito.mock(ForumController.class);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.briarproject.briar.android.logging;
|
||||
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
import static java.util.logging.Level.FINE;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.SEVERE;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.briarproject.briar.android.logging.BriefLogFormatter.formatLog;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class LogEncryptionDecryptionTest extends BrambleMockTestCase {
|
||||
|
||||
@ClassRule
|
||||
public static TemporaryFolder folder = new TemporaryFolder();
|
||||
|
||||
private final SecureRandom random;
|
||||
private final CachingLogHandler cachingLogHandler;
|
||||
private final LogEncrypter logEncrypter;
|
||||
private final LogDecrypter logDecrypter;
|
||||
private final BriefLogFormatter logFormatter = new BriefLogFormatter();
|
||||
|
||||
public LogEncryptionDecryptionTest() throws IOException {
|
||||
LoggingComponent loggingComponent = DaggerLoggingComponent.builder()
|
||||
.loggingTestModule(new LoggingTestModule(folder.newFile()))
|
||||
.build();
|
||||
random = loggingComponent.random();
|
||||
logEncrypter = loggingComponent.logEncrypter();
|
||||
logDecrypter = loggingComponent.logDecrypter();
|
||||
cachingLogHandler = loggingComponent.cachingLogHandler();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptedMatchesDecrypted() {
|
||||
ArrayList<LogRecord> logRecords =
|
||||
new ArrayList<>(random.nextInt(99) + 1);
|
||||
for (int i = 0; i < logRecords.size(); i++) {
|
||||
LogRecord logRecord = getRandomLogRecord();
|
||||
cachingLogHandler.publish(logRecord);
|
||||
logRecords.add(logRecord);
|
||||
}
|
||||
byte[] logKey = logEncrypter.encryptLogs();
|
||||
assertEquals(formatLog(logFormatter, logRecords),
|
||||
logDecrypter.decryptLogs(logKey));
|
||||
}
|
||||
|
||||
private LogRecord getRandomLogRecord() {
|
||||
Level[] levels = {SEVERE, WARNING, INFO, FINE};
|
||||
Level level = levels[random.nextInt(levels.length)];
|
||||
LogRecord logRecord =
|
||||
new LogRecord(level, getRandomString(random.nextInt(128) + 1));
|
||||
logRecord.setLoggerName(getRandomString(random.nextInt(23) + 1));
|
||||
return logRecord;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.briarproject.briar.android.logging;
|
||||
|
||||
import org.briarproject.bramble.BrambleCoreModule;
|
||||
import org.briarproject.bramble.test.TestSecureRandomModule;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {
|
||||
BrambleCoreModule.class,
|
||||
TestSecureRandomModule.class,
|
||||
LoggingModule.class,
|
||||
LoggingTestModule.class,
|
||||
})
|
||||
public interface LoggingComponent {
|
||||
|
||||
SecureRandom random();
|
||||
|
||||
CachingLogHandler cachingLogHandler();
|
||||
|
||||
LogEncrypter logEncrypter();
|
||||
|
||||
LogDecrypter logDecrypter();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.briarproject.briar.android.logging;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.PublicKey;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.reporting.DevConfig;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module
|
||||
class LoggingTestModule {
|
||||
|
||||
private final File logFile;
|
||||
|
||||
LoggingTestModule(File logFile) {
|
||||
this.logFile = logFile;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
DevConfig provideDevConfig() {
|
||||
@NotNullByDefault
|
||||
DevConfig devConfig = new DevConfig() {
|
||||
@Override
|
||||
public PublicKey getDevPublicKey() {
|
||||
throw new UnsupportedOperationException();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDevOnionAddress() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getReportDir() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getLogcatFile() {
|
||||
return logFile;
|
||||
}
|
||||
};
|
||||
return devConfig;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@@ -15,11 +15,9 @@ public interface MessageTree<T extends MessageTree.MessageNode> {
|
||||
|
||||
void add(T node);
|
||||
|
||||
void setComparator(Comparator<T> comparator);
|
||||
|
||||
void clear();
|
||||
|
||||
Collection<T> depthFirstOrder();
|
||||
List<T> depthFirstOrder();
|
||||
|
||||
boolean contains(MessageId m);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@@ -59,6 +60,12 @@ public interface ForumManager {
|
||||
*/
|
||||
ForumPostHeader addLocalPost(ForumPost p) throws DbException;
|
||||
|
||||
/**
|
||||
* Stores a local forum post.
|
||||
*/
|
||||
ForumPostHeader addLocalPost(Transaction txn, ForumPost p)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the forum with the given ID.
|
||||
*/
|
||||
@@ -84,11 +91,22 @@ public interface ForumManager {
|
||||
*/
|
||||
String getPostText(MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the text of the forum post with the given ID.
|
||||
*/
|
||||
String getPostText(Transaction txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the headers of all posts in the given forum.
|
||||
*/
|
||||
Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the headers of all posts in the given forum.
|
||||
*/
|
||||
List<ForumPostHeader> getPostHeaders(Transaction txn, GroupId g)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Registers a hook to be called whenever a forum is removed.
|
||||
*/
|
||||
@@ -97,7 +115,6 @@ public interface ForumManager {
|
||||
/**
|
||||
* Returns the group count for the given forum.
|
||||
*/
|
||||
@Deprecated
|
||||
GroupCount getGroupCount(GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,21 +8,14 @@ import javax.annotation.concurrent.Immutable;
|
||||
@NotNullByDefault
|
||||
public class JoinMessageHeader extends GroupMessageHeader {
|
||||
|
||||
private final Visibility visibility;
|
||||
private final boolean isInitial;
|
||||
|
||||
public JoinMessageHeader(GroupMessageHeader h, Visibility visibility,
|
||||
boolean isInitial) {
|
||||
public JoinMessageHeader(GroupMessageHeader h, boolean isInitial) {
|
||||
super(h.getGroupId(), h.getId(), h.getParentId(), h.getTimestamp(),
|
||||
h.getAuthor(), h.getAuthorInfo(), h.isRead());
|
||||
this.visibility = visibility;
|
||||
this.isInitial = isInitial;
|
||||
}
|
||||
|
||||
public Visibility getVisibility() {
|
||||
return visibility;
|
||||
}
|
||||
|
||||
public boolean isInitial() {
|
||||
return isInitial;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface PrivateGroupManager {
|
||||
@@ -81,6 +82,12 @@ public interface PrivateGroupManager {
|
||||
*/
|
||||
GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
|
||||
|
||||
/**
|
||||
* Stores and sends a local private group message.
|
||||
*/
|
||||
GroupMessageHeader addLocalMessage(Transaction txn, GroupMessage p)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the private group with the given ID.
|
||||
*/
|
||||
@@ -107,16 +114,33 @@ public interface PrivateGroupManager {
|
||||
*/
|
||||
String getMessageText(MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the text of the private group message with the given ID.
|
||||
*/
|
||||
String getMessageText(Transaction txn, MessageId m) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the headers of all messages in the given private group.
|
||||
*/
|
||||
Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns the headers of all messages in the given private group.
|
||||
*/
|
||||
List<GroupMessageHeader> getHeaders(Transaction txn, GroupId g)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all members of the given private group.
|
||||
*/
|
||||
Collection<GroupMember> getMembers(GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all members of the given private group.
|
||||
*/
|
||||
Collection<GroupMember> getMembers(Transaction txn, GroupId g)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the given author is a member of the given private group.
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.briar.api.sharing;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
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.briar.api.client.SessionId;
|
||||
@@ -45,6 +46,12 @@ public interface SharingManager<S extends Shareable>
|
||||
*/
|
||||
Collection<Contact> getSharedWith(GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all contacts with whom the given group is shared.
|
||||
*/
|
||||
Collection<Contact> getSharedWith(Transaction txn, GroupId g)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the group not already shared and no invitation is open
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
dependencyVerification {
|
||||
verify = [
|
||||
'junit:junit:4.13.1:junit-4.13.1.jar:c30719db974d6452793fe191b3638a5777005485bae145924044530ffa5f6122',
|
||||
'org.codehaus.mojo.signature:java16:1.1:java16-1.1.signature:53799223a2c98dba2d0add810bed76315460df285c69e4f397ae6098f87dd619',
|
||||
'org.codehaus.mojo:animal-sniffer-ant-tasks:1.16:animal-sniffer-ant-tasks-1.16.jar:890040976fbe2d584619a6a61b1fd2e925b3b5eb342a85eb2762c467c0d64e90',
|
||||
'org.codehaus.mojo:animal-sniffer:1.16:animal-sniffer-1.16.jar:72be8bcc226ba43b937c722a08a07852bfa1b11400089265d5df0ee7b38b1d52',
|
||||
'org.hamcrest:hamcrest-core:1.3:hamcrest-core-1.3.jar:66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9',
|
||||
'org.ow2.asm:asm-all:5.2:asm-all-5.2.jar:7fbffbc1db3422e2101689fd88df8384b15817b52b9b2b267b9f6d2511dc198d',
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
||||
private final List<List<T>> unsortedLists = new ArrayList<>();
|
||||
|
||||
@SuppressWarnings("UseCompareMethod")
|
||||
private Comparator<T> comparator = (o1, o2) ->
|
||||
private final Comparator<T> comparator = (o1, o2) ->
|
||||
Long.valueOf(o1.getTimestamp()).compareTo(o2.getTimestamp());
|
||||
|
||||
@Override
|
||||
@@ -79,6 +79,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
||||
@GuardedBy("this")
|
||||
private void sortUnsorted() {
|
||||
for (List<T> list : unsortedLists) {
|
||||
//noinspection Java8ListSort
|
||||
Collections.sort(list, comparator);
|
||||
}
|
||||
unsortedLists.clear();
|
||||
@@ -95,17 +96,7 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setComparator(Comparator<T> comparator) {
|
||||
this.comparator = comparator;
|
||||
// Sort all lists with the new comparator
|
||||
Collections.sort(roots, comparator);
|
||||
for (Map.Entry<MessageId, List<T>> entry : nodeMap.entrySet()) {
|
||||
Collections.sort(entry.getValue(), comparator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Collection<T> depthFirstOrder() {
|
||||
public synchronized List<T> depthFirstOrder() {
|
||||
List<T> orderedList = new ArrayList<>();
|
||||
for (T root : roots) {
|
||||
traverse(orderedList, root, 0);
|
||||
|
||||
@@ -126,29 +126,30 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
||||
|
||||
@Override
|
||||
public ForumPostHeader addLocalPost(ForumPost p) throws DbException {
|
||||
return db.transactionWithResult(false, txn -> {
|
||||
try {
|
||||
return addLocalPost(txn, p);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
});
|
||||
return db.transactionWithResult(false, txn -> addLocalPost(txn, p));
|
||||
}
|
||||
|
||||
private ForumPostHeader addLocalPost(Transaction txn, ForumPost p)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(KEY_TIMESTAMP, p.getMessage().getTimestamp());
|
||||
if (p.getParent() != null) meta.put(KEY_PARENT, p.getParent());
|
||||
Author a = p.getAuthor();
|
||||
meta.put(KEY_AUTHOR, clientHelper.toList(a));
|
||||
meta.put(KEY_LOCAL, true);
|
||||
meta.put(MSG_KEY_READ, true);
|
||||
clientHelper.addLocalMessage(txn, p.getMessage(), meta, true, false);
|
||||
messageTracker.trackOutgoingMessage(txn, p.getMessage());
|
||||
AuthorInfo authorInfo = authorManager.getMyAuthorInfo(txn);
|
||||
return new ForumPostHeader(p.getMessage().getId(), p.getParent(),
|
||||
p.getMessage().getTimestamp(), p.getAuthor(), authorInfo, true);
|
||||
@Override
|
||||
public ForumPostHeader addLocalPost(Transaction txn, ForumPost p)
|
||||
throws DbException {
|
||||
try {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(KEY_TIMESTAMP, p.getMessage().getTimestamp());
|
||||
if (p.getParent() != null) meta.put(KEY_PARENT, p.getParent());
|
||||
Author a = p.getAuthor();
|
||||
meta.put(KEY_AUTHOR, clientHelper.toList(a));
|
||||
meta.put(KEY_LOCAL, true);
|
||||
meta.put(MSG_KEY_READ, true);
|
||||
clientHelper
|
||||
.addLocalMessage(txn, p.getMessage(), meta, true, false);
|
||||
messageTracker.trackOutgoingMessage(txn, p.getMessage());
|
||||
AuthorInfo authorInfo = authorManager.getMyAuthorInfo(txn);
|
||||
return new ForumPostHeader(p.getMessage().getId(), p.getParent(),
|
||||
p.getMessage().getTimestamp(), p.getAuthor(), authorInfo,
|
||||
true);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -192,6 +193,15 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPostText(Transaction txn, MessageId m) throws DbException {
|
||||
try {
|
||||
return getPostText(clientHelper.getMessageAsList(txn, m));
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPostText(BdfList body) throws FormatException {
|
||||
// Parent ID, author, text, signature
|
||||
return body.getString(2);
|
||||
@@ -200,33 +210,35 @@ class ForumManagerImpl extends BdfIncomingMessageHook implements ForumManager {
|
||||
@Override
|
||||
public Collection<ForumPostHeader> getPostHeaders(GroupId g)
|
||||
throws DbException {
|
||||
return db.transactionWithResult(true, txn -> getPostHeaders(txn, g));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ForumPostHeader> getPostHeaders(Transaction txn, GroupId g)
|
||||
throws DbException {
|
||||
try {
|
||||
return db.transactionWithResult(true, txn -> {
|
||||
Collection<ForumPostHeader> headers = new ArrayList<>();
|
||||
Map<MessageId, BdfDictionary> metadata =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
// get all authors we need to get the info for
|
||||
Set<AuthorId> authors = new HashSet<>();
|
||||
for (Entry<MessageId, BdfDictionary> entry :
|
||||
metadata.entrySet()) {
|
||||
BdfList authorList = entry.getValue().getList(KEY_AUTHOR);
|
||||
Author a = clientHelper.parseAndValidateAuthor(authorList);
|
||||
authors.add(a.getId());
|
||||
}
|
||||
// get information for all authors
|
||||
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
|
||||
for (AuthorId id : authors) {
|
||||
authorInfos.put(id, authorManager.getAuthorInfo(txn, id));
|
||||
}
|
||||
// Parse the metadata
|
||||
for (Entry<MessageId, BdfDictionary> entry :
|
||||
metadata.entrySet()) {
|
||||
BdfDictionary meta = entry.getValue();
|
||||
headers.add(getForumPostHeader(txn, entry.getKey(), meta,
|
||||
authorInfos));
|
||||
}
|
||||
return headers;
|
||||
});
|
||||
List<ForumPostHeader> headers = new ArrayList<>();
|
||||
Map<MessageId, BdfDictionary> metadata =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
// get all authors we need to get the info for
|
||||
Set<AuthorId> authors = new HashSet<>();
|
||||
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
|
||||
BdfList authorList = entry.getValue().getList(KEY_AUTHOR);
|
||||
Author a = clientHelper.parseAndValidateAuthor(authorList);
|
||||
authors.add(a.getId());
|
||||
}
|
||||
// get information for all authors
|
||||
Map<AuthorId, AuthorInfo> authorInfos = new HashMap<>();
|
||||
for (AuthorId id : authors) {
|
||||
authorInfos.put(id, authorManager.getAuthorInfo(txn, id));
|
||||
}
|
||||
// Parse the metadata
|
||||
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
|
||||
BdfDictionary meta = entry.getValue();
|
||||
headers.add(getForumPostHeader(txn, entry.getKey(), meta,
|
||||
authorInfos));
|
||||
}
|
||||
return headers;
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user