Compare commits

...

59 Commits

Author SHA1 Message Date
akwizgran
e24728055c Update lint suppressions. 2021-02-09 11:15:01 +00:00
akwizgran
b2dc99bf26 Remove code that's only needed on API 15. 2021-02-09 11:15:01 +00:00
akwizgran
50d144c917 Avoid repeated calls to Resources.getSystem(). 2021-02-09 11:15:01 +00:00
goapunk
da7f57e0af update notifications on system language change 2021-02-09 11:15:01 +00:00
Julian Dehm
2374f8b4c9 address review 2021-02-09 11:15:01 +00:00
goapunk
5b77f28ce0 Skip setting the locale if system default is used 2021-02-09 11:15:00 +00:00
goapunk
c247e3aa4e Hardcode unsupported native language names 2021-02-09 11:15:00 +00:00
goapunk
8e0b71c76f Force locale on account deletion 2021-02-09 11:15:00 +00:00
goapunk
88f57893e8 Don't show unsupported locales in the settings 2021-02-09 11:14:58 +00:00
Julian Dehm
b111abb484 Set Locale only once 2021-02-09 11:14:16 +00:00
goapunk
4415598d3d Respect the deprecation of updateConfiguration 2021-02-09 11:14:16 +00:00
goapunk
40e14d3e94 Update the system configuration locale 2021-02-09 11:14:15 +00:00
akwizgran
1b19b331b1 Merge branch '1904-fragment-started-too-late' into 'master'
Don't launch fragments with back button when not started

Closes #1904

See merge request briar/briar!1365
2021-02-09 11:05:08 +00:00
akwizgran
d151a2d7f7 Merge branch '1910-state-exception-when-adding-contact' into 'master'
Restore remote handshake link when AddContactViewModel gets destroyed

Closes #1910

See merge request briar/briar!1364
2021-02-09 10:49:38 +00:00
Torsten Grote
9712a4b849 Don't launch fragments with back button when not started
Sounds strange, but apparently can happen.
2021-02-08 16:38:15 -03:00
Torsten Grote
cf1ac5e3e5 Restore remote handshake link when AddContactViewModel gets destroyed 2021-02-08 16:03:10 -03:00
akwizgran
0b9345f867 Merge branch '1621-link-disappearing' into 'master'
Remove monospace typeface from our briar:// link

Closes #1621

See merge request briar/briar!1362
2021-02-08 18:36:16 +00:00
Torsten Grote
12988120d1 Remove monospace typeface from our briar:// link
as this makes the text to become invisible when selecting all text on API 15-17
2021-02-08 14:45:57 -03:00
akwizgran
8d6c866e62 Merge branch '1926-cap-scrypt-cost' into 'master'
Cap the scrypt cost parameter to avoid OOM

Closes #1926

See merge request briar/briar!1360
2021-02-08 17:30:57 +00:00
akwizgran
8f82cf3c73 Merge branch '1917-logcat-process' into 'master'
Fix crash reporter to capture logs from main process

Closes #1917

See merge request briar/briar!1359
2021-02-08 16:58:12 +00:00
Torsten Grote
21112ce092 Encrypt logs before handing them to crash report process 2021-02-08 13:43:37 -03:00
akwizgran
21ee3ea00d Merge branch 'add-custom-dictionary' into 'master'
Add a custom dictionary

See merge request briar/briar!1361
2021-02-08 14:01:43 +00:00
Sebastian Kürten
bb964101b3 Add a custom dictionary
This reduces the amount of words highlighted by the spell checker and
helps focussing on words that are really misspelled.
2021-02-08 14:35:14 +01:00
akwizgran
d796eff0f6 Cap the scrypt cost parameter to avoid OOM. 2021-02-08 11:32:03 +00:00
Torsten Grote
700ea2b387 Add support for logs to StreamReader and StreamWriter
Shamelessly stolen from d9b4c013
2021-02-05 17:07:48 -03:00
Torsten Grote
6e3a7d8d0c Merge branch 'gitlab-bridge-test' into 'master'
Add GitLab pipeline stage for running optional tests

See merge request briar/briar!1353
2021-01-29 16:07:49 +00:00
akwizgran
166b5d4add Run optional tests automatically for tags, otherwise manually. 2021-01-29 15:45:39 +00:00
akwizgran
0fd59a26f6 Raise BridgeTest timeout to avoid spurious failures. 2021-01-29 15:39:59 +00:00
akwizgran
4162bf990a Merge branch '1881-thread-list-controller' into 'master'
Migrate ThreadListController to ViewModel

Closes #1881, #1873, and #1870

See merge request briar/briar!1336
2021-01-29 15:10:16 +00:00
akwizgran
09cfadbf7e Add manual pipeline stage for running optional tests. 2021-01-29 14:38:03 +00:00
akwizgran
3fb27dbb12 Bump version numbers for 1.2.14 release. 2021-01-29 14:10:24 +00:00
Torsten Grote
ae4a04bada Finishing touches of ThreadListViewModel migration
docs and minor improvements
2021-01-29 08:33:28 -03:00
akwizgran
831c65b647 Merge branch 'vector-compat' into 'master'
Use vector support libraries instead of rasterizing all drawables

See merge request briar/briar!1346
2021-01-28 17:59:08 +00:00
akwizgran
afcd38b84c Update translations. 2021-01-28 16:15:44 +00:00
Torsten Grote
d670179e30 Access MessageTree only on UiThread and improve code in the process 2021-01-27 15:37:09 -03:00
Torsten Grote
998c435b13 Allow to add forum/group posts in transaction 2021-01-27 15:37:09 -03:00
Torsten Grote
4a0327a62b thread list: fix redundant load and dissolved dialog showing again after screen rotation 2021-01-27 15:37:08 -03:00
akwizgran
70532732c8 Use commit action to add contacts on UI thread. 2021-01-27 15:37:08 -03:00
akwizgran
d69406dfe3 Add transactional getSharedWith() method to SharingManager. 2021-01-27 15:37:08 -03:00
akwizgran
98619df867 Use commit action to add contacts to SharingController. 2021-01-27 15:37:07 -03:00
akwizgran
f2eca0fdb6 Add transactional getMembers() method to PrivateGroupManager. 2021-01-27 15:37:07 -03:00
akwizgran
c62a57e8b2 Add transactional helper method to DbViewModel. 2021-01-27 15:37:07 -03:00
Torsten Grote
239c4a27ad Address first round of review feedback for thread list view model migration 2021-01-27 15:37:06 -03:00
Torsten Grote
e5d78a858d Clear thread notification automatically after blocking new ones 2021-01-26 15:42:18 -03:00
Torsten Grote
6005d156eb Rename ic_lock icon to notification_lock 2021-01-26 08:24:16 -03:00
Torsten Grote
635008fb60 Introduce SharingController with LiveData
and get rid of ThreadList controllers
2021-01-25 14:04:29 -03:00
Torsten Grote
b78569119a Remove Visibility from JoinMessageHeader and Item 2021-01-25 14:04:28 -03:00
Torsten Grote
8372bb01b2 Move marking thread list items read to ViewModel 2021-01-25 14:04:28 -03:00
Torsten Grote
766718e75c Remove text cache as it is no longer needed 2021-01-25 14:04:28 -03:00
Torsten Grote
1c107a851b Move thread list events, fields and notification handling into ViewModels 2021-01-25 14:04:26 -03:00
Torsten Grote
db53e79d1d Remove ForumActivityTest which provided little value anyway 2021-01-25 14:04:17 -03:00
Torsten Grote
21e56284fb Move adding new ThreadList items to ViewModel 2021-01-25 14:04:16 -03:00
Torsten Grote
d393b79ced Submit thread list items to ListAdapter 2021-01-25 14:04:09 -03:00
Torsten Grote
6611d7c02e Move removal of named groups into ViewModel 2021-01-25 14:00:43 -03:00
Torsten Grote
ab43dd4986 Create ThreadListViewModels and move loading of named groups there 2021-01-25 14:00:41 -03:00
Torsten Grote
36a9174781 Perform thread list core access within a single transaction 2021-01-25 14:00:15 -03:00
Torsten Grote
94dd75f24b Use VectorDrawableCompat compatible ways of setting drawables programmatically
so they won't crash on API < 21
2021-01-25 13:34:27 -03:00
Torsten Grote
c93e5441b0 Store rasterized notification icons, because NotificationCompat doesn't handle it
and would crash on API < 21
2021-01-25 13:34:27 -03:00
Torsten Grote
8ec8cc927b Use vector support libraries instead of rasterizing all drawables
https://developer.android.com/guide/topics/graphics/vector-drawable-resources#vector-drawables-backward-solution
2021-01-25 13:34:26 -03:00
195 changed files with 2540 additions and 2266 deletions

View File

@@ -2,6 +2,7 @@ image: briar/ci-image-android:latest
stages: stages:
- test - test
- optional_tests
- check_reproducibility - check_reproducibility
test: 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" - "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: only:
- tags - 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
View 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>

View File

@@ -11,8 +11,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 10213 versionCode 10214
versionName "1.2.13" versionName "1.2.14"
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -31,6 +31,7 @@ public class AndroidUtils {
private static final String FAKE_BLUETOOTH_ADDRESS = "02:00:00:00:00:00"; 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_REPORTS = "dev-reports";
private static final String STORED_LOGCAT = "dev-logcat";
public static Collection<String> getSupportedArchitectures() { public static Collection<String> getSupportedArchitectures() {
List<String> abis = new ArrayList<>(); List<String> abis = new ArrayList<>();
@@ -107,6 +108,10 @@ public class AndroidUtils {
return ctx.getDir(STORED_REPORTS, MODE_PRIVATE); 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. * Returns an array of supported content types for image attachments.
* GIFs can't be compressed on API < 24 so they're not supported. * GIFs can't be compressed on API < 24 so they're not supported.

View File

@@ -19,4 +19,10 @@ public interface StreamDecrypterFactory {
*/ */
StreamDecrypter createContactExchangeStreamDecrypter(InputStream in, StreamDecrypter createContactExchangeStreamDecrypter(InputStream in,
SecretKey headerKey); SecretKey headerKey);
/**
* Creates a {@link StreamDecrypter} for decrypting a log stream.
*/
StreamDecrypter createLogStreamDecrypter(InputStream in,
SecretKey headerKey);
} }

View File

@@ -17,6 +17,12 @@ public interface StreamEncrypterFactory {
* Creates a {@link StreamEncrypter} for encrypting a contact exchange * Creates a {@link StreamEncrypter} for encrypting a contact exchange
* stream. * 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); SecretKey headerKey);
} }

View File

@@ -13,4 +13,6 @@ public interface DevConfig {
String getDevOnionAddress(); String getDevOnionAddress();
File getReportDir(); File getReportDir();
File getLogcatFile();
} }

View File

@@ -16,8 +16,13 @@ public interface StreamReaderFactory {
/** /**
* Creates an {@link InputStream InputStream} for reading from a contact * Creates an {@link InputStream InputStream} for reading from a contact
* exchangestream. * exchange stream.
*/ */
InputStream createContactExchangeStreamReader(InputStream in, InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey); SecretKey headerKey);
/**
* Creates an {@link InputStream} for reading from a log stream.
*/
InputStream createLogStreamReader(InputStream in, SecretKey headerKey);
} }

View File

@@ -7,17 +7,19 @@ import java.io.OutputStream;
@NotNullByDefault @NotNullByDefault
public interface StreamWriterFactory { public interface StreamWriterFactory {
/** /**
* Creates an {@link OutputStream OutputStream} for writing to a * Creates a {@link StreamWriter} for writing to a transport stream.
* transport stream
*/ */
StreamWriter createStreamWriter(OutputStream out, StreamContext ctx); StreamWriter createStreamWriter(OutputStream out, StreamContext ctx);
/** /**
* Creates an {@link OutputStream OutputStream} for writing to a contact * Creates a {@link StreamWriter} for writing to a contact exchange stream.
* exchange stream.
*/ */
StreamWriter createContactExchangeStreamWriter(OutputStream out, StreamWriter createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey); SecretKey headerKey);
/**
* Creates a {@link StreamWriter} for writing to a log stream.
*/
StreamWriter createLogStreamWriter(OutputStream out, SecretKey headerKey);
} }

View File

@@ -9,14 +9,16 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static java.lang.Math.min;
import static java.util.logging.Level.INFO; 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.logDuration;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
class ScryptKdf implements PasswordBasedKdf { class ScryptKdf implements PasswordBasedKdf {
private static final Logger LOG = 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 MIN_COST = 256; // Min parameter N
private static final int MAX_COST = 1024 * 1024; // Max parameter N private static final int MAX_COST = 1024 * 1024; // Max parameter N
@@ -33,10 +35,20 @@ class ScryptKdf implements PasswordBasedKdf {
@Override @Override
public int chooseCostParameter() { 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 // Increase the cost from min to max while measuring performance
int cost = MIN_COST; 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; cost *= 2;
}
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("KDF cost parameter " + cost); LOG.info("KDF cost parameter " + cost);
return cost; return cost;

View File

@@ -36,4 +36,10 @@ class StreamDecrypterFactoryImpl implements StreamDecrypterFactory {
SecretKey headerKey) { SecretKey headerKey) {
return new StreamDecrypterImpl(in, cipherProvider.get(), 0, headerKey); return new StreamDecrypterImpl(in, cipherProvider.get(), 0, headerKey);
} }
@Override
public StreamDecrypter createLogStreamDecrypter(InputStream in,
SecretKey headerKey) {
return createContactExchangeStreamDecrypter(in, headerKey);
}
} }

View File

@@ -51,7 +51,7 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
} }
@Override @Override
public StreamEncrypter createContactExchangeStreamDecrypter( public StreamEncrypter createContactExchangeStreamEncrypter(
OutputStream out, SecretKey headerKey) { OutputStream out, SecretKey headerKey) {
AuthenticatedCipher cipher = cipherProvider.get(); AuthenticatedCipher cipher = cipherProvider.get();
byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH]; byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH];
@@ -60,4 +60,10 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
return new StreamEncrypterImpl(out, cipher, 0, null, streamHeaderNonce, return new StreamEncrypterImpl(out, cipher, 0, null, streamHeaderNonce,
headerKey, frameKey); headerKey, frameKey);
} }
@Override
public StreamEncrypter createLogStreamEncrypter(OutputStream out,
SecretKey headerKey) {
return createContactExchangeStreamEncrypter(out, headerKey);
}
} }

View File

@@ -24,15 +24,21 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
@Override @Override
public InputStream createStreamReader(InputStream in, StreamContext ctx) { public InputStream createStreamReader(InputStream in, StreamContext ctx) {
return new StreamReaderImpl( return new StreamReaderImpl(streamDecrypterFactory
streamDecrypterFactory.createStreamDecrypter(in, ctx)); .createStreamDecrypter(in, ctx));
} }
@Override @Override
public InputStream createContactExchangeStreamReader(InputStream in, public InputStream createContactExchangeStreamReader(InputStream in,
SecretKey headerKey) { SecretKey headerKey) {
return new StreamReaderImpl( return new StreamReaderImpl(streamDecrypterFactory
streamDecrypterFactory.createContactExchangeStreamDecrypter(in, .createContactExchangeStreamDecrypter(in, headerKey));
headerKey)); }
@Override
public InputStream createLogStreamReader(InputStream in,
SecretKey headerKey) {
return new StreamReaderImpl(streamDecrypterFactory
.createLogStreamDecrypter(in, headerKey));
} }
} }

View File

@@ -26,15 +26,21 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
@Override @Override
public StreamWriter createStreamWriter(OutputStream out, public StreamWriter createStreamWriter(OutputStream out,
StreamContext ctx) { StreamContext ctx) {
return new StreamWriterImpl( return new StreamWriterImpl(streamEncrypterFactory
streamEncrypterFactory.createStreamEncrypter(out, ctx)); .createStreamEncrypter(out, ctx));
} }
@Override @Override
public StreamWriter createContactExchangeStreamWriter(OutputStream out, public StreamWriter createContactExchangeStreamWriter(OutputStream out,
SecretKey headerKey) { SecretKey headerKey) {
return new StreamWriterImpl( return new StreamWriterImpl(streamEncrypterFactory
streamEncrypterFactory.createContactExchangeStreamDecrypter(out, .createContactExchangeStreamEncrypter(out, headerKey));
headerKey));
} }
}
@Override
public StreamWriter createLogStreamWriter(OutputStream out,
SecretKey headerKey) {
return new StreamWriterImpl(streamEncrypterFactory
.createLogStreamEncrypter(out, headerKey));
}
}

View File

@@ -53,7 +53,7 @@ public class BridgeTest extends BrambleTestCase {
return component.getCircumventionProvider().getBridges(false); 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()); private final static Logger LOG = getLogger(BridgeTest.class.getName());

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M16,17V14H9V10H16V7L21,12L16,17M14,2A2,2 0 0,1 16,4V6H14V4H5V20H14V18H16V20A2,2 0 0,1 14,22H5A2,2 0 0,1 3,20V4A2,2 0 0,1 5,2H14Z" /></svg>

After

Width:  |  Height:  |  Size: 423 B

View File

@@ -22,9 +22,11 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 29 targetSdkVersion 29
versionCode 10213 versionCode 10214
versionName "1.2.13" versionName "1.2.14"
applicationId "org.briarproject.briar.android" applicationId "org.briarproject.briar.android"
vectorDrawables.useSupportLibrary = true
buildConfigField "String", "GitHash", buildConfigField "String", "GitHash",
"\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\"" "\"${getStdout(['git', 'rev-parse', '--short=7', 'HEAD'], 'No commit hash')}\""
def now = (long) (System.currentTimeMillis() / 1000) def now = (long) (System.currentTimeMillis() / 1000)
@@ -134,6 +136,7 @@ dependencies {
testImplementation "org.jmock:jmock:$jmockVersion" testImplementation "org.jmock:jmock:$jmockVersion"
testImplementation "org.jmock:jmock-junit4:$jmockVersion" testImplementation "org.jmock:jmock-junit4:$jmockVersion"
testImplementation "org.jmock:jmock-legacy:$jmockVersion" testImplementation "org.jmock:jmock-legacy:$jmockVersion"
testAnnotationProcessor "com.google.dagger:dagger-compiler:2.24"
androidTestImplementation project(path: ':bramble-api', configuration: 'testOutput') androidTestImplementation project(path: ':bramble-api', configuration: 'testOutput')
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2'

View File

@@ -25,7 +25,7 @@ class BriarAccountManager extends AndroidAccountManager {
public void deleteAccount() { public void deleteAccount() {
synchronized (stateChangeLock) { synchronized (stateChangeLock) {
super.deleteAccount(); super.deleteAccount();
Localizer.reinitialize(); Localizer.reinitialize(appContext);
UiUtils.setTheme(appContext, UiUtils.setTheme(appContext,
appContext.getString(R.string.pref_theme_light_value)); appContext.getString(R.string.pref_theme_light_value));
} }

View File

@@ -34,6 +34,7 @@ import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.attachment.AttachmentModule; import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule; import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.conversation.glide.BriarModelLoader; 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.login.SignInReminderReceiver;
import org.briarproject.briar.android.view.EmojiTextInputView; import org.briarproject.briar.android.view.EmojiTextInputView;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -179,6 +180,10 @@ public interface AndroidComponent
AndroidWakeLockManager wakeLockManager(); AndroidWakeLockManager wakeLockManager();
CachingLogHandler logHandler();
Thread.UncaughtExceptionHandler exceptionHandler();
void inject(SignInReminderReceiver briarService); void inject(SignInReminderReceiver briarService);
void inject(BriarService briarService); void inject(BriarService briarService);

View File

@@ -255,7 +255,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
R.string.ongoing_notification_title; R.string.ongoing_notification_title;
int text = locked ? R.string.lock_tap_to_unlock : int text = locked ? R.string.lock_tap_to_unlock :
R.string.ongoing_notification_text; R.string.ongoing_notification_text;
int icon = locked ? R.drawable.startup_lock : int icon = locked ? R.drawable.notification_lock :
R.drawable.notification_ongoing; R.drawable.notification_ongoing;
// Ongoing foreground notification that shows BriarService is running // Ongoing foreground notification that shows BriarService is running
NotificationCompat.Builder b = NotificationCompat.Builder b =
@@ -579,6 +579,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@UiThread @UiThread
private void updateContactAddedNotification() { private void updateContactAddedNotification() {
if (contactAddedTotal == 0) return;
BriarNotificationBuilder b = BriarNotificationBuilder b =
new BriarNotificationBuilder(appContext, CONTACT_CHANNEL_ID); new BriarNotificationBuilder(appContext, CONTACT_CHANNEL_ID);
b.setSmallIcon(R.drawable.notification_contact_added); b.setSmallIcon(R.drawable.notification_contact_added);
@@ -624,7 +625,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
NotificationCompat.Builder b = NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext, REMINDER_CHANNEL_ID); new NotificationCompat.Builder(appContext, REMINDER_CHANNEL_ID);
b.setSmallIcon(R.drawable.ic_signout); b.setSmallIcon(R.drawable.notification_signout);
b.setColor(getColor(appContext, R.color.briar_primary)); b.setColor(getColor(appContext, R.color.briar_primary));
b.setContentTitle( b.setContentTitle(
appContext.getText(R.string.reminder_notification_title)); appContext.getText(R.string.reminder_notification_title));
@@ -713,4 +714,16 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
public void unblockAllBlogPostNotifications() { public void unblockAllBlogPostNotifications() {
androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = false); androidExecutor.runOnUiThread((Runnable) () -> blockBlogs = false);
} }
@Override
public void restartNotifications(boolean locked, boolean mayAlertAgain) {
androidExecutor.runOnUiThread(() -> {
updateForegroundNotification(locked);
updateContactNotification(mayAlertAgain);
updateBlogPostNotification(mayAlertAgain);
updateForumPostNotification(mayAlertAgain);
updateGroupMessageNotification(mayAlertAgain);
updateContactAddedNotification();
});
}
} }

View File

@@ -33,11 +33,14 @@ import org.briarproject.briar.android.account.SetupModule;
import org.briarproject.briar.android.contact.ContactListModule; import org.briarproject.briar.android.contact.ContactListModule;
import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.forum.ForumModule;
import org.briarproject.briar.android.keyagreement.ContactExchangeModule; 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.login.LoginModule;
import org.briarproject.briar.android.navdrawer.NavDrawerModule; 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.privategroup.list.GroupListModule;
import org.briarproject.briar.android.reporting.DevReportModule; 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.test.TestAvatarCreatorImpl;
import org.briarproject.briar.android.viewmodel.ViewModelModule; import org.briarproject.briar.android.viewmodel.ViewModelModule;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
@@ -72,6 +75,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
SetupModule.class, SetupModule.class,
DozeHelperModule.class, DozeHelperModule.class,
ContactExchangeModule.class, ContactExchangeModule.class,
LoggingModule.class,
LoginModule.class, LoginModule.class,
NavDrawerModule.class, NavDrawerModule.class,
ViewModelModule.class, ViewModelModule.class,
@@ -79,8 +83,10 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD;
DevReportModule.class, DevReportModule.class,
ContactListModule.class, ContactListModule.class,
// below need to be within same scope as ViewModelProvider.Factory // below need to be within same scope as ViewModelProvider.Factory
ForumModule.BindsModule.class, ForumModule.class,
GroupListModule.class, GroupListModule.class,
GroupConversationModule.class,
SharingModule.class,
}) })
public class AppModule { public class AppModule {
@@ -188,6 +194,11 @@ public class AppModule {
public File getReportDir() { public File getReportDir() {
return AndroidUtils.getReportDir(app.getApplicationContext()); return AndroidUtils.getReportDir(app.getApplicationContext());
} }
@Override
public File getLogcatFile() {
return AndroidUtils.getLogcatFile(app.getApplicationContext());
}
}; };
return devConfig; return devConfig;
} }

View File

@@ -6,9 +6,6 @@ import android.content.SharedPreferences;
import org.briarproject.bramble.BrambleApplication; import org.briarproject.bramble.BrambleApplication;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity; 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 * This exists so that the Application object will not necessarily be cast
* directly to the Briar application object. * directly to the Briar application object.
@@ -17,8 +14,6 @@ public interface BriarApplication extends BrambleApplication {
Class<? extends Activity> ENTRY_ACTIVITY = NavDrawerActivity.class; Class<? extends Activity> ENTRY_ACTIVITY = NavDrawerActivity.class;
Collection<LogRecord> getRecentLogRecords();
AndroidComponent getApplicationComponent(); AndroidComponent getApplicationComponent();
SharedPreferences getDefaultSharedPreferences(); SharedPreferences getDefaultSharedPreferences();

View File

@@ -20,12 +20,10 @@ import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.BriarCoreEagerSingletons;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.logging.CachingLogHandler; import org.briarproject.briar.android.logging.CachingLogHandler;
import org.briarproject.briar.android.reporting.BriarExceptionHandler;
import org.briarproject.briar.android.util.UiUtils; 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.Handler;
import java.util.logging.LogRecord;
import java.util.logging.Logger; import java.util.logging.Logger;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -42,16 +40,11 @@ public class BriarApplicationImpl extends Application
private static final Logger LOG = private static final Logger LOG =
getLogger(BriarApplicationImpl.class.getName()); getLogger(BriarApplicationImpl.class.getName());
private final CachingLogHandler logHandler = new CachingLogHandler();
private final BriarExceptionHandler exceptionHandler =
new BriarExceptionHandler(this);
private AndroidComponent applicationComponent; private AndroidComponent applicationComponent;
private volatile SharedPreferences prefs; private volatile SharedPreferences prefs;
@Override @Override
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
if (prefs == null) if (prefs == null)
prefs = PreferenceManager.getDefaultSharedPreferences(base); prefs = PreferenceManager.getDefaultSharedPreferences(base);
// Loading the language needs to be done here. // Loading the language needs to be done here.
@@ -67,6 +60,11 @@ public class BriarApplicationImpl extends Application
if (IS_DEBUG_BUILD) enableStrictMode(); if (IS_DEBUG_BUILD) enableStrictMode();
applicationComponent = createApplicationComponent();
UncaughtExceptionHandler exceptionHandler =
applicationComponent.exceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(exceptionHandler);
Logger rootLogger = getLogger(""); Logger rootLogger = getLogger("");
Handler[] handlers = rootLogger.getHandlers(); Handler[] handlers = rootLogger.getHandlers();
// Disable the Android logger for release builds // Disable the Android logger for release builds
@@ -78,12 +76,13 @@ public class BriarApplicationImpl extends Application
// Restore the default handlers after the level raising handler // Restore the default handlers after the level raising handler
for (Handler handler : handlers) rootLogger.addHandler(handler); for (Handler handler : handlers) rootLogger.addHandler(handler);
} }
CachingLogHandler logHandler = applicationComponent.logHandler();
rootLogger.addHandler(logHandler); rootLogger.addHandler(logHandler);
rootLogger.setLevel(IS_DEBUG_BUILD ? FINE : INFO); rootLogger.setLevel(IS_DEBUG_BUILD ? FINE : INFO);
LOG.info("Created"); LOG.info("Created");
Localizer.getInstance().setLocaleLegacy(this);
applicationComponent = createApplicationComponent();
EmojiManager.install(new GoogleEmojiProvider()); EmojiManager.install(new GoogleEmojiProvider());
} }
@@ -106,7 +105,9 @@ public class BriarApplicationImpl extends Application
@Override @Override
public void onConfigurationChanged(@NonNull Configuration newConfig) { public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
Localizer.getInstance().setLocale(this); Localizer.getInstance().applicationConfigurationChanged(this, newConfig,
applicationComponent.androidNotificationManager(),
applicationComponent.lockManager().isLocked());
} }
private void setTheme(Context ctx, SharedPreferences prefs) { private void setTheme(Context ctx, SharedPreferences prefs) {
@@ -136,11 +137,6 @@ public class BriarApplicationImpl extends Application
return applicationComponent; return applicationComponent;
} }
@Override
public Collection<LogRecord> getRecentLogRecords() {
return logHandler.getRecentLogRecords();
}
@Override @Override
public AndroidComponent getApplicationComponent() { public AndroidComponent getApplicationComponent() {
return applicationComponent; return applicationComponent;

View File

@@ -6,12 +6,16 @@ import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import java.util.Locale; import java.util.Locale;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import androidx.core.text.TextUtilsCompat;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE; import static org.briarproject.briar.android.settings.SettingsFragment.LANGUAGE;
@NotNullByDefault @NotNullByDefault
@@ -21,6 +25,7 @@ public class Localizer {
@Nullable @Nullable
private static Localizer INSTANCE; private static Localizer INSTANCE;
private final Locale systemLocale; private final Locale systemLocale;
@Nullable
private final Locale locale; private final Locale locale;
private Localizer(SharedPreferences sharedPreferences) { private Localizer(SharedPreferences sharedPreferences) {
@@ -30,10 +35,17 @@ public class Localizer {
private Localizer(Locale systemLocale, @Nullable Locale userLocale) { private Localizer(Locale systemLocale, @Nullable Locale userLocale) {
this.systemLocale = systemLocale; this.systemLocale = systemLocale;
if (userLocale == null) locale = systemLocale; locale = userLocale;
else locale = userLocale; setLocaleAndSystemConfiguration(locale);
} }
private Localizer(Locale systemLocale) {
this.systemLocale = systemLocale;
locale = null;
setLocaleAndSystemConfiguration(systemLocale);
}
// Instantiate the Localizer. // Instantiate the Localizer.
public static synchronized void initialize(SharedPreferences prefs) { public static synchronized void initialize(SharedPreferences prefs) {
if (INSTANCE == null) if (INSTANCE == null)
@@ -41,9 +53,11 @@ public class Localizer {
} }
// Reinstantiate the Localizer with the system locale // Reinstantiate the Localizer with the system locale
public static synchronized void reinitialize() { public static synchronized void reinitialize(Context appContext) {
if (INSTANCE != null) if (INSTANCE != null && INSTANCE.locale != null) {
INSTANCE = new Localizer(INSTANCE.systemLocale, null); INSTANCE = new Localizer(INSTANCE.systemLocale);
INSTANCE.forceLocale(appContext, INSTANCE.systemLocale);
}
} }
// Get the current instance. // Get the current instance.
@@ -64,29 +78,98 @@ public class Localizer {
if (tag.contains("-")) { if (tag.contains("-")) {
String[] langArray = tag.split("-"); String[] langArray = tag.split("-");
return new Locale(langArray[0], langArray[1]); return new Locale(langArray[0], langArray[1]);
} else } else {
return new Locale(tag); return new Locale(tag);
}
} }
// Returns the localized version of context // Returns the localized version of context
public Context setLocale(Context context) { public Context setLocale(Context context) {
if (locale == null || SDK_INT < 17) return context;
Resources res = context.getResources(); Resources res = context.getResources();
Configuration conf = res.getConfiguration(); Configuration conf = res.getConfiguration();
Locale currentLocale; updateConfiguration(conf, locale);
if (SDK_INT >= 24) { return context.createConfigurationContext(conf);
currentLocale = conf.getLocales().get(0); }
} else
currentLocale = conf.locale; // For API < 17 only.
if (locale.equals(currentLocale)) public void setLocaleLegacy(Context appContext) {
return context; if (SDK_INT >= 17 || locale == null) return;
Locale.setDefault(locale); forceLocale(appContext, locale);
}
// Forces the update of the resources through the deprecated API.
private void forceLocale(Context context, Locale locale) {
Resources res = context.getResources();
Configuration conf = res.getConfiguration();
updateConfiguration(conf, locale);
res.updateConfiguration(conf, res.getDisplayMetrics());
}
private void updateConfiguration(Configuration conf, Locale locale) {
if (SDK_INT >= 17) { if (SDK_INT >= 17) {
conf.setLocale(locale); conf.setLocale(locale);
context.createConfigurationContext(conf); } else {
} else
conf.locale = locale; conf.locale = locale;
//noinspection deprecation }
res.updateConfiguration(conf, res.getDisplayMetrics());
return context;
} }
private void setLocaleAndSystemConfiguration(@Nullable Locale locale) {
if (locale == null) return;
Locale.setDefault(locale);
if (SDK_INT >= 23) return;
Resources systemResources = Resources.getSystem();
Configuration systemConfiguration = systemResources.getConfiguration();
updateConfiguration(systemConfiguration, locale);
// DateUtils uses the system resources, so we need to update them too.
//noinspection deprecation
systemResources.updateConfiguration(systemConfiguration,
systemResources.getDisplayMetrics());
}
private Locale getLocaleFromConfig(Configuration config) {
if (SDK_INT >= 24) {
return config.getLocales().get(0);
} else {
//noinspection deprecation
return config.locale;
}
}
public void applicationConfigurationChanged(Context appContext,
Configuration newConfig,
AndroidNotificationManager androidNotificationManager,
boolean locked) {
Locale newLocale = getLocaleFromConfig(newConfig);
if (locale == null && newLocale != systemLocale) {
androidNotificationManager.restartNotifications(locked, false);
}
if (newLocale == locale) return;
setLocaleAndSystemConfiguration(locale);
if (SDK_INT < 17) setLocaleLegacy(appContext);
}
/**
* Indicates whether the language represented by locale
* should be offered to the user on this device.
* * Android doesn't pick up Asturian on API < 21
* * RTL languages are supported since API >= 17
*/
public static boolean isLocaleSupported(Locale locale) {
if (SDK_INT >= 21) return true;
if (locale.getLanguage().equals("ast")) return false;
if (SDK_INT >= 17) return true;
return isLeftToRight(locale);
}
// Exclude RTL locales on API < 17, they won't be laid out correctly
private static boolean isLeftToRight(Locale locale) {
// TextUtilsCompat returns the wrong direction for Hebrew on some phones
String language = locale.getLanguage();
if (language.equals("iw") || language.equals("he")) return false;
int direction =
TextUtilsCompat.getLayoutDirectionFromLocale(locale);
return direction == LAYOUT_DIRECTION_LTR;
}
} }

View File

@@ -32,7 +32,6 @@ import org.briarproject.briar.android.conversation.ImageFragment;
import org.briarproject.briar.android.forum.CreateForumActivity; import org.briarproject.briar.android.forum.CreateForumActivity;
import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumActivity;
import org.briarproject.briar.android.forum.ForumListFragment; 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.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.introduction.ContactChooserFragment; import org.briarproject.briar.android.introduction.ContactChooserFragment;
import org.briarproject.briar.android.introduction.IntroductionActivity; 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.PanicPreferencesActivity;
import org.briarproject.briar.android.panic.PanicResponderActivity; import org.briarproject.briar.android.panic.PanicResponderActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity; 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.CreateGroupActivity;
import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment; import org.briarproject.briar.android.privategroup.creation.CreateGroupFragment;
import org.briarproject.briar.android.privategroup.creation.CreateGroupModule; import org.briarproject.briar.android.privategroup.creation.CreateGroupModule;
@@ -89,12 +87,10 @@ import dagger.Component;
ActivityModule.class, ActivityModule.class,
BlogModule.class, BlogModule.class,
CreateGroupModule.class, CreateGroupModule.class,
ForumModule.class,
GroupInvitationModule.class, GroupInvitationModule.class,
GroupConversationModule.class,
GroupMemberModule.class, GroupMemberModule.class,
GroupRevealModule.class, GroupRevealModule.class,
SharingModule.class SharingModule.SharingLegacyModule.class
}, dependencies = AndroidComponent.class) }, dependencies = AndroidComponent.class)
public interface ActivityComponent { public interface ActivityComponent {

View File

@@ -14,7 +14,6 @@ import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.DestroyableContext; import org.briarproject.briar.android.DestroyableContext;
import org.briarproject.briar.android.Localizer; import org.briarproject.briar.android.Localizer;
import org.briarproject.briar.android.controller.ActivityLifecycleController; 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.BaseFragment;
import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
@@ -87,7 +86,6 @@ public abstract class BaseActivity extends AppCompatActivity
activityComponent = DaggerActivityComponent.builder() activityComponent = DaggerActivityComponent.builder()
.androidComponent(applicationComponent) .androidComponent(applicationComponent)
.activityModule(getActivityModule()) .activityModule(getActivityModule())
.forumModule(getForumModule())
.build(); .build();
injectActivity(activityComponent); injectActivity(activityComponent);
super.onCreate(state); super.onCreate(state);
@@ -122,11 +120,6 @@ public abstract class BaseActivity extends AppCompatActivity
return new ActivityModule(this); return new ActivityModule(this);
} }
// TODO use a test module where this is used in tests
protected ForumModule getForumModule() {
return new ForumModule();
}
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();

View File

@@ -160,7 +160,6 @@ public abstract class BriarActivity extends BaseActivity {
* @param ownLayout true if the custom toolbar brings its own layout * @param ownLayout true if the custom toolbar brings its own layout
* @return the Toolbar object or null if content view did not contain one * @return the Toolbar object or null if content view did not contain one
*/ */
@Nullable
protected Toolbar setUpCustomToolbar(boolean ownLayout) { protected Toolbar setUpCustomToolbar(boolean ownLayout) {
// Custom Toolbar // Custom Toolbar
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);

View File

@@ -124,7 +124,7 @@ public class AddContactViewModel extends DbViewModel {
return addContactResult; return addContactResult;
} }
public void updatePendingContact(String name, PendingContact p) { void updatePendingContact(String name, PendingContact p) {
runOnDbThread(() -> { runOnDbThread(() -> {
try { try {
contactManager.removePendingContact(p.getId()); contactManager.removePendingContact(p.getId());

View File

@@ -29,11 +29,11 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.app.AlertDialog.Builder;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders;
import static android.view.View.INVISIBLE; import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE; import static android.view.View.VISIBLE;
@@ -48,6 +48,7 @@ import static org.briarproject.briar.android.util.UiUtils.getDialogIcon;
public class NicknameFragment extends BaseFragment { public class NicknameFragment extends BaseFragment {
private static final String TAG = NicknameFragment.class.getName(); private static final String TAG = NicknameFragment.class.getName();
private static final String SAVED_LINK = "savedLink";
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@@ -67,6 +68,20 @@ public class NicknameFragment extends BaseFragment {
@Override @Override
public void injectFragment(ActivityComponent component) { public void injectFragment(ActivityComponent component) {
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(requireActivity(), viewModelFactory)
.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 @Nullable
@@ -74,14 +89,9 @@ public class NicknameFragment extends BaseFragment {
public View onCreateView(LayoutInflater inflater, public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) { @Nullable Bundle savedInstanceState) {
if (getActivity() == null || getContext() == null) return null;
View v = inflater.inflate(R.layout.fragment_nickname, View v = inflater.inflate(R.layout.fragment_nickname,
container, false); container, false);
viewModel = ViewModelProviders.of(getActivity(), viewModelFactory)
.get(AddContactViewModel.class);
contactNameLayout = v.findViewById(R.id.contactNameLayout); contactNameLayout = v.findViewById(R.id.contactNameLayout);
contactNameInput = v.findViewById(R.id.contactNameInput); contactNameInput = v.findViewById(R.id.contactNameInput);
@@ -93,6 +103,12 @@ public class NicknameFragment extends BaseFragment {
return v; return v;
} }
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(SAVED_LINK, viewModel.getRemoteHandshakeLink());
}
@Nullable @Nullable
private String getNicknameOrNull() { private String getNicknameOrNull() {
Editable text = contactNameInput.getText(); Editable text = contactNameInput.getText();

View File

@@ -7,6 +7,7 @@ import java.util.Collection;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
@Deprecated
@NotNullByDefault @NotNullByDefault
public interface SharingController { public interface SharingController {

View File

@@ -18,6 +18,7 @@ import javax.inject.Inject;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
@Deprecated
@NotNullByDefault @NotNullByDefault
public class SharingControllerImpl implements SharingController, EventListener { public class SharingControllerImpl implements SharingController, EventListener {

View File

@@ -3,6 +3,7 @@ package org.briarproject.briar.android.conversation;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.transition.Slide; import android.transition.Slide;
@@ -105,6 +106,7 @@ import androidx.recyclerview.selection.SelectionTracker.SelectionObserver;
import androidx.recyclerview.selection.StorageStrategy; import androidx.recyclerview.selection.StorageStrategy;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
@@ -479,12 +481,10 @@ public class ConversationActivity extends BriarActivity
@UiThread @UiThread
private void displayContactOnlineStatus() { private void displayContactOnlineStatus() {
if (connectionRegistry.isConnected(contactId)) { if (connectionRegistry.isConnected(contactId)) {
toolbarStatus.setImageDrawable(ContextCompat.getDrawable( toolbarStatus.setImageResource(R.drawable.contact_online);
ConversationActivity.this, R.drawable.contact_online));
toolbarStatus.setContentDescription(getString(R.string.online)); toolbarStatus.setContentDescription(getString(R.string.online));
} else { } else {
toolbarStatus.setImageDrawable(ContextCompat.getDrawable( toolbarStatus.setImageResource(R.drawable.contact_offline);
ConversationActivity.this, R.drawable.contact_offline));
toolbarStatus.setContentDescription(getString(R.string.offline)); toolbarStatus.setContentDescription(getString(R.string.offline));
} }
} }
@@ -935,13 +935,16 @@ public class ConversationActivity extends BriarActivity
return; return;
} }
int color =
ContextCompat.getColor(this, R.color.briar_primary);
Drawable drawable = VectorDrawableCompat
.create(getResources(), R.drawable.ic_more_vert_accent, null);
new MaterialTapTargetPrompt.Builder(ConversationActivity.this, new MaterialTapTargetPrompt.Builder(ConversationActivity.this,
R.style.OnboardingDialogTheme).setTarget(target) R.style.OnboardingDialogTheme).setTarget(target)
.setPrimaryText(R.string.introduction_onboarding_title) .setPrimaryText(R.string.introduction_onboarding_title)
.setSecondaryText(R.string.introduction_onboarding_text) .setSecondaryText(R.string.introduction_onboarding_text)
.setIcon(R.drawable.ic_more_vert_accent) .setIconDrawable(drawable)
.setBackgroundColour( .setBackgroundColour(color)
ContextCompat.getColor(this, R.color.briar_primary))
.show(); .show();
} }

View File

@@ -6,52 +6,54 @@ import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.forum.ForumController.ForumListener;
import org.briarproject.briar.android.sharing.ForumSharingStatusActivity; import org.briarproject.briar.android.sharing.ForumSharingStatusActivity;
import org.briarproject.briar.android.sharing.ShareForumActivity; import org.briarproject.briar.android.sharing.ShareForumActivity;
import org.briarproject.briar.android.threaded.ThreadItemAdapter; import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadListActivity; import org.briarproject.briar.android.threaded.ThreadListActivity;
import org.briarproject.briar.android.threaded.ThreadListController; import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.api.forum.Forum;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; 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.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.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; import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_POST_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ForumActivity extends public class ForumActivity extends
ThreadListActivity<Forum, ForumPostItem, ThreadItemAdapter<ForumPostItem>> ThreadListActivity<ForumPostItem, ThreadItemAdapter<ForumPostItem>> {
implements ForumListener {
@Inject @Inject
ForumController forumController; ViewModelProvider.Factory viewModelFactory;
private ForumViewModel viewModel;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(ForumViewModel.class);
} }
@Override @Override
protected ThreadListController<Forum, ForumPostItem> getController() { protected ThreadListViewModel<ForumPostItem> getViewModel() {
return forumController; return viewModel;
}
@Override
protected ThreadItemAdapter<ForumPostItem> createAdapter() {
return new ThreadItemAdapter<>(this);
} }
@Override @Override
@@ -59,36 +61,27 @@ public class ForumActivity extends
super.onCreate(state); super.onCreate(state);
Toolbar toolbar = setUpCustomToolbar(false); 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 // Open member list on Toolbar click
if (toolbar != null) { toolbar.setOnClickListener(v -> {
toolbar.setOnClickListener(v -> { Intent i = new Intent(ForumActivity.this,
Intent i1 = new Intent(ForumActivity.this, ForumSharingStatusActivity.class);
ForumSharingStatusActivity.class); i.putExtra(GROUP_ID, groupId.getBytes());
i1.putExtra(GROUP_ID, groupId.getBytes()); startActivity(i);
startActivity(i1); });
});
String groupName = getIntent().getStringExtra(GROUP_NAME);
if (groupName != null) {
setTitle(groupName);
} else {
observeOnce(viewModel.loadForum(), this, forum ->
setTitle(forum.getName())
);
} }
} }
@Override @Override
protected void onNamedGroupLoaded(Forum forum) { protected void onActivityResult(int request, int result,
setTitle(forum.getName()); @Nullable Intent data) {
}
@Override
protected ThreadItemAdapter<ForumPostItem> createAdapter(
LinearLayoutManager layoutManager) {
return new ThreadItemAdapter<>(this, layoutManager);
}
@Override
protected void onActivityResult(int request, int result, Intent data) {
super.onActivityResult(request, result, data); super.onActivityResult(request, result, data);
if (request == REQUEST_SHARE_FORUM && result == RESULT_OK) { 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 // Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.forum_actions, menu); inflater.inflate(R.menu.forum_actions, menu);
super.onCreateOptionsMenu(menu);
return super.onCreateOptionsMenu(menu); return true;
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
// Handle presses on the action bar items // Handle presses on the action bar items
switch (item.getItemId()) { int itemId = item.getItemId();
case R.id.action_forum_share: if (itemId == R.id.action_forum_share) {
Intent i2 = new Intent(this, ShareForumActivity.class); Intent i = new Intent(this, ShareForumActivity.class);
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i2.putExtra(GROUP_ID, groupId.getBytes()); i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i2, REQUEST_SHARE_FORUM); startActivityForResult(i, REQUEST_SHARE_FORUM);
return true; return true;
case R.id.action_forum_sharing_status: } else if (itemId == R.id.action_forum_sharing_status) {
Intent i3 = new Intent(this, ForumSharingStatusActivity.class); Intent i = new Intent(this, ForumSharingStatusActivity.class);
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP); i.setFlags(FLAG_ACTIVITY_CLEAR_TOP);
i3.putExtra(GROUP_ID, groupId.getBytes()); i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i3); startActivity(i);
return true; return true;
case R.id.action_forum_delete: } else if (itemId == R.id.action_forum_delete) {
showUnsubscribeDialog(); showUnsubscribeDialog();
return true; return true;
default:
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
@Override @Override
@@ -135,7 +127,7 @@ public class ForumActivity extends
} }
private void showUnsubscribeDialog() { private void showUnsubscribeDialog() {
OnClickListener okListener = (dialog, which) -> deleteForum(); OnClickListener okListener = (dialog, which) -> viewModel.deleteForum();
AlertDialog.Builder builder = new AlertDialog.Builder(this, AlertDialog.Builder builder = new AlertDialog.Builder(this,
R.style.BriarDialogTheme); R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.dialog_title_leave_forum)); builder.setTitle(getString(R.string.dialog_title_leave_forum));
@@ -145,27 +137,4 @@ public class ForumActivity extends
builder.show(); 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());
}
} }

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -1,32 +1,23 @@
package org.briarproject.briar.android.forum; 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 org.briarproject.briar.android.viewmodel.ViewModelKey;
import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModel;
import dagger.Binds; import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
@Module @Module
public class ForumModule { public interface ForumModule {
@Module @Binds
public interface BindsModule { @IntoMap
@Binds @ViewModelKey(ForumListViewModel.class)
@IntoMap ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
@ViewModelKey(ForumListViewModel.class)
ViewModel bindForumListViewModel(ForumListViewModel forumListViewModel);
}
@ActivityScope @Binds
@Provides @IntoMap
ForumController provideForumController(BaseActivity activity, @ViewModelKey(ForumViewModel.class)
ForumControllerImpl forumController) { ViewModel bindForumViewModel(ForumViewModel forumViewModel);
activity.addLifecycleController(forumController);
return forumController;
}
} }

View File

@@ -6,7 +6,6 @@ import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.android.threaded.ThreadItem; import org.briarproject.briar.android.threaded.ThreadItem;
import org.briarproject.briar.api.forum.ForumPostHeader; import org.briarproject.briar.api.forum.ForumPostHeader;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.NotThreadSafe;
@NotThreadSafe @NotThreadSafe
@@ -17,9 +16,4 @@ class ForumPostItem extends ThreadItem {
h.getAuthorInfo(), h.isRead()); 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);
}
} }

View File

@@ -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();
}
}

View File

@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.logging.Formatter; import java.util.logging.Formatter;
@@ -17,6 +18,16 @@ import static java.util.Locale.US;
@NotNullByDefault @NotNullByDefault
public class BriefLogFormatter extends Formatter { 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 Object lock = new Object();
private final DateFormat dateFormat; // Locking: lock private final DateFormat dateFormat; // Locking: lock
private final Date date; // Locking: lock private final Date date; // Locking: lock

View File

@@ -21,6 +21,10 @@ public class CachingLogHandler extends Handler {
// Locking: lock // Locking: lock
private final Queue<LogRecord> recent = new LinkedList<>(); private final Queue<LogRecord> recent = new LinkedList<>();
// package-private constructor
CachingLogHandler() {
}
@Override @Override
public void publish(LogRecord record) { public void publish(LogRecord record) {
synchronized (lock) { synchronized (lock) {

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}

View File

@@ -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');
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,17 +1,13 @@
package org.briarproject.briar.android.login; package org.briarproject.briar.android.login;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.Drawable;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import static androidx.core.content.ContextCompat.getColor; import static org.briarproject.briar.android.util.UiUtils.getDialogIcon;
import static androidx.core.content.ContextCompat.getDrawable;
import static androidx.core.graphics.drawable.DrawableCompat.setTint;
import static java.util.Objects.requireNonNull;
@NotNullByDefault @NotNullByDefault
class LoginUtils { class LoginUtils {
@@ -19,9 +15,7 @@ class LoginUtils {
static AlertDialog createKeyStrengthenerErrorDialog(Context ctx) { static AlertDialog createKeyStrengthenerErrorDialog(Context ctx) {
AlertDialog.Builder builder = AlertDialog.Builder builder =
new AlertDialog.Builder(ctx, R.style.BriarDialogTheme); new AlertDialog.Builder(ctx, R.style.BriarDialogTheme);
Drawable icon = getDrawable(ctx, R.drawable.alerts_and_states_error); builder.setIcon(getDialogIcon(ctx, R.drawable.alerts_and_states_error));
setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary));
builder.setIcon(icon);
builder.setTitle(R.string.dialog_title_cannot_check_password); builder.setTitle(R.string.dialog_title_cannot_check_password);
builder.setMessage(R.string.dialog_message_cannot_check_password); builder.setMessage(R.string.dialog_message_cannot_check_password);
builder.setPositiveButton(R.string.ok, null); builder.setPositiveButton(R.string.ok, null);

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.navdrawer;
import android.content.Intent; import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -59,6 +60,7 @@ import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
import static android.view.View.GONE; import static android.view.View.GONE;
@@ -66,6 +68,7 @@ import static android.view.View.VISIBLE;
import static androidx.core.view.GravityCompat.START; import static androidx.core.view.GravityCompat.START;
import static androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED; import static androidx.drawerlayout.widget.DrawerLayout.LOCK_MODE_LOCKED_CLOSED;
import static androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE; 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.Objects.requireNonNull;
import static java.util.logging.Logger.getLogger; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING;
@@ -295,6 +298,12 @@ public class NavDrawerActivity extends BriarActivity implements
finish(); finish();
} else if (fm.getBackStackEntryCount() == 0 } else if (fm.getBackStackEntryCount() == 0
&& fm.findFragmentByTag(ContactListFragment.TAG) == null) { && 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 * This makes sure that the first fragment (ContactListFragment) the
* user sees is the same as the last fragment the user sees before * user sees is the same as the last fragment the user sees before
@@ -433,8 +442,7 @@ public class NavDrawerActivity extends BriarActivity implements
Transport t = getItem(position); Transport t = getItem(position);
ImageView icon = view.findViewById(R.id.imageView); ImageView icon = view.findViewById(R.id.imageView);
icon.setImageDrawable(ContextCompat.getDrawable( icon.setImageResource(t.iconDrawable);
NavDrawerActivity.this, t.iconDrawable));
icon.setColorFilter(ContextCompat.getColor( icon.setColorFilter(ContextCompat.getColor(
NavDrawerActivity.this, t.iconColor)); NavDrawerActivity.this, t.iconColor));
@@ -476,11 +484,13 @@ public class NavDrawerActivity extends BriarActivity implements
private void showTransportsOnboarding(boolean show, ImageView imageView) { private void showTransportsOnboarding(boolean show, ImageView imageView) {
if (show) { if (show) {
int color = resolveColorAttribute(this, R.attr.colorControlNormal); int color = resolveColorAttribute(this, R.attr.colorControlNormal);
Drawable drawable = VectorDrawableCompat
.create(getResources(), R.drawable.transport_tor, null);
new MaterialTapTargetPrompt.Builder(NavDrawerActivity.this, new MaterialTapTargetPrompt.Builder(NavDrawerActivity.this,
R.style.OnboardingDialogTheme).setTarget(imageView) R.style.OnboardingDialogTheme).setTarget(imageView)
.setPrimaryText(R.string.network_settings_title) .setPrimaryText(R.string.network_settings_title)
.setSecondaryText(R.string.transports_onboarding_text) .setSecondaryText(R.string.transports_onboarding_text)
.setIcon(R.drawable.transport_tor) .setIconDrawable(drawable)
.setIconDrawableColourFilter(color) .setIconDrawableColourFilter(color)
.setBackgroundColour( .setBackgroundColour(
ContextCompat.getColor(this, R.color.briar_primary)) ContextCompat.getColor(this, R.color.briar_primary))

View File

@@ -141,8 +141,7 @@ public class TransportsActivity extends BriarActivity {
Transport t = getItem(position); Transport t = getItem(position);
ImageView icon = view.findViewById(R.id.icon); ImageView icon = view.findViewById(R.id.icon);
icon.setImageDrawable(ContextCompat.getDrawable( icon.setImageResource(t.iconDrawable);
TransportsActivity.this, t.iconDrawable));
icon.setColorFilter(ContextCompat.getColor( icon.setColorFilter(ContextCompat.getColor(
TransportsActivity.this, t.iconColor)); TransportsActivity.this, t.iconColor));

View File

@@ -1,67 +1,60 @@
package org.briarproject.briar.android.privategroup.conversation; package org.briarproject.briar.android.privategroup.conversation;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.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.creation.GroupInviteActivity;
import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity; import org.briarproject.briar.android.privategroup.memberlist.GroupMemberListActivity;
import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity; import org.briarproject.briar.android.privategroup.reveal.RevealContactsActivity;
import org.briarproject.briar.android.threaded.ThreadListActivity; import org.briarproject.briar.android.threaded.ThreadListActivity;
import org.briarproject.briar.android.threaded.ThreadListController; import org.briarproject.briar.android.threaded.ThreadListViewModel;
import org.briarproject.briar.api.privategroup.PrivateGroup;
import org.briarproject.briar.api.privategroup.Visibility;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; 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.GONE;
import static android.view.View.VISIBLE; 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.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; import static org.briarproject.briar.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_TEXT_LENGTH;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class GroupActivity extends public class GroupActivity extends
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageAdapter> ThreadListActivity<GroupMessageItem, GroupMessageAdapter> {
implements GroupListener, OnClickListener {
@Inject @Inject
GroupController controller; ViewModelProvider.Factory viewModelFactory;
@Nullable private GroupViewModel viewModel;
private Boolean isCreator = null;
private boolean isDissolved = false;
private MenuItem revealMenuItem, inviteMenuItem, leaveMenuItem,
dissolveMenuItem;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
component.inject(this); component.inject(this);
viewModel = new ViewModelProvider(this, viewModelFactory)
.get(GroupViewModel.class);
} }
@Override @Override
protected ThreadListController<PrivateGroup, GroupMessageItem> getController() { protected ThreadListViewModel<GroupMessageItem> getViewModel() {
return controller; return viewModel;
}
@Override
protected GroupMessageAdapter createAdapter() {
return new GroupMessageAdapter(this);
} }
@Override @Override
@@ -69,65 +62,27 @@ public class GroupActivity extends
super.onCreate(state); super.onCreate(state);
Toolbar toolbar = setUpCustomToolbar(false); 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 // Open member list on Toolbar click
if (toolbar != null) { toolbar.setOnClickListener(v -> {
toolbar.setOnClickListener(v -> { Intent i = new Intent(GroupActivity.this,
Intent i1 = new Intent(GroupActivity.this, GroupMemberListActivity.class);
GroupMemberListActivity.class); i.putExtra(GROUP_ID, groupId.getBytes());
i1.putExtra(GROUP_ID, groupId.getBytes()); startActivity(i);
startActivity(i1); });
});
}
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); setGroupEnabled(false);
} viewModel.isDissolved().observeEvent(this, dissolved -> {
setGroupEnabled(!dissolved);
@Override if (dissolved) onGroupDissolved();
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);
}
});
} }
@Override @Override
@@ -136,74 +91,61 @@ public class GroupActivity extends
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.group_actions, menu); inflater.inflate(R.menu.group_actions, menu);
revealMenuItem = menu.findItem(R.id.action_group_reveal); // show items based on role (which will not change, so observe once)
inviteMenuItem = menu.findItem(R.id.action_group_invite); observeOnce(viewModel.isCreator(), this, isCreator -> {
leaveMenuItem = menu.findItem(R.id.action_group_leave); menu.findItem(R.id.action_group_reveal).setVisible(!isCreator);
dissolveMenuItem = menu.findItem(R.id.action_group_dissolve); menu.findItem(R.id.action_group_invite).setVisible(isCreator);
menu.findItem(R.id.action_group_leave).setVisible(!isCreator);
// all role-dependent items are invisible until we know our role menu.findItem(R.id.action_group_dissolve).setVisible(isCreator);
revealMenuItem.setVisible(false); });
inviteMenuItem.setVisible(false); super.onCreateOptionsMenu(menu);
leaveMenuItem.setVisible(false); return true;
dissolveMenuItem.setVisible(false);
// show items based on role
showMenuItems();
return super.onCreateOptionsMenu(menu);
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { int itemId = item.getItemId();
case R.id.action_group_member_list: if (itemId == R.id.action_group_member_list) {
Intent i1 = new Intent(this, GroupMemberListActivity.class); Intent i = new Intent(this, GroupMemberListActivity.class);
i1.putExtra(GROUP_ID, groupId.getBytes()); i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i1); startActivity(i);
return true; return true;
case R.id.action_group_reveal: } else if (itemId == R.id.action_group_reveal) {
if (isCreator == null || isCreator) if (requireNonNull(viewModel.isCreator().getValue()))
throw new IllegalStateException(); throw new IllegalStateException();
Intent i2 = new Intent(this, RevealContactsActivity.class); Intent i = new Intent(this, RevealContactsActivity.class);
i2.putExtra(GROUP_ID, groupId.getBytes()); i.putExtra(GROUP_ID, groupId.getBytes());
startActivity(i2); startActivity(i);
return true; return true;
case R.id.action_group_invite: } else if (itemId == R.id.action_group_invite) {
if (isCreator == null || !isCreator) if (!requireNonNull(viewModel.isCreator().getValue()))
throw new IllegalStateException(); throw new IllegalStateException();
Intent i3 = new Intent(this, GroupInviteActivity.class); Intent i = new Intent(this, GroupInviteActivity.class);
i3.putExtra(GROUP_ID, groupId.getBytes()); i.putExtra(GROUP_ID, groupId.getBytes());
startActivityForResult(i3, REQUEST_GROUP_INVITE); startActivityForResult(i, REQUEST_GROUP_INVITE);
return true; return true;
case R.id.action_group_leave: } else if (itemId == R.id.action_group_leave) {
if (isCreator == null || isCreator) if (requireNonNull(viewModel.isCreator().getValue()))
throw new IllegalStateException(); throw new IllegalStateException();
showLeaveGroupDialog(); showLeaveGroupDialog();
return true; return true;
case R.id.action_group_dissolve: } else if (itemId == R.id.action_group_dissolve) {
if (isCreator == null || !isCreator) if (!requireNonNull(viewModel.isCreator().getValue()))
throw new IllegalStateException(); throw new IllegalStateException();
showDissolveGroupDialog(); showDissolveGroupDialog();
default: return true;
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
@Override @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) { if (request == REQUEST_GROUP_INVITE && result == RESULT_OK) {
displaySnackbar(R.string.groups_invitation_sent); displaySnackbar(R.string.groups_invitation_sent);
} else super.onActivityResult(request, result, data); } 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 @Override
protected int getMaxTextLength() { protected int getMaxTextLength() {
return MAX_GROUP_POST_TEXT_LENGTH; return MAX_GROUP_POST_TEXT_LENGTH;
@@ -211,11 +153,11 @@ public class GroupActivity extends
@Override @Override
public void onReplyClick(GroupMessageItem item) { public void onReplyClick(GroupMessageItem item) {
if (!isDissolved) super.onReplyClick(item); Boolean isDissolved = viewModel.isDissolved().getLastValue();
if (isDissolved != null && !isDissolved) super.onReplyClick(item);
} }
private void setGroupEnabled(boolean enabled) { private void setGroupEnabled(boolean enabled) {
isDissolved = !enabled;
sendController.setReady(enabled); sendController.setReady(enabled);
list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f); list.getRecyclerView().setAlpha(enabled ? 1f : 0.5f);
@@ -227,21 +169,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() { private void showLeaveGroupDialog() {
AlertDialog.Builder builder = AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme); new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.groups_leave_dialog_title)); builder.setTitle(getString(R.string.groups_leave_dialog_title));
builder.setMessage(getString(R.string.groups_leave_dialog_message)); 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.setPositiveButton(R.string.cancel, null);
builder.show(); builder.show();
} }
@@ -251,37 +185,19 @@ public class GroupActivity extends
new AlertDialog.Builder(this, R.style.BriarDialogTheme); new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.groups_dissolve_dialog_title)); builder.setTitle(getString(R.string.groups_dissolve_dialog_title));
builder.setMessage(getString(R.string.groups_dissolve_dialog_message)); 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.setPositiveButton(R.string.cancel, null);
builder.show(); builder.show();
} }
@Override private void deleteGroup() {
public void onClick(DialogInterface dialog, int which) { // The activity is going to be destroyed by the
controller.deleteNamedGroup( // GroupRemovedEvent being fired
new UiExceptionHandler<DbException>(this) { viewModel.deletePrivateGroup();
// The activity is going to be destroyed by the
// GroupRemovedEvent being fired
@Override
public void onExceptionUi(DbException exception) {
handleException(exception);
}
});
} }
@Override private void onGroupDissolved() {
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);
AlertDialog.Builder builder = AlertDialog.Builder builder =
new AlertDialog.Builder(this, R.style.BriarDialogTheme); new AlertDialog.Builder(this, R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.groups_dissolved_dialog_title)); builder.setTitle(getString(R.string.groups_dissolved_dialog_title));

View File

@@ -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();
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -1,19 +1,18 @@
package org.briarproject.briar.android.privategroup.conversation; package org.briarproject.briar.android.privategroup.conversation;
import org.briarproject.briar.android.activity.ActivityScope; import org.briarproject.briar.android.viewmodel.ViewModelKey;
import org.briarproject.briar.android.activity.BaseActivity;
import androidx.lifecycle.ViewModel;
import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.multibindings.IntoMap;
@Module @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;
}
} }

View File

@@ -4,19 +4,14 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.threaded.BaseThreadItemViewHolder; import org.briarproject.briar.android.threaded.BaseThreadItemViewHolder;
import org.briarproject.briar.android.threaded.ThreadItemAdapter; import org.briarproject.briar.android.threaded.ThreadItemAdapter;
import org.briarproject.briar.android.threaded.ThreadPostViewHolder; import org.briarproject.briar.android.threaded.ThreadPostViewHolder;
import org.briarproject.briar.api.privategroup.Visibility;
import androidx.annotation.LayoutRes; import androidx.annotation.LayoutRes;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.recyclerview.widget.LinearLayoutManager;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
@UiThread @UiThread
@NotNullByDefault @NotNullByDefault
@@ -24,15 +19,14 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
private boolean isCreator = false; private boolean isCreator = false;
GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener, GroupMessageAdapter(ThreadItemListener<GroupMessageItem> listener) {
LinearLayoutManager layoutManager) { super(listener);
super(listener, layoutManager);
} }
@LayoutRes @LayoutRes
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
GroupMessageItem item = items.get(position); GroupMessageItem item = getItem(position);
return item.getLayout(); return item.getLayout();
} }
@@ -47,30 +41,9 @@ class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
return new ThreadPostViewHolder<>(v); return new ThreadPostViewHolder<>(v);
} }
void setPerspective(boolean isCreator) { void setIsCreator(boolean isCreator) {
this.isCreator = isCreator; this.isCreator = isCreator;
notifyDataSetChanged(); 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
}
} }

View File

@@ -0,0 +1,289 @@
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.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
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 MutableLiveEvent<Boolean> isDissolved =
new MutableLiveEvent<>();
@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.setEvent(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
.postEvent(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;
}
LiveEvent<Boolean> isDissolved() {
return isDissolved;
}
}

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.privategroup.conversation;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.api.privategroup.JoinMessageHeader; import org.briarproject.briar.api.privategroup.JoinMessageHeader;
import org.briarproject.briar.api.privategroup.Visibility;
import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.NotThreadSafe;
@@ -13,13 +12,11 @@ import androidx.annotation.UiThread;
@NotThreadSafe @NotThreadSafe
class JoinMessageItem extends GroupMessageItem { class JoinMessageItem extends GroupMessageItem {
private Visibility visibility;
private final boolean isInitial; private final boolean isInitial;
JoinMessageItem(JoinMessageHeader h, String text) { JoinMessageItem(JoinMessageHeader h, String text) {
super(h, text); super(h, text);
this.visibility = h.getVisibility(); isInitial = h.isInitial();
this.isInitial = h.isInitial();
} }
@Override @Override
@@ -33,14 +30,6 @@ class JoinMessageItem extends GroupMessageItem {
return R.layout.list_item_group_join_notice; return R.layout.list_item_group_join_notice;
} }
Visibility getVisibility() {
return visibility;
}
void setVisibility(Visibility visibility) {
this.visibility = visibility;
}
boolean isInitial() { boolean isInitial() {
return isInitial; return isInitial;
} }

View File

@@ -1,29 +1,40 @@
package org.briarproject.briar.android.reporting; package org.briarproject.briar.android.reporting;
import android.content.Context; import android.app.Application;
import android.os.Process; import android.os.Process;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.android.logging.LogEncrypter;
import java.lang.Thread.UncaughtExceptionHandler; import java.lang.Thread.UncaughtExceptionHandler;
import javax.inject.Inject;
import static org.briarproject.briar.android.util.UiUtils.startDevReportActivity; import static org.briarproject.briar.android.util.UiUtils.startDevReportActivity;
@NotNullByDefault @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; private final long appStartTime;
public BriarExceptionHandler(Context ctx) { @Inject
this.ctx = ctx; BriarExceptionHandler(Application app, LogEncrypter logEncrypter) {
this.appStartTime = System.currentTimeMillis(); this.app = app;
this.logEncrypter = logEncrypter;
appStartTime = System.currentTimeMillis();
} }
@Override @Override
public void uncaughtException(Thread t, Throwable e) { 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 // 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()); Process.killProcess(Process.myPid());
System.exit(10); System.exit(10);
} }

View File

@@ -25,8 +25,6 @@ import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.BuildConfig; import org.briarproject.briar.BuildConfig;
import org.briarproject.briar.R; 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.MultiReportInfo;
import org.briarproject.briar.android.reporting.ReportData.ReportItem; import org.briarproject.briar.android.reporting.ReportData.ReportItem;
import org.briarproject.briar.android.reporting.ReportData.SingleReportInfo; import org.briarproject.briar.android.reporting.ReportData.SingleReportInfo;
@@ -41,8 +39,6 @@ import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.logging.Formatter;
import java.util.logging.LogRecord;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
@@ -74,8 +70,8 @@ class BriarReportCollector {
this.ctx = ctx; this.ctx = ctx;
} }
public ReportData collectReportData(@Nullable Throwable t, ReportData collectReportData(@Nullable Throwable t, long appStartTime,
long appStartTime) { String logs) {
ReportData reportData = new ReportData() ReportData reportData = new ReportData()
.add(getBasicInfo(t)) .add(getBasicInfo(t))
.add(getDeviceInfo()); .add(getDeviceInfo());
@@ -86,7 +82,7 @@ class BriarReportCollector {
.add(getStorage()) .add(getStorage())
.add(getConnectivity()) .add(getConnectivity())
.add(getBuildConfig()) .add(getBuildConfig())
.add(getLogcat()) .add(getLogcat(logs))
.add(getDeviceFeatures()); .add(getDeviceFeatures());
} }
@@ -313,15 +309,8 @@ class BriarReportCollector {
buildConfig); buildConfig);
} }
private ReportItem getLogcat() { private ReportItem getLogcat(String logs) {
BriarApplication app = (BriarApplication) ctx.getApplicationContext(); return new ReportItem("Logcat", R.string.dev_report_logcat, logs);
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 getDeviceFeatures() { private ReportItem getDeviceFeatures() {

View File

@@ -35,6 +35,7 @@ public class CrashReportActivity extends BaseActivity
public static final String EXTRA_THROWABLE = "throwable"; public static final String EXTRA_THROWABLE = "throwable";
public static final String EXTRA_APP_START_TIME = "appStartTime"; public static final String EXTRA_APP_START_TIME = "appStartTime";
public static final String EXTRA_APP_LOGCAT = "logcat";
@Inject @Inject
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
@@ -56,7 +57,8 @@ public class CrashReportActivity extends BaseActivity
Intent intent = getIntent(); Intent intent = getIntent();
Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE); Throwable t = (Throwable) intent.getSerializableExtra(EXTRA_THROWABLE);
long appStartTime = intent.getLongExtra(EXTRA_APP_START_TIME, -1); 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 -> { viewModel.getShowReport().observeEvent(this, show -> {
if (show) displayFragment(true); if (show) displayFragment(true);
}); });

View File

@@ -15,4 +15,8 @@ public abstract class DevReportModule {
@ViewModelKey(ReportViewModel.class) @ViewModelKey(ReportViewModel.class)
abstract ViewModel bindReportViewModel(ReportViewModel reportViewModel); abstract ViewModel bindReportViewModel(ReportViewModel reportViewModel);
@Binds
abstract Thread.UncaughtExceptionHandler bindUncaughtExceptionHandler(
BriarExceptionHandler handler);
} }

View File

@@ -11,6 +11,9 @@ import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.reporting.DevReporter; import org.briarproject.bramble.api.reporting.DevReporter;
import org.briarproject.bramble.util.AndroidUtils; import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.briar.R; 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.reporting.ReportData.MultiReportInfo;
import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.LiveEvent;
import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent;
@@ -19,6 +22,7 @@ import org.json.JSONException;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Formatter;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; 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.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.briar.android.logging.BriefLogFormatter.formatLog;
@NotNullByDefault @NotNullByDefault
public class ReportViewModel extends AndroidViewModel { class ReportViewModel extends AndroidViewModel {
private static final Logger LOG = private static final Logger LOG =
getLogger(ReportViewModel.class.getName()); getLogger(ReportViewModel.class.getName());
private final CachingLogHandler logHandler;
private final LogDecrypter logDecrypter;
private final BriarReportCollector collector; private final BriarReportCollector collector;
private final DevReporter reporter; private final DevReporter reporter;
private final PluginManager pluginManager; private final PluginManager pluginManager;
@@ -58,18 +65,39 @@ public class ReportViewModel extends AndroidViewModel {
private boolean isFeedback; private boolean isFeedback;
@Inject @Inject
public ReportViewModel(@NonNull Application application, ReportViewModel(@NonNull Application application,
DevReporter reporter, PluginManager pluginManager) { CachingLogHandler logHandler,
LogDecrypter logDecrypter,
DevReporter reporter,
PluginManager pluginManager) {
super(application); super(application);
this.collector = new BriarReportCollector(application); collector = new BriarReportCollector(application);
this.logHandler = logHandler;
this.logDecrypter = logDecrypter;
this.reporter = reporter; this.reporter = reporter;
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
} }
void init(@Nullable Throwable t, long appStartTime) { void init(@Nullable Throwable t, long appStartTime,
@Nullable byte[] logKey) {
isFeedback = t == null; isFeedback = t == null;
if (reportData.getValue() == null) new SingleShotAndroidExecutor(() -> { 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); reportData.postValue(data);
}).start(); }).start();
} }
@@ -110,8 +138,8 @@ public class ReportViewModel extends AndroidViewModel {
} }
/** /**
* The content of the report * The content of the report that will be loaded after
* that will be loaded after {@link #init(Throwable, long)} was called. * {@link #init(Throwable, long, byte[])} was called.
*/ */
LiveData<ReportData> getReportData() { LiveData<ReportData> getReportData() {
return reportData; return reportData;

View File

@@ -44,7 +44,6 @@ import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.text.TextUtilsCompat;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.Preference.OnPreferenceChangeListener;
@@ -70,7 +69,6 @@ import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID; import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI; import static android.provider.Settings.System.DEFAULT_NOTIFICATION_URI;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
@@ -296,19 +294,27 @@ public class SettingsFragment extends PreferenceFragmentCompat
Locale locale = Localizer.getLocaleFromTag(tag); Locale locale = Localizer.getLocaleFromTag(tag);
if (locale == null) if (locale == null)
throw new IllegalStateException(); throw new IllegalStateException();
// Exclude RTL locales on API < 17, they won't be laid out correctly // Check if the locale is supported on this device
if (SDK_INT < 17 && !isLeftToRight(locale)) { if (!Localizer.isLocaleSupported(locale)) {
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Skipping RTL locale " + tag); LOG.info("Skipping unsupported locale " + tag);
continue; continue;
} }
String nativeName = locale.getDisplayName(locale); String nativeName;
// Fallback to English if the name is unknown in both native and // Unknown languages won't be translated to their native name.
// current locale. if (locale.getLanguage().equals("ast")) {
if (nativeName.equals(tag)) { nativeName = "Asturianu";
String tmp = locale.getDisplayLanguage(Locale.ENGLISH); } else if (locale.getLanguage().equals("oc")) {
if (!tmp.isEmpty() && !tmp.equals(nativeName)) nativeName = "Occitan";
nativeName = tmp; } else {
nativeName = locale.getDisplayName(locale);
// Fallback to English if the name is unknown in both native and
// current locale.
if (nativeName.equals(tag)) {
String tmp = locale.getDisplayLanguage(Locale.ENGLISH);
if (!tmp.isEmpty() && !tmp.equals(nativeName))
nativeName = tmp;
}
} }
// Prefix with LRM marker to prevent any RTL direction // Prefix with LRM marker to prevent any RTL direction
entries.add("\u200E" + nativeName.substring(0, 1).toUpperCase() entries.add("\u200E" + nativeName.substring(0, 1).toUpperCase()
@@ -319,13 +325,6 @@ public class SettingsFragment extends PreferenceFragmentCompat
language.setEntryValues(entryValues.toArray(new CharSequence[0])); language.setEntryValues(entryValues.toArray(new CharSequence[0]));
} }
private boolean isLeftToRight(Locale locale) {
// TextUtilsCompat returns the wrong direction for Hebrew on some phones
String language = locale.getLanguage();
if (language.equals("iw") || language.equals("he")) return false;
int direction = TextUtilsCompat.getLayoutDirectionFromLocale(locale);
return direction == LAYOUT_DIRECTION_LTR;
}
private void setTorNetworkSummary(int torNetworkSetting) { private void setTorNetworkSummary(int torNetworkSetting) {
if (torNetworkSetting != PREF_TOR_NETWORK_AUTOMATIC) { if (torNetworkSetting != PREF_TOR_NETWORK_AUTOMATIC) {

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -9,36 +9,47 @@ import dagger.Provides;
@Module @Module
public class SharingModule { public class SharingModule {
@ActivityScope @Module
@Provides @Deprecated
ShareForumController provideShareForumController( public static class SharingLegacyModule {
ShareForumControllerImpl shareForumController) {
return shareForumController; @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 @Provides
BlogInvitationController provideInvitationBlogController( SharingController provideSharingController(
BaseActivity activity, SharingControllerImpl sharingController) {
BlogInvitationControllerImpl blogInvitationController) { return sharingController;
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;
} }
} }

View File

@@ -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();
}
}

View File

@@ -99,4 +99,14 @@ public abstract class ThreadItem implements MessageNode {
return highlighted; 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);
}
} }

View File

@@ -4,39 +4,45 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.util.ItemReturningAdapter; import org.briarproject.briar.android.util.ItemReturningAdapter;
import org.briarproject.briar.android.util.VersionedAdapter;
import java.util.Collection;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.ListAdapter;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
@UiThread @UiThread
@NotNullByDefault
public class ThreadItemAdapter<I extends ThreadItem> public class ThreadItemAdapter<I extends ThreadItem>
extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>> extends ListAdapter<I, BaseThreadItemViewHolder<I>>
implements VersionedAdapter, ItemReturningAdapter<I> { implements ItemReturningAdapter<I> {
static final int UNDEFINED = -1; static final int UNDEFINED = -1;
protected final NestedTreeList<I> items = new NestedTreeList<>();
private final ThreadItemListener<I> listener; 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, @Override
LinearLayoutManager layoutManager) { public boolean areContentsTheSame(I a, I b) {
return a.isHighlighted() == b.isHighlighted() &&
a.isRead() == b.isRead();
}
});
this.listener = listener; this.listener = listener;
this.layoutManager = layoutManager;
} }
@NonNull @NonNull
@@ -51,76 +57,27 @@ public class ThreadItemAdapter<I extends ThreadItem>
@Override @Override
public void onBindViewHolder(@NonNull BaseThreadItemViewHolder<I> ui, public void onBindViewHolder(@NonNull BaseThreadItemViewHolder<I> ui,
int position) { int position) {
I item = items.get(position); I item = getItem(position);
ui.bind(item, listener); ui.bind(item, listener);
} }
@Override int findItemPosition(MessageId id) {
public int getItemCount() { for (int i = 0; i < getItemCount(); i++) {
return items.size(); if (id.equals(getItem(i).getId())) return i;
}
@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;
} }
return NO_POSITION; // Not found return NO_POSITION; // Not found
} }
boolean contains(MessageId m) {
return items.contains(m);
}
/** /**
* Highlights the item with the given {@link MessageId} * Highlights the item with the given {@link MessageId}
* and disables the highlight for a previously highlighted item, if any. * and disables the highlight for a previously highlighted item, if any.
* * <p>
* Only one item can be highlighted at a time. * Only one item can be highlighted at a time.
*/ */
void setHighlightedItem(@Nullable MessageId id) { void setHighlightedItem(@Nullable MessageId id) {
for (int i = 0; i < items.size(); i++) { for (int i = 0; i < getItemCount(); i++) {
I item = items.get(i); I item = getItem(i);
if (id != null && item.getId().equals(id)) { if (item.getId().equals(id)) {
item.setHighlighted(true); item.setHighlighted(true);
notifyItemChanged(i, item); notifyItemChanged(i, item);
} else if (item.isHighlighted()) { } else if (item.isHighlighted()) {
@@ -132,20 +89,27 @@ public class ThreadItemAdapter<I extends ThreadItem>
@Nullable @Nullable
I getHighlightedItem() { I getHighlightedItem() {
for (I i : items) { for (I item : getCurrentList()) {
if (i.isHighlighted()) return i; if (item.isHighlighted()) return item;
} }
return null; 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 * Returns the position of the first unread item below the current viewport
*/ */
int getVisibleUnreadPosBottom() { int getVisibleUnreadPosBottom(LinearLayoutManager layoutManager) {
int positionBottom = layoutManager.findLastVisibleItemPosition(); int positionBottom = layoutManager.findLastVisibleItemPosition();
if (positionBottom == NO_POSITION) return NO_POSITION; if (positionBottom == NO_POSITION) return NO_POSITION;
for (int i = positionBottom + 1; i < items.size(); i++) { for (int i = positionBottom + 1; i < getItemCount(); i++) {
if (!items.get(i).isRead()) return i; if (!getItem(i).isRead()) return i;
} }
return NO_POSITION; 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 * Returns the position of the first unread item above the current viewport
*/ */
int getVisibleUnreadPosTop() { int getVisibleUnreadPosTop(LinearLayoutManager layoutManager) {
int positionTop = layoutManager.findFirstVisibleItemPosition(); int positionTop = layoutManager.findFirstVisibleItemPosition();
int position = NO_POSITION; int position = NO_POSITION;
for (int i = 0; i < items.size(); i++) { for (int i = 0; i < getItemCount(); i++) {
if (i < positionTop && !items.get(i).isRead()) { if (i < positionTop && !getItem(i).isRead()) {
position = i; position = i;
} else if (i >= positionTop) { } else if (i >= positionTop) {
return position; return position;
@@ -166,6 +130,11 @@ public class ThreadItemAdapter<I extends ThreadItem>
return NO_POSITION; return NO_POSITION;
} }
@Override
public I getItemAt(int position) {
return getItem(position);
}
public interface ThreadItemListener<I> { public interface ThreadItemListener<I> {
void onReplyClick(I item); void onReplyClick(I item);
} }

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -2,25 +2,18 @@ package org.briarproject.briar.android.threaded;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.view.MenuItem; import android.view.MenuItem;
import com.google.android.material.snackbar.Snackbar; 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.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.controller.SharingController; import org.briarproject.briar.android.sharing.SharingController.SharingInfo;
import org.briarproject.briar.android.controller.SharingController.SharingListener;
import org.briarproject.briar.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.briar.android.threaded.ThreadItemAdapter.ThreadItemListener; 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.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView; import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView; 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.TextSendController.SendListener;
import org.briarproject.briar.android.view.UnreadMessageButton; import org.briarproject.briar.android.view.UnreadMessageButton;
import org.briarproject.briar.api.attachment.AttachmentHeader; 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.List;
import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@@ -48,32 +36,19 @@ import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadItem, A extends ThreadItemAdapter<I>> public abstract class ThreadListActivity<I extends ThreadItem, A extends ThreadItemAdapter<I>>
extends BriarActivity extends BriarActivity implements SendListener, ThreadItemListener<I> {
implements ThreadListListener<I>, SendListener, SharingListener,
ThreadItemListener<I>, ThreadListDataSource {
protected static final String KEY_REPLY_ID = "replyId"; protected final A adapter = createAdapter();
protected abstract ThreadListViewModel<I> getViewModel();
private static final Logger LOG = protected abstract A createAdapter();
Logger.getLogger(ThreadListActivity.class.getName());
protected A adapter;
private ThreadScrollListener<I> scrollListener;
protected BriarRecyclerView list; protected BriarRecyclerView list;
private LinearLayoutManager layoutManager;
protected TextInputView textInput; protected TextInputView textInput;
protected TextSendController sendController; protected TextSendController sendController;
protected GroupId groupId; protected GroupId groupId;
@Nullable
private Parcelable layoutManagerState;
@Nullable
private MessageId replyId;
protected abstract ThreadListController<G, I> getController(); private LinearLayoutManager layoutManager;
private ThreadScrollListener<I> scrollListener;
@Inject
protected SharingController sharingController;
@CallSuper @CallSuper
@Override @Override
@@ -86,7 +61,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
byte[] b = i.getByteArrayExtra(GROUP_ID); byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No GroupId in intent."); if (b == null) throw new IllegalStateException("No GroupId in intent.");
groupId = new GroupId(b); groupId = new GroupId(b);
getController().setGroupId(groupId); ThreadListViewModel<I> viewModel = getViewModel();
viewModel.setGroupId(groupId);
textInput = findViewById(R.id.text_input_container); textInput = findViewById(R.id.text_input_container);
sendController = new TextSendController(textInput, this, false); 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); list = findViewById(R.id.list);
layoutManager = new LinearLayoutManager(this); layoutManager = new LinearLayoutManager(this);
list.setLayoutManager(layoutManager); list.setLayoutManager(layoutManager);
adapter = createAdapter(layoutManager);
list.setAdapter(adapter); list.setAdapter(adapter);
scrollListener = new ThreadScrollListener<>(adapter, getController(), scrollListener = new ThreadScrollListener<>(adapter, viewModel,
upButton, downButton); upButton, downButton);
list.getRecyclerView().addOnScrollListener(scrollListener); list.getRecyclerView().addOnScrollListener(scrollListener);
upButton.setOnClickListener(v -> { upButton.setOnClickListener(v -> {
int position = adapter.getVisibleUnreadPosTop(); int position = adapter.getVisibleUnreadPosTop(layoutManager);
if (position != NO_POSITION) { if (position != NO_POSITION) {
list.getRecyclerView().scrollToPosition(position); list.getRecyclerView().scrollToPosition(position);
} }
}); });
downButton.setOnClickListener(v -> { downButton.setOnClickListener(v -> {
int position = adapter.getVisibleUnreadPosBottom(); int position = adapter.getVisibleUnreadPosBottom(layoutManager);
if (position != NO_POSITION) { if (position != NO_POSITION) {
list.getRecyclerView().scrollToPosition(position); list.getRecyclerView().scrollToPosition(position);
} }
}); });
if (state != null) { viewModel.getItems().observe(this, result -> result
byte[] replyIdBytes = state.getByteArray(KEY_REPLY_ID); .onError(this::handleException)
if (replyIdBytes != null) replyId = new MessageId(replyIdBytes); .onSuccess(this::displayItems)
} );
sharingController.setSharingListener(this); viewModel.getSharingInfo().observe(this, this::setToolbarSubTitle);
loadSharingContacts();
}
@Override viewModel.getGroupRemoved().observe(this, removed -> {
@Nullable if (removed) supportFinishAfterTransition();
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);
}
});
} }
@CallSuper @CallSuper
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
sharingController.onStart(); getViewModel().blockAndClearNotifications();
loadItems();
list.startPeriodicUpdate(); list.startPeriodicUpdate();
} }
@@ -232,91 +118,98 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
sharingController.onStop(); getViewModel().unblockNotifications();
list.stopPeriodicUpdate(); 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 @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { if (item.getItemId() == android.R.id.home) {
case android.R.id.home: supportFinishAfterTransition();
supportFinishAfterTransition(); return true;
return true;
default:
return super.onOptionsItemSelected(item);
} }
return super.onOptionsItemSelected(item);
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
if (adapter.getHighlightedItem() != null) { if (adapter.getHighlightedItem() != null) {
textInput.clearText(); textInput.clearText();
replyId = null; getViewModel().setReplyId(null);
updateTextInput(); updateTextInput();
} else { } else {
super.onBackPressed(); 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 @Override
public void onReplyClick(I item) { public void onReplyClick(I item) {
replyId = item.getId(); getViewModel().setReplyId(item.getId());
updateTextInput(); updateTextInput();
// FIXME This does not work for a hardware keyboard // FIXME This does not work for a hardware keyboard
if (textInput.isKeyboardOpen()) { if (textInput.isKeyboardOpen()) {
scrollToItemAtTop(item); scrollToItemAtTop(item.getId());
} else { } else {
// wait with scrolling until keyboard opened // wait with scrolling until keyboard opened
textInput.setOnKeyboardShownListener(() -> { textInput.setOnKeyboardShownListener(() -> {
scrollToItemAtTop(item); scrollToItemAtTop(item.getId());
textInput.setOnKeyboardShownListener(null); textInput.setOnKeyboardShownListener(null);
}); });
} }
} }
@Override protected void setToolbarSubTitle(SharingInfo sharingInfo) {
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) {
ActionBar actionBar = getSupportActionBar(); ActionBar actionBar = getSupportActionBar();
if (actionBar != null) { if (actionBar != null) {
actionBar.setSubtitle( actionBar.setSubtitle(getString(R.string.shared_with,
getString(R.string.shared_with, total, online)); sharingInfo.total, sharingInfo.online));
} }
} }
private void scrollToItemAtTop(I item) { private void scrollToItemAtTop(MessageId messageId) {
int position = adapter.findItemPosition(item); int position = adapter.findItemPosition(messageId);
if (position != NO_POSITION) { if (position != NO_POSITION) {
layoutManager layoutManager.scrollToPositionWithOffset(position, 0);
.scrollToPositionWithOffset(position, 0);
} }
} }
@@ -327,6 +220,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
} }
private void updateTextInput() { private void updateTextInput() {
MessageId replyId = getViewModel().getReplyId();
if (replyId != null) { if (replyId != null) {
textInput.setHint(R.string.forum_message_reply_hint); textInput.setHint(R.string.forum_message_reply_hint);
textInput.showSoftKeyboard(); textInput.showSoftKeyboard();
@@ -341,54 +235,14 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
List<AttachmentHeader> headers) { List<AttachmentHeader> headers) {
if (isNullOrEmpty(text)) throw new AssertionError(); if (isNullOrEmpty(text)) throw new AssertionError();
I replyItem = adapter.getHighlightedItem(); MessageId replyId = getViewModel().getReplyId();
UiResultExceptionHandler<I, DbException> handler = getViewModel().createAndStoreMessage(text, replyId);
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);
textInput.hideSoftKeyboard(); textInput.hideSoftKeyboard();
textInput.clearText(); textInput.clearText();
replyId = null; getViewModel().setReplyId(null);
updateTextInput(); updateTextInput();
} }
protected abstract int getMaxTextLength(); 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);
}
}
} }

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,256 @@
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) {
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);
}
}

View File

@@ -20,15 +20,15 @@ class ThreadScrollListener<I extends ThreadItem>
private static final Logger LOG = private static final Logger LOG =
getLogger(ThreadScrollListener.class.getName()); getLogger(ThreadScrollListener.class.getName());
private final ThreadListController<?, I> controller; private final ThreadListViewModel<I> viewModel;
private final UnreadMessageButton upButton, downButton; private final UnreadMessageButton upButton, downButton;
ThreadScrollListener(ThreadItemAdapter<I> adapter, ThreadScrollListener(ThreadItemAdapter<I> adapter,
ThreadListController<?, I> controller, ThreadListViewModel<I> viewModel,
UnreadMessageButton upButton, UnreadMessageButton upButton,
UnreadMessageButton downButton) { UnreadMessageButton downButton) {
super(adapter); super(adapter);
this.controller = controller; this.viewModel = viewModel;
this.upButton = upButton; this.upButton = upButton;
this.downButton = downButton; this.downButton = downButton;
} }
@@ -44,7 +44,7 @@ class ThreadScrollListener<I extends ThreadItem>
protected void onItemVisible(I item) { protected void onItemVisible(I item) {
if (!item.isRead()) { if (!item.isRead()) {
item.setRead(true); item.setRead(true);
controller.markItemRead(item); viewModel.markItemRead(item);
} }
} }

View File

@@ -54,6 +54,7 @@ import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import static android.content.Context.KEYGUARD_SERVICE; import static android.content.Context.KEYGUARD_SERVICE;
import static android.content.Context.POWER_SERVICE; import static android.content.Context.POWER_SERVICE;
@@ -88,7 +89,6 @@ import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO;
import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES;
import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode; import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode;
import static androidx.core.content.ContextCompat.getColor; import static androidx.core.content.ContextCompat.getColor;
import static androidx.core.content.ContextCompat.getDrawable;
import static androidx.core.content.ContextCompat.getSystemService; import static androidx.core.content.ContextCompat.getSystemService;
import static androidx.core.graphics.drawable.DrawableCompat.setTint; import static androidx.core.graphics.drawable.DrawableCompat.setTint;
import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL; import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL;
@@ -97,6 +97,7 @@ import static java.util.concurrent.TimeUnit.DAYS;
import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes; import static org.briarproject.bramble.util.AndroidUtils.getSupportedImageContentTypes;
import static org.briarproject.briar.BuildConfig.APPLICATION_ID; import static org.briarproject.briar.BuildConfig.APPLICATION_ID;
import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE; 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_APP_START_TIME;
import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE; import static org.briarproject.briar.android.reporting.CrashReportActivity.EXTRA_THROWABLE;
@@ -357,16 +358,17 @@ public class UiUtils {
} }
public static void triggerFeedback(Context ctx) { 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, public static void startDevReportActivity(Context ctx,
Class<? extends FragmentActivity> activity, @Nullable Throwable t, Class<? extends FragmentActivity> activity, @Nullable Throwable t,
@Nullable Long appStartTime) { @Nullable Long appStartTime, @Nullable byte[] logKey) {
final Intent dialogIntent = new Intent(ctx, activity); final Intent dialogIntent = new Intent(ctx, activity);
dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK); dialogIntent.setFlags(FLAG_ACTIVITY_NEW_TASK);
dialogIntent.putExtra(EXTRA_THROWABLE, t); dialogIntent.putExtra(EXTRA_THROWABLE, t);
dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime); dialogIntent.putExtra(EXTRA_APP_START_TIME, appStartTime);
dialogIntent.putExtra(EXTRA_APP_LOGCAT, logKey);
ctx.startActivity(dialogIntent); ctx.startActivity(dialogIntent);
} }
@@ -436,7 +438,8 @@ public class UiUtils {
} }
public static Drawable getDialogIcon(Context ctx, @DrawableRes int resId) { public static Drawable getDialogIcon(Context ctx, @DrawableRes int resId) {
Drawable icon = getDrawable(ctx, resId); Drawable icon =
VectorDrawableCompat.create(ctx.getResources(), resId, null);
setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary)); setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary));
return icon; return icon;
} }

View File

@@ -2,7 +2,6 @@ package org.briarproject.briar.android.view;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -53,9 +52,8 @@ public class BriarRecyclerView extends FrameLayout {
R.styleable.BriarRecyclerView); R.styleable.BriarRecyclerView);
isScrollingToEnd = attributes isScrollingToEnd = attributes
.getBoolean(R.styleable.BriarRecyclerView_scrollToEnd, true); .getBoolean(R.styleable.BriarRecyclerView_scrollToEnd, true);
Drawable drawable = attributes int drawableRes = attributes.getResourceId(R.styleable.BriarRecyclerView_emptyImage, -1);
.getDrawable(R.styleable.BriarRecyclerView_emptyImage); if (drawableRes != -1) setEmptyImage(drawableRes);
if (drawable != null) setEmptyImage(drawable);
String emtpyText = String emtpyText =
attributes.getString(R.styleable.BriarRecyclerView_emptyText); attributes.getString(R.styleable.BriarRecyclerView_emptyText);
if (emtpyText != null) setEmptyText(emtpyText); if (emtpyText != null) setEmptyText(emtpyText);
@@ -139,11 +137,6 @@ public class BriarRecyclerView extends FrameLayout {
} }
} }
public void setEmptyImage(Drawable drawable) {
if (recyclerView == null) initViews();
emptyImage.setImageDrawable(drawable);
}
public void setEmptyImage(@DrawableRes int res) { public void setEmptyImage(@DrawableRes int res) {
if (recyclerView == null) initViews(); if (recyclerView == null) initViews();
emptyImage.setImageResource(res); emptyImage.setImageResource(res);

View File

@@ -4,6 +4,7 @@ import android.app.Activity;
import android.content.ClipData; import android.content.ClipData;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
@@ -28,6 +29,7 @@ import androidx.customview.view.AbsSavedState;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt;
import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION.SDK_INT;
@@ -253,12 +255,14 @@ public class TextAttachmentController extends TextSendController
public void showImageOnboarding(Activity activity) { public void showImageOnboarding(Activity activity) {
int color = resolveColorAttribute(activity, R.attr.colorControlNormal); int color = resolveColorAttribute(activity, R.attr.colorControlNormal);
Drawable drawable = VectorDrawableCompat
.create(activity.getResources(), R.drawable.ic_image, null);
new MaterialTapTargetPrompt.Builder(activity, new MaterialTapTargetPrompt.Builder(activity,
R.style.OnboardingDialogTheme).setTarget(sendButton) R.style.OnboardingDialogTheme).setTarget(sendButton)
.setPrimaryText(R.string.dialog_title_image_support) .setPrimaryText(R.string.dialog_title_image_support)
.setSecondaryText(R.string.dialog_message_image_support) .setSecondaryText(R.string.dialog_message_image_support)
.setBackgroundColour(getColor(activity, R.color.briar_primary)) .setBackgroundColour(getColor(activity, R.color.briar_primary))
.setIcon(R.drawable.ic_image) .setIconDrawable(drawable)
.setIconDrawableColourFilter(color) .setIconDrawableColourFilter(color)
.show(); .show();
} }

View File

@@ -8,7 +8,6 @@ import org.briarproject.briar.R;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.content.ContextCompat;
@UiThread @UiThread
public class TrustIndicatorView extends AppCompatImageView { public class TrustIndicatorView extends AppCompatImageView {
@@ -44,7 +43,7 @@ public class TrustIndicatorView extends AppCompatImageView {
default: default:
res = R.drawable.trust_indicator_unknown; res = R.drawable.trust_indicator_unknown;
} }
setImageDrawable(ContextCompat.getDrawable(getContext(), res)); setImageResource(res);
setVisibility(VISIBLE); setVisibility(VISIBLE);
invalidate(); invalidate();

View File

@@ -5,6 +5,7 @@ import android.app.Application;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbCallable; import org.briarproject.bramble.api.db.DbCallable;
import org.briarproject.bramble.api.db.DbException; 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.Transaction;
import org.briarproject.bramble.api.db.TransactionManager; import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
@@ -23,6 +24,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; import androidx.annotation.UiThread;
import androidx.arch.core.util.Function; import androidx.arch.core.util.Function;
import androidx.core.util.Consumer;
import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@@ -57,8 +59,8 @@ public abstract class DbViewModel extends AndroidViewModel {
} }
/** /**
* Runs the given task on the {@link DatabaseExecutor} * Waits for the DB to open and runs the given task on the
* and waits for the DB to open. * {@link DatabaseExecutor}.
* <p> * <p>
* If you need a list of items to be displayed in a * If you need a list of items to be displayed in a
* {@link RecyclerView.Adapter}, * {@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 * Loads a list of items on the {@link DatabaseExecutor} within a single
* {@link Transaction} and publishes it as a {@link LiveResult} * {@link Transaction} and publishes it as a {@link LiveResult}

View File

@@ -39,6 +39,17 @@ public class LiveEvent<T> extends LiveData<LiveEvent.ConsumableEvent<T>> {
super.observeForever(observer); 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> { static class ConsumableEvent<T> {
private final T content; private final T content;

View File

@@ -93,4 +93,7 @@ public interface AndroidNotificationManager {
void blockAllBlogPostNotifications(); void blockAllBlogPostNotifications();
void unblockAllBlogPostNotifications(); void unblockAllBlogPostNotifications();
void restartNotifications(boolean locked, boolean mayAlertAgain);
} }

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:viewportWidth="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M13,12h7v1.5h-7zM13,9.5h7L20,11h-7zM13,14.5h7L20,16h-7zM21,4L3,4c-1.1,0 -2,0.9 -2,2v13c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM21,19h-9L12,6h9v13z"/> android:pathData="M13,12h7v1.5h-7zM13,9.5h7L20,11h-7zM13,14.5h7L20,16h-7zM21,4L3,4c-1.1,0 -2,0.9 -2,2v13c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,6c0,-1.1 -0.9,-2 -2,-2zM21,19h-9L12,6h9v13z" />
</vector> </vector>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:viewportWidth="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M20,0L4,0v2h16L20,0zM4,24h16v-2L4,22v2zM20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM12,6.75c1.24,0 2.25,1.01 2.25,2.25s-1.01,2.25 -2.25,2.25S9.75,10.24 9.75,9 10.76,6.75 12,6.75zM17,17L7,17v-1.5c0,-1.67 3.33,-2.5 5,-2.5s5,0.83 5,2.5L17,17z"/> android:pathData="M20,0L4,0v2h16L20,0zM4,24h16v-2L4,22v2zM20,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM12,6.75c1.24,0 2.25,1.01 2.25,2.25s-1.01,2.25 -2.25,2.25S9.75,10.24 9.75,9 10.76,6.75 12,6.75zM17,17L7,17v-1.5c0,-1.67 3.33,-2.5 5,-2.5s5,0.83 5,2.5L17,17z" />
</vector> </vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="@android:color/white"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
</group>
</vector>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:viewportWidth="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/> android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z" />
</vector> </vector>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:viewportWidth="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FFFFFFFF" android:fillColor="#FFFFFFFF"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/> android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z" />
</vector> </vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="#FF000000"
android:pathData="M16,17V14H9V10H16V7L21,12L16,17M14,2A2,2 0,0 1,16 4V6H14V4H5V20H14V18H16V20A2,2 0,0 1,14 22H5A2,2 0,0 1,3 20V4A2,2 0,0 1,5 2H14Z"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Some files were not shown because too many files have changed in this diff Show More