Compare commits

...

71 Commits

Author SHA1 Message Date
Santiago Torres
cef4c31e0b FIX: Addresses reviewer comments 2016-09-23 09:21:03 -04:00
Santiago Torres
62527a62c1 WIP: Replace session states in the Engine classes 2016-09-19 22:39:19 -04:00
Torsten Grote
89ab4dd04e Merge branch '665-android-executor-npe' into 'master'
Fix potential NPE in AndroidExecutorImpl

Always wait for the startup latch to ensure the background thread's Handler is initialised before using it.

Closes #665.

See merge request !319
2016-09-19 16:15:41 +00:00
akwizgran
f3ea731565 Always wait for latch before using background handler. 2016-09-19 10:46:25 +01:00
Torsten Grote
bf023874f0 Merge branch '664-no-delivery-hook' into 'master'
Don't throw an exception if the client has no delivery hook

Closes #664.

See merge request !318
2016-09-16 23:41:53 +00:00
akwizgran
16ac73d002 Don't throw an exception if the client has no delivery hook. 2016-09-16 22:02:04 +01:00
akwizgran
cc439aa74f Merge branch '569-convert-html-to-plain-text-safely-and-readably' into 'master'
Clean HTML from RSS feeds with Jsoup and Show Link Warning

This adds HTML cleaning with the Jsoup library based on a whitelist. The resulting HTML is then used to create a `Spannable` in Android. This spannable is traversed and URLs are replaced by a custom dialog fragment that shows the following warning.

![device-2016-09-07-154936](/uploads/87a9800bde2676b0f2ddf5a894aa04eb/device-2016-09-07-154936.png)

Closes #569

See merge request !311
2016-09-15 10:29:39 +00:00
Torsten Grote
7a1003178d Introduce new MovementMethod for text selection and link clicks 2016-09-13 15:02:27 -03:00
Torsten Grote
cab667ef6a Show all apps that can open links and dismis dialog after click 2016-09-13 14:47:52 -03:00
Torsten Grote
7035d8063c Clean HTML from RSS feeds with Jsoup and show Link Warning 2016-09-13 14:47:51 -03:00
Torsten Grote
2fbbb663a3 Merge branch '619-validation-transactions' into 'master'
Refactor ValidationManager and fix some bugs

The validation manager uses a pattern of throwing an exception from within a transaction, then trying to ensure that the transaction gets committed anyway by setting it complete in a finally block. This is a bad idea - if the intentional exception isn't thrown but a later method throws an unintentional exception, the changes up to that point will be committed instead of rolled back.

I reworked the control flow to remove this pattern, and in the process realised that the validation manager was doing a lot of work to provide objects to attach to MessageStateChangedEvents, which are only used by tests these days. So I fixed that and a couple of other issues.

The VALID state is no longer needed and has been removed.

Closes #619

See merge request !313
2016-09-13 15:18:16 +00:00
akwizgran
b4714b5360 Removed redundant message state check. 2016-09-12 12:09:21 +01:00
akwizgran
8a3e5bfb50 Refactor ValidationManager and fix some bugs. #619 2016-09-09 18:03:14 +01:00
akwizgran
fd4275733f Merge branch 'fix-master-ui-handlers' into 'master'
Introduce a DestroyableActivity for UI result handlers



See merge request !314
2016-09-08 14:52:07 +00:00
Torsten Grote
af5b246d7b Introduce a DestroyableActivity for UI result handlers 2016-09-08 11:25:02 -03:00
akwizgran
49e9ee2b6d Merge branch '637-effect-of-touching-a-post-is-inconsistent' into 'master'
Resolve Outstanding Blog Issues

This MR has several commits which fix individual small issues. The biggest commit is a refactoring of the `BlogActivity`, so its `ViewPager` can be used also for the Blog feed.

Closes #637

See merge request !309
2016-09-08 11:57:18 +00:00
akwizgran
1a611d55be Merge branch '610-result-handlers-can-return-results-to-destroyed-activities-fragments' into '637-effect-of-touching-a-post-is-inconsistent'
Check if activity is still alive before returning results to it

Closes #610

See merge request !312
2016-09-08 11:45:52 +00:00
Torsten Grote
7c5945de01 Check if activity is still alive before returning results to it 2016-09-07 17:44:07 -03:00
Torsten Grote
ccc49df08e Address review issues 2016-09-07 14:24:19 -03:00
Torsten Grote
ac325d4423 remove unused resources 2016-09-07 12:51:43 -03:00
Torsten Grote
0a0e1b4280 Always replace current blog activity when new one opened
Fixes #635
2016-09-07 12:51:42 -03:00
Torsten Grote
cb64740916 Set blog title when opening blog
Fixes #634
2016-09-07 12:51:42 -03:00
Torsten Grote
73bc86df4a show only a teaser for blog posts in lists 2016-09-07 12:51:41 -03:00
Torsten Grote
307e124ee8 Make the blog post pager usable for the feed and individual blogs 2016-09-07 12:51:41 -03:00
Torsten Grote
60b4c5649a Make blog post text clickable
by making it only selectable in detail view
2016-09-07 12:51:40 -03:00
akwizgran
e0e46a726e Merge branch '641-rss-feed-entry-comparator-wrongly-assumes-existing-date' into 'master'
Don't crash if an RSS entry has no date

Closes #641

See merge request !310
2016-09-07 14:06:06 +00:00
akwizgran
668eaf45f6 Merge branch '636-blogpostfragment-should-use-the-same-layout-as-the-blog-feed' into 'master'
Show Blog Posts always in the same design

This also adds comments to the blog post detail screen that were previously missing.
It re-uses the existing `BlogPostViewHolder` and its `bind()` methods effectively reducing the amount of code necessary.

Closes #636

See merge request !307
2016-09-07 13:12:25 +00:00
akwizgran
fb9d743d11 Merge branch '627-tests-for-introduction-security-properties' into 'master'
Add one introduction test for modified response message

This is only the first part of #627, but I am putting it up for review already, since the second part will be very similar and issues found here will likely apply to both.

See merge request !306
2016-09-07 13:08:34 +00:00
akwizgran
fc2213c042 Merge branch '633-blog-posts-from-other-users-sometimes-have-bold-nicknames' into 'master'
Fix view recycling issue with author name typeface

Closes #633

See merge request !305
2016-09-07 12:43:08 +00:00
akwizgran
34f9311f43 Merge branch '638-illegalstateexception-when-navigating-out-of-combined-feed-and-back-in' into 'master'
Allow Blog Controller listener to re-attach

Closes #638

See merge request !304
2016-09-07 12:39:04 +00:00
akwizgran
28fd84b261 Merge branch '632-badtokenexception-if-rss-error-dialog-is-show-after-activity-is-destroyed' into 'master'
Only show RSS Feed Import Failed Dialog when Activity lives

Closes #632

See merge request !303
2016-09-07 12:36:12 +00:00
Torsten Grote
448bf86379 don't crash if an RSS entry has no date 2016-09-06 15:55:53 -03:00
Torsten Grote
91e529a9e9 Merge branch 'socks-socket' into 'master'
Fetch RSS feeds via Tor

This patch replaces jsocks with our own minimal SOCKS 5 implementation, which is compatible with Android's OpenSSL hacks (see discussion on #599 for the horrifying details). This allows us to use OkHttp over Tor to fetch RSS feeds.

It turns out that SOCKS 5 without authentication is a really simple protocol: https://tools.ietf.org/html/rfc1928

Closes #599.

See merge request !308
2016-09-06 18:37:38 +00:00
akwizgran
74f9a0dd73 Use our own SOCKS implementation to talk to Tor. 2016-09-05 19:59:22 +01:00
Torsten Grote
54290bc374 Show Blog Posts always in the same design
This also adds comments to the blog post detail screen that were
previously missing.
2016-09-05 13:33:44 -03:00
Torsten Grote
3e39ad8a68 Add one introduction test for modified response message 2016-09-02 11:49:10 -03:00
Torsten Grote
07445ada41 Fix view recycling issue with author name typeface 2016-09-02 11:38:44 -03:00
Torsten Grote
5281d8b468 Allow Blog Controller listener to re-attach 2016-09-02 11:25:17 -03:00
Torsten Grote
0deac1d1b6 Only show RSS Feed Import Failed Dialog when Activity lives 2016-09-02 11:12:24 -03:00
akwizgran
387e44d114 Merge branch '496-implement-ui-for-reblogging-and-blog-comments' into 'master'
UI for reblogging and blog comments

This MR wraps all blog posts inside a `CardView` and adds a reblog button to blog posts. When this button is pressed, a new activity opens that shows the blog post again and allows the user to reblog it into their own blog. An optional comment can be added.

Also a new compound view `AuthorView` is introduced to display authors and their trust level in a consistent way.

The `BlogController` has been refactored, so the `FeedController` can share most code through an abstract base class.

![reblog2](/uploads/ce4764851c4cbe39684c95a3b427e3a2/reblog2.gif)

Closes #496, #437 

See merge request !300
2016-09-02 11:16:28 +00:00
akwizgran
bb00412187 Added @UiThread annotations, minor code cleanups. 2016-09-02 11:53:16 +01:00
Torsten Grote
bb0a2a5b32 Add a scene transition animation when reblogging a blog post 2016-09-02 11:53:16 +01:00
Torsten Grote
6d7e0aab07 Reblogging and Comment UI 2016-09-02 11:53:08 +01:00
Torsten Grote
da08072409 Make the user's own identities visually distinct
Closes #593
2016-09-02 11:52:34 +01:00
Torsten Grote
9664aea520 Introduce and use CardView and AuthorView for Blog Posts 2016-09-02 11:52:34 +01:00
akwizgran
636b587b9c Merge branch '605-add-database-method-for-retrieving-a-contact-by-local-and-remote-author-ids' into 'master'
Add database method for retrieving a contact by local and remote author IDs

Closes #605

See merge request !302
2016-09-01 20:00:27 +00:00
akwizgran
0afab09fdf Merge branch '616-continue-button-is-off-screen-when-adding-a-contact' into 'master'
Scroll down Adding Contact screen

Normally, you would introduce a new layout only for small screens, but in this case it is easier and has less maintenance overhead to just scroll down the screen, so the button is always visible after loading.

While I was at it, I also replaced `getLocalAuthors()` by the new `getLocalAuthor()`.

Closes #616

See merge request !294
2016-09-01 19:54:31 +00:00
Torsten Grote
fe4f71fe0f Remove lots of unused code for identity selection 2016-09-01 16:13:55 -03:00
Torsten Grote
6f0ffa8439 Add database method for retrieving a contact by author ID
and use it for retreiving the status of an author faster.
Also add tests for both.
2016-09-01 14:06:38 -03:00
Torsten Grote
69dd399bd2 Scroll down Adding Contact screen
Closes #616
2016-09-01 12:16:26 -03:00
akwizgran
8b7304f4cb Merge branch '599-fetch-rss-feeds-via-tor' into 'master'
Factor out Tor Socks Port

This MR factors out the local Tor port, so other clients can listen to an `TransportEnabledEvent` for Tor and start tasks as soon as Tor becomes available. This would also enable client to pass traffic through Tor's local SOCKS proxy.

See merge request !301
2016-09-01 11:57:13 +00:00
Torsten Grote
e023e5d8fc Start RSS Feed Fetcher only after TransportEnabledEvent for Tor 2016-09-01 12:55:25 +01:00
akwizgran
042050453a Merge branch '359-introduction-message-can-be-overlooked' into 'master'
Attach optional messages to invitations in Conversation view

![device-2016-08-29-115823](/uploads/e819324046b8a37ff06da7300046369c/device-2016-08-29-115823.png)![device-2016-08-29-115834](/uploads/051b4b64938bfa6f3ed610db28909865/device-2016-08-29-115834.png)

Closes #359

See merge request !299
2016-09-01 11:43:18 +00:00
akwizgran
1c15e690e1 Merge branch '352-conversation-screen-has-too-much-padding' into 'master'
Improve conversation screen graphics with less padding

This is how the conversation screen looks with this MR:

![1](https://code.briarproject.org/akwizgran/briar/uploads/3078636c93f7edc6b07034ad808ffc7c/device-2016-08-26-105325.png)
![2](https://code.briarproject.org/akwizgran/briar/uploads/fe0dcd3b1cbc6593aabed6faab1801d7/device-2016-08-26-105425.png)

This MR also replaces more raster graphics with vector graphics.

Closes #352

See merge request !298
2016-09-01 11:37:10 +00:00
Torsten Grote
61c05c1dd4 Fix tests broken by latest merges! 2016-08-31 11:32:53 -03:00
akwizgran
d8a9d83caf Merge branch '494-implement-backend-for-reblogging-and-blog-comments' into 'master'
Add support for comments and reblogging to Blog Client

Comments and reblogs need to depend on the post they refer to.
Since message dependencies are limited to one group,
the post and also the comments need to be wrapped
when commented on or reblogged to another blog (and group).

For this reason, in addition to comments, two new wrapping message types
are introduced. They retain all data of the original messages and allow
for reconstruction and signature verification.

This MR breaks backwards compatibility with old blog posts.
It removes the content type, title and parent ID from the post.

Furthermore, it includes one commit that replaces the `Message` in `MessageSharedEvent` with a `MessageId`.

Closes #494

See merge request !285
2016-08-30 23:09:31 +00:00
akwizgran
70a44e8a27 Merge branch '364-introduction-responses-should-be-signed' into 'master'
Sign Introduction Information to be able to discover MITM attack

The local introducee doesn't know whether each piece of information received from the introducer originates from the remote introducee or has been replaced by the introducer, i.e. whether the introducer is carrying out a man-in-the-middle attack.

The introduction protocol doesn't aim to detect or prevent man-in-the-middle attacks. We only aim to establish that if the remote identity public key is not replaced then the remote ephemeral public key, transport properties and timestamp are not replaced either. This MR adds a MAC and a signature to the introduction protocol's ACK message to fulfill that aim. See #364 for the detailed security argument.

Later, when the local introducee verifies that the remote identity public key belongs to a particular person (#513), she can also be sure that the remote ephemeral public key, transport properties and timestamp originated from that person.

Closes #364

See merge request !297
2016-08-30 23:05:25 +00:00
Torsten Grote
fc5a7290e3 Calculate and verify signature and MAC for Introduction ACKs
Before the introducee sends her ACK,
she derives a master key from the ephemeral shared secret as before.

Two nonces and a MAC key are then derived from the master key.
The local introducee signs one of the nonces and calculates a MAC
over her own identity public key, ephemeral public key,
transport properties and timestamp.
The local introducee includes the signature and MAC in her ACK.

On receiving the remote introducee's ACK,
the local introducee verifies the signature and MAC.
Should the verification fail, an ABORT is sent to the introducer and
the remote introducee that was added as inactive is deleted again.
2016-08-30 19:58:54 -03:00
Torsten Grote
7db0e4472a Introduction Client: Add MAC and signature to ACK message
The MAC and signature are not yet generated and verified.
This will happen in a later commit.
2016-08-30 19:25:53 -03:00
Torsten Grote
65bdd5558e Address bugs and comments from reblogging code review 2016-08-30 18:52:33 -03:00
Torsten Grote
3dd3a18694 Add support for comments and reblogging to Blog Client
Comments and reblogs need to depend on the post they refer to.
Since message dependencies are limited to one group,
the post and also the comments need to be wrapped
when commented on or reblogged to another blog.

For this reason, in addition to comments, two new wrapping message types
are introduced. They retain all data of the original messages and allow
for reconstruction and signature verification.

This commit breaks backwards compatibility with old blog posts.
It removes the content type, title and parent ID from the post
message structure.
2016-08-29 13:37:20 -03:00
Torsten Grote
743fc7dd1f Do not require an entire Message for the MessageSharedEvent 2016-08-29 13:36:09 -03:00
akwizgran
eb3da2aac9 Merge branch '539-564-568-notification-overhaul' into 'master'
Fix various notification bugs

Fixes #539, #564, #568.

Depends on !276.

See merge request !286
2016-08-29 16:27:47 +00:00
Torsten Grote
4518f59a54 Improving notices with messages attached in conversation view 2016-08-29 12:01:51 -03:00
Torsten Grote
11c0cb667f Improve conversation screen graphics with less padding
Also replace more raster graphics with vector graphics.
2016-08-29 10:02:15 -03:00
Torsten Grote
132a03d0a9 Merge branch '364-add-mac-to-crypto-component' into 'master'
Add MAC function to crypto component



See merge request !295
2016-08-26 13:34:56 +00:00
Torsten Grote
c666b0493c Merge branch '618-bdf-dictionary-iteration-order' into 'master'
Define iteration order of BdfDictionary

Closes #618

See merge request !296
2016-08-26 13:29:06 +00:00
akwizgran
625276067a Define iteration order of BdfDictionary. 2016-08-26 10:57:57 +01:00
akwizgran
30f2c192c6 Added MAC function to crypto component. 2016-08-26 10:06:24 +01:00
akwizgran
28be7d5de3 Added UI thread methods to AndroidExecutor. 2016-08-20 18:41:05 +01:00
akwizgran
dbdf567d4e Overhaul notifications to fix various bugs.
Fixes #539, #564 and #568.
2016-08-20 18:40:59 +01:00
235 changed files with 7898 additions and 4314 deletions

View File

@@ -37,6 +37,34 @@
<JavaCodeStyleSettings>
<option name="ANNOTATION_PARAMETER_WRAP" value="1" />
</JavaCodeStyleSettings>
<Objective-C-extensions>
<option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" />
<option name="RELEASE_STYLE" value="IVAR" />
<option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" />
<file>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
</file>
<class>
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
</class>
<extensions>
<pair source="cpp" header="h" />
<pair source="c" header="h" />
</extensions>
</Objective-C-extensions>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>

View File

@@ -3,6 +3,7 @@ package org.briarproject;
import net.jodah.concurrentunit.Waiter;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogCommentHeader;
import org.briarproject.api.blogs.BlogFactory;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPost;
@@ -50,11 +51,13 @@ import javax.inject.Inject;
import static junit.framework.Assert.assertFalse;
import static org.briarproject.TestPluginsModule.MAX_LATENCY;
import static org.briarproject.api.blogs.MessageType.COMMENT;
import static org.briarproject.api.blogs.MessageType.POST;
import static org.briarproject.api.blogs.MessageType.WRAPPED_COMMENT;
import static org.briarproject.api.blogs.MessageType.WRAPPED_POST;
import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.api.sync.ValidationManager.State.PENDING;
import static org.briarproject.api.sync.ValidationManager.State.VALID;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -89,7 +92,6 @@ public class BlogManagerTest {
private final int TIMEOUT = 15000;
private final String AUTHOR1 = "Author 1";
private final String AUTHOR2 = "Author 2";
private final String CONTENT_TYPE = "text/plain";
private static final Logger LOG =
Logger.getLogger(ForumSharingIntegrationTest.class.getName());
@@ -172,15 +174,15 @@ public class BlogManagerTest {
defaultInit();
// check that blog0 has no posts
final byte[] body = TestUtils.getRandomBytes(42);
final String body = TestUtils.getRandomString(42);
Collection<BlogPostHeader> headers0 =
blogManager0.getPostHeaders(blog0.getId());
assertEquals(0, headers0.size());
// add a post to blog0
BlogPost p = blogPostFactory
.createBlogPost(blog0.getId(), null, clock.currentTimeMillis(),
null, author0, CONTENT_TYPE, body);
.createBlogPost(blog0.getId(), clock.currentTimeMillis(), null,
author0, body);
blogManager0.addLocalPost(p);
// check that post is now in blog0
@@ -188,8 +190,7 @@ public class BlogManagerTest {
assertEquals(1, headers0.size());
// check that body is there
assertArrayEquals(body,
blogManager0.getPostBody(p.getMessage().getId()));
assertEquals(body, blogManager0.getPostBody(p.getMessage().getId()));
// make sure that blog0 at author1 doesn't have the post yet
Collection<BlogPostHeader> headers1 =
@@ -203,10 +204,10 @@ public class BlogManagerTest {
// make sure post arrived
headers1 = blogManager1.getPostHeaders(blog0.getId());
assertEquals(1, headers1.size());
assertEquals(POST, headers1.iterator().next().getType());
// check that body is there
assertArrayEquals(body,
blogManager1.getPostBody(p.getMessage().getId()));
assertEquals(body, blogManager1.getPostBody(p.getMessage().getId()));
stopLifecycles();
}
@@ -217,10 +218,10 @@ public class BlogManagerTest {
defaultInit();
// add a post to blog1
final byte[] body = TestUtils.getRandomBytes(42);
final String body = TestUtils.getRandomString(42);
BlogPost p = blogPostFactory
.createBlogPost(blog1.getId(), null, clock.currentTimeMillis(),
null, author0, CONTENT_TYPE, body);
.createBlogPost(blog1.getId(), clock.currentTimeMillis(), null,
author0, body);
blogManager0.addLocalPost(p);
// check that post is now in blog1
@@ -287,20 +288,261 @@ public class BlogManagerTest {
stopLifecycles();
}
@Test
public void testBlogComment() throws Exception {
startLifecycles();
defaultInit();
// add a post to blog0
final String body = TestUtils.getRandomString(42);
BlogPost p = blogPostFactory
.createBlogPost(blog0.getId(), clock.currentTimeMillis(), null,
author0, body);
blogManager0.addLocalPost(p);
// sync the post over
sync0To1();
deliveryWaiter.await(TIMEOUT, 1);
// make sure post arrived
Collection<BlogPostHeader> headers1 =
blogManager1.getPostHeaders(blog0.getId());
assertEquals(1, headers1.size());
assertEquals(POST, headers1.iterator().next().getType());
// 1 adds a comment to that blog post
String comment = "This is a comment on a blog post!";
blogManager1
.addLocalComment(author1, blog1.getId(), comment,
headers1.iterator().next());
// sync comment over
sync1To0();
deliveryWaiter.await(TIMEOUT, 2);
// make sure comment and wrapped post arrived
Collection<BlogPostHeader> headers0 =
blogManager0.getPostHeaders(blog1.getId());
assertEquals(1, headers0.size());
assertEquals(COMMENT, headers0.iterator().next().getType());
BlogCommentHeader h = (BlogCommentHeader) headers0.iterator().next();
assertEquals(author0, h.getParent().getAuthor());
// ensure that body can be retrieved from wrapped post
assertEquals(body, blogManager0.getPostBody(h.getParentId()));
// 1 has only their own comment in their blog
headers1 = blogManager1.getPostHeaders(blog1.getId());
assertEquals(1, headers1.size());
stopLifecycles();
}
@Test
public void testBlogCommentOnOwnPost() throws Exception {
startLifecycles();
defaultInit();
// add a post to blog0
final String body = TestUtils.getRandomString(42);
BlogPost p = blogPostFactory
.createBlogPost(blog0.getId(), clock.currentTimeMillis(), null,
author0, body);
blogManager0.addLocalPost(p);
// get header of own post
Collection<BlogPostHeader> headers0 =
blogManager0.getPostHeaders(blog0.getId());
assertEquals(1, headers0.size());
BlogPostHeader header = headers0.iterator().next();
// add a comment on own post
String comment = "This is a comment on my own blog post!";
blogManager0
.addLocalComment(author0, blog0.getId(), comment, header);
// sync the post and comment over
sync0To1();
deliveryWaiter.await(TIMEOUT, 2);
// make sure post arrived
Collection<BlogPostHeader> headers1 =
blogManager1.getPostHeaders(blog0.getId());
assertEquals(2, headers1.size());
for (BlogPostHeader h : headers1) {
if (h.getType() == POST) {
assertEquals(body, blogManager1.getPostBody(h.getId()));
} else {
assertEquals(comment, ((BlogCommentHeader)h).getComment());
}
}
stopLifecycles();
}
@Test
public void testCommentOnComment() throws Exception {
startLifecycles();
defaultInit();
// add a post to blog0
final String body = TestUtils.getRandomString(42);
BlogPost p = blogPostFactory
.createBlogPost(blog0.getId(), clock.currentTimeMillis(), null,
author0, body);
blogManager0.addLocalPost(p);
// sync the post over
sync0To1();
deliveryWaiter.await(TIMEOUT, 1);
// make sure post arrived
Collection<BlogPostHeader> headers1 =
blogManager1.getPostHeaders(blog0.getId());
assertEquals(1, headers1.size());
assertEquals(POST, headers1.iterator().next().getType());
// 1 reblogs that blog post
blogManager1
.addLocalComment(author1, blog1.getId(), null,
headers1.iterator().next());
// sync comment over
sync1To0();
deliveryWaiter.await(TIMEOUT, 2);
// make sure comment and wrapped post arrived
Collection<BlogPostHeader> headers0 =
blogManager0.getPostHeaders(blog1.getId());
assertEquals(1, headers0.size());
// get header of comment
BlogPostHeader cHeader = headers0.iterator().next();
assertEquals(COMMENT, cHeader.getType());
// comment on the comment
String comment = "This is a comment on a reblogged post.";
blogManager0
.addLocalComment(author0, blog0.getId(), comment, cHeader);
// sync comment over
sync0To1();
deliveryWaiter.await(TIMEOUT, 3);
// check that comment arrived
headers1 =
blogManager1.getPostHeaders(blog0.getId());
assertEquals(2, headers1.size());
// get header of comment
cHeader = null;
for (BlogPostHeader h : headers1) {
if (h.getType() == COMMENT) {
cHeader = h;
}
}
assertTrue(cHeader != null);
// another comment on the comment
String comment2 = "This is a comment on a comment.";
blogManager1.addLocalComment(author1, blog1.getId(), comment2, cHeader);
// sync comment over
sync1To0();
deliveryWaiter.await(TIMEOUT, 4);
// make sure new comment arrived
headers0 =
blogManager0.getPostHeaders(blog1.getId());
assertEquals(2, headers0.size());
boolean satisfied = false;
for (BlogPostHeader h : headers0) {
assertEquals(COMMENT, h.getType());
BlogCommentHeader c = (BlogCommentHeader) h;
if (c.getComment() != null && c.getComment().equals(comment2)) {
assertEquals(author0, c.getParent().getAuthor());
assertEquals(WRAPPED_COMMENT, c.getParent().getType());
assertEquals(comment,
((BlogCommentHeader) c.getParent()).getComment());
assertEquals(WRAPPED_COMMENT,
((BlogCommentHeader) c.getParent()).getParent()
.getType());
assertEquals(WRAPPED_POST,
((BlogCommentHeader) ((BlogCommentHeader) c
.getParent()).getParent()).getParent()
.getType());
satisfied = true;
}
}
assertTrue(satisfied);
stopLifecycles();
}
@Test
public void testCommentOnOwnComment() throws Exception {
startLifecycles();
defaultInit();
// add a post to blog0
final String body = TestUtils.getRandomString(42);
BlogPost p = blogPostFactory
.createBlogPost(blog0.getId(), clock.currentTimeMillis(), null,
author0, body);
blogManager0.addLocalPost(p);
// sync the post over
sync0To1();
deliveryWaiter.await(TIMEOUT, 1);
// make sure post arrived
Collection<BlogPostHeader> headers1 =
blogManager1.getPostHeaders(blog0.getId());
assertEquals(1, headers1.size());
assertEquals(POST, headers1.iterator().next().getType());
// 1 reblogs that blog post with a comment
String comment = "This is a comment on a post.";
blogManager1
.addLocalComment(author1, blog1.getId(), comment,
headers1.iterator().next());
// get comment from own blog
headers1 = blogManager1.getPostHeaders(blog1.getId());
assertEquals(1, headers1.size());
assertEquals(COMMENT, headers1.iterator().next().getType());
BlogCommentHeader ch = (BlogCommentHeader) headers1.iterator().next();
assertEquals(comment, ch.getComment());
comment = "This is a comment on a post with a comment.";
blogManager1.addLocalComment(author1, blog1.getId(), comment, ch);
// sync both comments over (2 comments + 1 wrapped post)
sync1To0();
deliveryWaiter.await(TIMEOUT, 3);
// make sure both comments arrived
Collection<BlogPostHeader> headers0 =
blogManager0.getPostHeaders(blog1.getId());
assertEquals(2, headers0.size());
stopLifecycles();
}
@After
public void tearDown() throws Exception {
TestUtils.deleteTestDirectory(testDir);
}
private class Listener implements EventListener {
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
if (!event.isLocal()) {
if (event.getState() == DELIVERED) {
deliveryWaiter.resume();
} else if (event.getState() == VALID ||
event.getState() == INVALID ||
} else if (event.getState() == INVALID ||
event.getState() == PENDING) {
validationWaiter.resume();
}

View File

@@ -25,7 +25,6 @@ import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sharing.InvitationMessage;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.SyncSession;
import org.briarproject.api.sync.SyncSessionFactory;
import org.briarproject.api.sync.ValidationManager.State;
@@ -424,7 +423,7 @@ public class BlogSharingIntegrationTest extends BriarTestCase {
eventWaiter.await(TIMEOUT, 1);
assertTrue(listener0.responseReceived);
// blog was added successfully and is shard both ways
// blog was added successfully and is shared both ways
assertEquals(3, blogManager1.getBlogs().size());
Collection<Contact> sharedWith =
blogSharingManager0.getSharedWith(blog2.getId());
@@ -514,20 +513,13 @@ public class BlogSharingIntegrationTest extends BriarTestCase {
volatile boolean requestReceived = false;
volatile boolean responseReceived = false;
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
State s = event.getState();
ClientId c = event.getClientId();
if ((s == DELIVERED || s == INVALID) &&
c.equals(blogSharingManager0.getClientId()) &&
!event.isLocal()) {
LOG.info("TEST: Sharer received message in group " +
event.getMessage().getGroupId().hashCode());
msgWaiter.resume();
} else if (s == DELIVERED && !event.isLocal() &&
c.equals(blogManager0.getClientId())) {
LOG.info("TEST: Sharer received blog post");
if ((s == DELIVERED || s == INVALID) && !event.isLocal()) {
LOG.info("TEST: Sharer received message");
msgWaiter.resume();
}
} else if (e instanceof BlogInvitationResponseReceivedEvent) {
@@ -572,20 +564,13 @@ public class BlogSharingIntegrationTest extends BriarTestCase {
this(accept, true);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
State s = event.getState();
ClientId c = event.getClientId();
if ((s == DELIVERED || s == INVALID) &&
c.equals(blogSharingManager0.getClientId()) &&
!event.isLocal()) {
LOG.info("TEST: Invitee received message in group " +
event.getMessage().getGroupId().hashCode());
msgWaiter.resume();
} else if (s == DELIVERED && !event.isLocal() &&
c.equals(blogManager0.getClientId())) {
LOG.info("TEST: Invitee received blog post");
if ((s == DELIVERED || s == INVALID) && !event.isLocal()) {
LOG.info("TEST: Invitee received message");
msgWaiter.resume();
}
} else if (e instanceof BlogInvitationReceivedEvent) {

View File

@@ -57,7 +57,6 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGT
import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.api.sync.ValidationManager.State.INVALID;
import static org.briarproject.api.sync.ValidationManager.State.PENDING;
import static org.briarproject.api.sync.ValidationManager.State.VALID;
import static org.junit.Assert.assertTrue;
public class ForumManagerTest {
@@ -298,7 +297,7 @@ public class ForumManagerTest {
assertEquals(1, forumManager0.getPostHeaders(g).size());
assertEquals(0, forumManager1.getPostHeaders(g).size());
// send posts to 1
// send the child post to 1
sync0To1();
validationWaiter.await(TIMEOUT, 1);
assertEquals(1, forumManager0.getPostHeaders(g).size());
@@ -327,14 +326,14 @@ public class ForumManagerTest {
}
private class Listener implements EventListener {
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
if (!event.isLocal()) {
if (event.getState() == DELIVERED) {
deliveryWaiter.resume();
} else if (event.getState() == VALID ||
event.getState() == INVALID ||
} else if (event.getState() == INVALID ||
event.getState() == PENDING) {
validationWaiter.resume();
}

View File

@@ -38,7 +38,6 @@ import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.sharing.InvitationItem;
import org.briarproject.api.sharing.InvitationMessage;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.SyncSession;
import org.briarproject.api.sync.SyncSessionFactory;
@@ -928,20 +927,13 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
volatile boolean requestReceived = false;
volatile boolean responseReceived = false;
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
State s = event.getState();
ClientId c = event.getClientId();
if ((s == DELIVERED || s == INVALID) &&
c.equals(forumSharingManager0.getClientId()) &&
!event.isLocal()) {
LOG.info("TEST: Sharer received message in group " +
event.getMessage().getGroupId().hashCode());
msgWaiter.resume();
} else if (s == DELIVERED && !event.isLocal() &&
c.equals(forumManager0.getClientId())) {
LOG.info("TEST: Sharer received forum post");
if ((s == DELIVERED || s == INVALID) && !event.isLocal()) {
LOG.info("TEST: Sharer received message");
msgWaiter.resume();
}
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
@@ -986,20 +978,13 @@ public class ForumSharingIntegrationTest extends BriarTestCase {
this(accept, true);
}
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
State s = event.getState();
ClientId c = event.getClientId();
if ((s == DELIVERED || s == INVALID) &&
c.equals(forumSharingManager0.getClientId()) &&
!event.isLocal()) {
LOG.info("TEST: Invitee received message in group " +
event.getMessage().getGroupId().hashCode());
msgWaiter.resume();
} else if (s == DELIVERED && !event.isLocal() &&
c.equals(forumManager0.getClientId())) {
LOG.info("TEST: Invitee received forum post");
if ((s == DELIVERED || s == INVALID) && !event.isLocal()) {
LOG.info("TEST: Invitee received message");
msgWaiter.resume();
}
} else if (e instanceof ForumInvitationReceivedEvent) {

View File

@@ -1,11 +1,17 @@
package org.briarproject;
import android.support.annotation.Nullable;
import net.jodah.concurrentunit.Waiter;
import org.briarproject.api.FormatException;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.SessionId;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyPair;
import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfEntry;
@@ -23,18 +29,18 @@ import org.briarproject.api.event.MessageStateChangedEvent;
import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.introduction.IntroducerProtocolState;
import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.lifecycle.LifecycleManager;
import org.briarproject.api.properties.TransportProperties;
import org.briarproject.api.properties.TransportPropertyManager;
import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.SyncSession;
import org.briarproject.api.sync.SyncSessionFactory;
import org.briarproject.api.sync.ValidationManager;
import org.briarproject.api.sync.ValidationManager.State;
import org.briarproject.api.system.Clock;
import org.briarproject.contact.ContactModule;
@@ -56,6 +62,7 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -66,13 +73,17 @@ import javax.inject.Inject;
import static org.briarproject.TestPluginsModule.MAX_LATENCY;
import static org.briarproject.TestPluginsModule.TRANSPORT_ID;
import static org.briarproject.api.clients.MessageQueueManager.QUEUE_STATE_KEY;
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
import static org.briarproject.api.introduction.IntroductionConstants.STATE;
import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.api.sync.ValidationManager.State.INVALID;
import static org.junit.Assert.assertEquals;
@@ -81,17 +92,23 @@ import static org.junit.Assert.assertTrue;
public class IntroductionIntegrationTest extends BriarTestCase {
LifecycleManager lifecycleManager0, lifecycleManager1, lifecycleManager2;
SyncSessionFactory sync0, sync1, sync2;
ContactManager contactManager0, contactManager1, contactManager2;
ContactId contactId0, contactId1, contactId2;
IdentityManager identityManager0, identityManager1, identityManager2;
LocalAuthor author0, author1, author2;
private LifecycleManager lifecycleManager0, lifecycleManager1,
lifecycleManager2;
private SyncSessionFactory sync0, sync1, sync2;
private ContactManager contactManager0, contactManager1, contactManager2;
private ContactId contactId0, contactId1, contactId2;
private IdentityManager identityManager0, identityManager1,
identityManager2;
private LocalAuthor author0, author1, author2;
@Inject
Clock clock;
@Inject
CryptoComponent crypto;
@Inject
AuthorFactory authorFactory;
@Inject
IntroductionGroupFactory introductionGroupFactory;
// objects accessed from background threads need to be volatile
private volatile IntroductionManager introductionManager0;
@@ -155,32 +172,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
public void testIntroductionSession() throws Exception {
startLifecycles();
try {
// Add Identities
// Add Identities And Contacts
addDefaultIdentities();
addDefaultContacts();
// Add Transport Properties
addTransportProperties();
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
contactId2 = contactManager0.addContact(author2,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0,
author1.getId(), master, clock.currentTimeMillis(), true,
true, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), true,
true, true
);
assertTrue(contactId0.equals(contactId02));
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
@@ -260,29 +258,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
public void testIntroductionSessionFirstDecline() throws Exception {
startLifecycles();
try {
// Add Identities
// Add Identities And Contacts
addDefaultIdentities();
addDefaultContacts();
// Add Transport Properties
addTransportProperties();
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1, author0.getId(),
master, clock.currentTimeMillis(), true, true, true
);
contactId2 = contactManager0.addContact(author2, author0.getId(),
master, clock.currentTimeMillis(), true, true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0, author1.getId(),
master, clock.currentTimeMillis(), true, true, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), true,
true, true
);
assertTrue(contactId0.equals(contactId02));
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
@@ -318,7 +300,7 @@ public class IntroductionIntegrationTest extends BriarTestCase {
assertTrue(listener0.response2Received);
// sync first forwarded response
deliverMessage(sync0, contactId0, sync2, contactId2);
deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
// note how the introducer does not forward the second response,
// because after the first decline the protocol finished
@@ -353,29 +335,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
public void testIntroductionSessionSecondDecline() throws Exception {
startLifecycles();
try {
// Add Identities
// Add Identities And Contacts
addDefaultIdentities();
addDefaultContacts();
// Add Transport Properties
addTransportProperties();
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1, author0.getId(),
master, clock.currentTimeMillis(), true, true, true
);
contactId2 = contactManager0.addContact(author2, author0.getId(),
master, clock.currentTimeMillis(), true, true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0, author1.getId(),
master, clock.currentTimeMillis(), false, true, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), false,
true, true
);
assertTrue(contactId0.equals(contactId02));
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
@@ -441,29 +407,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
public void testIntroductionSessionDelayedFirstDecline() throws Exception {
startLifecycles();
try {
// Add Identities
// Add Identities And Contacts
addDefaultIdentities();
addDefaultContacts();
// Add Transport Properties
addTransportProperties();
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1, author0.getId(),
master, clock.currentTimeMillis(), true, true, true
);
contactId2 = contactManager0.addContact(author2, author0.getId(),
master, clock.currentTimeMillis(), true, true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0, author1.getId(),
master, clock.currentTimeMillis(), false, true, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), false,
true, true
);
assertTrue(contactId0.equals(contactId02));
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
@@ -520,21 +470,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
public void testIntroductionToSameContact() throws Exception {
startLifecycles();
try {
// Add Identities
// Add Identities And Contacts
addDefaultIdentities();
addDefaultContacts();
// Add Transport Properties
addTransportProperties();
// Add introducee as contact
contactId1 = contactManager0.addContact(author1, author0.getId(),
master, clock.currentTimeMillis(), true, true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0, author1.getId(),
master, clock.currentTimeMillis(), true, true, true
);
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
@@ -670,32 +612,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
public void testSessionIdReuse() throws Exception {
startLifecycles();
try {
// Add Identities
// Add Identities And Contacts
addDefaultIdentities();
addDefaultContacts();
// Add Transport Properties
addTransportProperties();
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
contactId2 = contactManager0.addContact(author2,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0,
author1.getId(), master, clock.currentTimeMillis(), true,
true, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), true,
true, true
);
assertTrue(contactId0.equals(contactId02));
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
@@ -767,32 +690,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
public void testIntroducerRemovedCleanup() throws Exception {
startLifecycles();
try {
// Add Identities
// Add Identities And Contacts
addDefaultIdentities();
addDefaultContacts();
// Add Transport Properties
addTransportProperties();
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
contactId2 = contactManager0.addContact(author2,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0,
author1.getId(), master, clock.currentTimeMillis(), true,
true, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), true,
true, true
);
assertTrue(contactId0.equals(contactId02));
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
@@ -853,32 +757,13 @@ public class IntroductionIntegrationTest extends BriarTestCase {
public void testIntroduceesRemovedCleanup() throws Exception {
startLifecycles();
try {
// Add Identities
// Add Identities And Contacts
addDefaultIdentities();
addDefaultContacts();
// Add Transport Properties
addTransportProperties();
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
contactId2 = contactManager0.addContact(author2,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0,
author1.getId(), master, clock.currentTimeMillis(), true,
true, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), true,
true, true
);
assertTrue(contactId0.equals(contactId02));
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
@@ -949,7 +834,133 @@ public class IntroductionIntegrationTest extends BriarTestCase {
}
}
// TODO add a test for faking responses when #256 is implemented
@Test
public void testModifiedResponse() throws Exception {
startLifecycles();
try {
addDefaultIdentities();
addDefaultContacts();
addTransportProperties();
// listen to events
IntroducerListener listener0 = new IntroducerListener();
t0.getEventBus().addListener(listener0);
IntroduceeListener listener1 = new IntroduceeListener(1, true);
t1.getEventBus().addListener(listener1);
IntroduceeListener listener2 = new IntroduceeListener(2, true);
t2.getEventBus().addListener(listener2);
// make introduction
long time = clock.currentTimeMillis();
Contact introducee1 = contactManager0.getContact(contactId1);
Contact introducee2 = contactManager0.getContact(contactId2);
introductionManager0
.makeIntroduction(introducee1, introducee2, "Hi!", time);
// sync request messages
deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
eventWaiter.await(TIMEOUT, 2);
// sync first response
deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
eventWaiter.await(TIMEOUT, 1);
// get response to be forwarded
MessageId responseId = null;
BdfDictionary response = null;
Group g2 = introductionGroupFactory
.createIntroductionGroup(introducee2);
ClientHelper clientHelper0 = t0.getClientHelper();
Map<MessageId, BdfDictionary> map =
clientHelper0.getMessageMetadataAsDictionary(g2.getId());
for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
if (entry.getValue().getLong(TYPE) == TYPE_RESPONSE) {
responseId = entry.getKey();
response = entry.getValue();
}
}
assertTrue(responseId != null && response != null);
// adapt outgoing message queue to removed message
decreaseOutgoingMessageCounter(clientHelper0, g2.getId(), 1);
// modify response by changing transport properties
BdfDictionary tp = response.getDictionary(TRANSPORT);
tp.put("fakeId", BdfDictionary.of(new BdfEntry("fake", "fake")));
response.put(TRANSPORT, tp);
// replace original response with modified one
MessageSender sender0 = t0.getMessageSender();
DatabaseComponent db0 = t0.getDatabaseComponent();
Transaction txn = db0.startTransaction(false);
try {
db0.deleteMessage(txn, responseId);
sender0.sendMessage(txn, response);
txn.setComplete();
} finally {
db0.endTransaction(txn);
}
// sync second response
deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0");
eventWaiter.await(TIMEOUT, 1);
// sync forwarded responses to introducees
deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
// sync first ACK and its forward
deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0");
deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
// sync second ACK and forward it
deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0");
deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
// introducee2 should have detected the fake now
// and deleted introducee1 again
Collection<Contact> contacts2;
DatabaseComponent db2 = t2.getDatabaseComponent();
txn = db2.startTransaction(true);
try {
contacts2 = db2.getContacts(txn);
txn.setComplete();
} finally {
db2.endTransaction(txn);
}
assertEquals(1, contacts2.size());
// sync abort message to introducer
deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0");
// ensure introducer got the abort
SessionId sessionId = new SessionId(response.getRaw(SESSION_ID));
BdfDictionary state =
clientHelper0.getMessageMetadataAsDictionary(sessionId);
assertEquals(IntroducerProtocolState.ERROR.getValue(),
state.getLong(STATE).intValue());
// sync abort messages to introducees
deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2");
// although aborted, introducee1 keeps the contact,
// so introducer can not make contacts disappear by sending abort
Collection<Contact> contacts1;
DatabaseComponent db1 = t1.getDatabaseComponent();
txn = db1.startTransaction(true);
try {
contacts1 = db1.getContacts(txn);
txn.setComplete();
} finally {
db1.endTransaction(txn);
}
assertEquals(2, contacts1.size());
} finally {
stopLifecycles();
}
}
@After
public void tearDown() throws InterruptedException {
@@ -992,20 +1003,48 @@ public class IntroductionIntegrationTest extends BriarTestCase {
}
private void addDefaultIdentities() throws DbException {
author0 = authorFactory.createLocalAuthor(INTRODUCER,
TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
TestUtils.getRandomBytes(123));
KeyPair keyPair0 = crypto.generateSignatureKeyPair();
byte[] publicKey0 = keyPair0.getPublic().getEncoded();
byte[] privateKey0 = keyPair0.getPrivate().getEncoded();
author0 = authorFactory
.createLocalAuthor(INTRODUCER, publicKey0, privateKey0);
identityManager0.addLocalAuthor(author0);
author1 = authorFactory.createLocalAuthor(INTRODUCEE1,
TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
TestUtils.getRandomBytes(123));
KeyPair keyPair1 = crypto.generateSignatureKeyPair();
byte[] publicKey1 = keyPair1.getPublic().getEncoded();
byte[] privateKey1 = keyPair1.getPrivate().getEncoded();
author1 = authorFactory
.createLocalAuthor(INTRODUCEE1, publicKey1, privateKey1);
identityManager1.addLocalAuthor(author1);
author2 = authorFactory.createLocalAuthor(INTRODUCEE2,
TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
TestUtils.getRandomBytes(123));
KeyPair keyPair2 = crypto.generateSignatureKeyPair();
byte[] publicKey2 = keyPair2.getPublic().getEncoded();
byte[] privateKey2 = keyPair2.getPrivate().getEncoded();
author2 = authorFactory
.createLocalAuthor(INTRODUCEE2, publicKey2, privateKey2);
identityManager2.addLocalAuthor(author2);
}
private void addDefaultContacts() throws DbException {
// Add introducees as contacts
contactId1 = contactManager0.addContact(author1,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
contactId2 = contactManager0.addContact(author2,
author0.getId(), master, clock.currentTimeMillis(), true,
true, true
);
// Add introducer back
contactId0 = contactManager1.addContact(author0,
author1.getId(), master, clock.currentTimeMillis(), true,
true, true
);
ContactId contactId02 = contactManager2.addContact(author0,
author2.getId(), master, clock.currentTimeMillis(), true,
true, true
);
assertTrue(contactId0.equals(contactId02));
}
private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
SyncSessionFactory toSync, ContactId toId)
throws IOException, TimeoutException {
@@ -1013,7 +1052,7 @@ public class IntroductionIntegrationTest extends BriarTestCase {
}
private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
SyncSessionFactory toSync, ContactId toId, String debug)
SyncSessionFactory toSync, ContactId toId, @Nullable String debug)
throws IOException, TimeoutException {
if (debug != null) LOG.info("TEST: Sending message from " + debug);
@@ -1050,14 +1089,14 @@ public class IntroductionIntegrationTest extends BriarTestCase {
private class IntroduceeListener implements EventListener {
public volatile boolean requestReceived = false;
public volatile boolean succeeded = false;
public volatile boolean aborted = false;
private volatile boolean requestReceived = false;
private volatile boolean succeeded = false;
private volatile boolean aborted = false;
private final int introducee;
private final boolean accept;
IntroduceeListener(int introducee, boolean accept) {
private IntroduceeListener(int introducee, boolean accept) {
this.introducee = introducee;
this.accept = accept;
}
@@ -1067,13 +1106,9 @@ public class IntroductionIntegrationTest extends BriarTestCase {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
State s = event.getState();
ClientId c = event.getClientId();
if ((s == DELIVERED || s == INVALID) &&
c.equals(introductionManager0.getClientId()) &&
!event.isLocal()) {
if ((s == DELIVERED || s == INVALID) && !event.isLocal()) {
LOG.info("TEST: Introducee" + introducee +
" received message in group " +
event.getMessage().getGroupId().hashCode());
" received message");
msgWaiter.resume();
}
} else if (e instanceof IntroductionRequestReceivedEvent) {
@@ -1107,6 +1142,7 @@ public class IntroductionIntegrationTest extends BriarTestCase {
}
}
} catch (DbException | IOException exception) {
msgWaiter.rethrow(exception);
eventWaiter.rethrow(exception);
} finally {
eventWaiter.resume();
@@ -1126,19 +1162,16 @@ public class IntroductionIntegrationTest extends BriarTestCase {
private class IntroducerListener implements EventListener {
public volatile boolean response1Received = false;
public volatile boolean response2Received = false;
public volatile boolean aborted = false;
private volatile boolean response1Received = false;
private volatile boolean response2Received = false;
private volatile boolean aborted = false;
@Override
public void eventOccurred(Event e) {
if (e instanceof MessageStateChangedEvent) {
MessageStateChangedEvent event = (MessageStateChangedEvent) e;
if (event.getState() == DELIVERED && event.getClientId()
.equals(introductionManager0.getClientId()) &&
!event.isLocal()) {
LOG.info("TEST: Introducer received message in group " +
event.getMessage().getGroupId().hashCode());
if (event.getState() == DELIVERED && !event.isLocal()) {
LOG.info("TEST: Introducer received message");
msgWaiter.resume();
}
} else if (e instanceof IntroductionResponseReceivedEvent) {
@@ -1160,6 +1193,16 @@ public class IntroductionIntegrationTest extends BriarTestCase {
}
}
private void decreaseOutgoingMessageCounter(ClientHelper clientHelper,
GroupId g, int num) throws FormatException, DbException {
BdfDictionary gD = clientHelper.getGroupMetadataAsDictionary(g);
LOG.warning(gD.toString());
BdfDictionary queue = gD.getDictionary(QUEUE_STATE_KEY);
queue.put("nextOut", queue.getLong("nextOut") - num);
gD.put(QUEUE_STATE_KEY, queue);
clientHelper.mergeGroupMetadata(g, gD);
}
private void injectEagerSingletons(
IntroductionIntegrationTestComponent component) {

View File

@@ -1,5 +1,6 @@
package org.briarproject;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.event.EventBus;
@@ -85,6 +86,8 @@ public interface IntroductionIntegrationTestComponent {
DatabaseComponent getDatabaseComponent();
ClientHelper getClientHelper();
MessageSender getMessageSender();
IntroductionGroupFactory getIntroductionGroupFactory();

View File

@@ -211,6 +211,17 @@
/>
</activity>
<activity
android:name=".android.blogs.ReblogActivity"
android:label="@string/blogs_reblog_button"
android:parentActivityName=".android.blogs.BlogActivity"
android:windowSoftInputMode="stateHidden">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.blogs.BlogActivity"
/>
</activity>
<activity
android:name=".android.blogs.RssFeedImportActivity"
android:label="@string/blogs_rss_feeds_import"

View File

@@ -11,9 +11,6 @@ dependencies {
compile project(':briar-api')
compile project(':briar-core')
compile fileTree(dir: 'libs', include: '*.jar')
// This shouldn't be necessary; per section 23.4.4 of the Gradle docs:
// "file dependencies are included in transitive project dependencies within the same build".
compile files('../briar-core/libs/jsocks.jar')
compile "com.android.support:support-v4:$supportVersion"
compile("com.android.support:appcompat-v7:$supportVersion") {
@@ -27,6 +24,7 @@ dependencies {
exclude module: 'support-v4'
exclude module: 'recyclerview-v7'
}
compile "com.android.support:cardview-v7:$supportVersion"
compile('ch.acra:acra:4.8.5') {
exclude module: 'support-v4'
exclude module: 'support-annotations'
@@ -61,6 +59,7 @@ dependencyVerification {
'com.android.support:animated-vector-drawable:06d1963b85aa917099d7757e6a7b3e4dc06889413dc747f625ae8683606db3a1',
'com.android.support:support-vector-drawable:799bafe4c3de812386f0b291f744d5d6876452722dd40189b9ab87dbbf594ea1',
'com.android.support:recyclerview-v7:44040a888e23e0c93162a3377cfe06751080e3c22d369ab0d4301ef60d63b0fe',
'com.android.support:cardview-v7:4595f1c4a28cfa083b6c0920ad4d49e1c2ca4b8302a955e548f68eb63b74931b',
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,5 @@
<vector android:alpha="0.54" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M15.73,3L8.27,3L3,8.27v7.46L8.27,21h7.46L21,15.73L21,8.27L15.73,3zM12,17.3c-0.72,0 -1.3,-0.58 -1.3,-1.3 0,-0.72 0.58,-1.3 1.3,-1.3 0.72,0 1.3,0.58 1.3,1.3 0,0.72 -0.58,1.3 -1.3,1.3zM13,13h-2L11,7h2v6z"/>
</vector>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:radius="@dimen/unread_bubble_size"/>
<solid
android:color="@color/briar_text_primary_inverse"/>
<stroke
android:color="@color/briar_text_primary"
android:width="1dp"/>
</shape>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:alpha="0.54"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,5.9c1.16,0 2.1,0.94 2.1,2.1s-0.94,2.1 -2.1,2.1S9.9,9.16 9.9,8s0.94,-2.1 2.1,-2.1m0,9c2.97,0 6.1,1.46 6.1,2.1v1.1L5.9,18.1L5.9,17c0,-0.64 3.13,-2.1 6.1,-2.1M12,4C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,13c-2.67,0 -8,1.34 -8,4v3h16v-3c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@@ -4,6 +4,6 @@
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
android:fillColor="#FFFFFFFF"
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
</vector>

View File

@@ -6,5 +6,5 @@
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
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,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
@@ -22,6 +23,7 @@
android:src="@drawable/qr_code_intro"/>
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
@@ -37,22 +39,6 @@
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical">
<TextView
style="@style/BriarTextBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/your_nickname"
android:visibility="gone"/>
<Spinner
android:id="@+id/spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:background="@drawable/border_spinner"
android:spinnerMode="dropdown"
android:visibility="gone"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -65,7 +51,8 @@
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:padding="@dimen/margin_medium"
android:src="@drawable/qr_code_explanation"/>
android:src="@drawable/qr_code_explanation"
tools:ignore="ContentDescription"/>
<TextView
style="@style/BriarTextBody"

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="@+id/fragmentContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="invisible"/>
</FrameLayout>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="@+id/introductionContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="@layout/list_item_blog_post">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
style="@style/BriarAvatar"
android:layout_width="@dimen/blogs_avatar_normal_size"
android:layout_height="@dimen/blogs_avatar_normal_size"
android:layout_centerVertical="true"
android:layout_marginRight="@dimen/margin_medium"
tools:src="@drawable/ic_launcher"/>
<ImageView
android:id="@+id/avatarIcon"
android:layout_width="@dimen/blogs_avatar_icon_size"
android:layout_height="@dimen/blogs_avatar_icon_size"
android:layout_alignBottom="@+id/avatar"
android:layout_alignRight="@+id/avatar"
android:background="@drawable/bubble_white"
android:padding="2dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_repeat"
android:visibility="invisible"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/authorName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_small"
tools:text="Author Name"/>
<org.briarproject.android.util.TrustIndicatorView
android:id="@+id/trustIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/authorName"
android:layout_alignTop="@+id/authorName"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@id/authorName"
android:scaleType="center"
tools:src="@drawable/trust_indicator_verified"/>
<TextView
android:id="@+id/dateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/authorName"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:gravity="bottom"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_tiny"
tools:text="yesterday"/>
</merge>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarView"
style="@style/BriarAvatar"
android:layout_width="@dimen/dropdown_picture_size"
android:layout_height="@dimen/dropdown_picture_size"
android:layout_margin="@dimen/margin_small"
tools:src="@drawable/ic_launcher"/>
<TextView
android:id="@+id/nameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textSize="@dimen/text_size_medium"
tools:text="This is a name of an author. It can be quite long."/>
</LinearLayout>

View File

@@ -1,78 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/margin_activity_horizontal">
android:descendantFocusability="beforeDescendants"
android:focusable="true"
android:focusableInTouchMode="true">
<!-- Above focusability attributes prevent automatic scroll-down,
because body text is selectable -->
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
style="@style/BriarAvatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginRight="@dimen/margin_medium"
tools:src="@drawable/ic_launcher"/>
<include
android:id="@+id/postLayout"
style="@style/BriarCard"
layout="@layout/list_item_blog_post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/authorName"
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_tiny"
tools:text="Author Name"/>
android:layout_gravity="center"/>
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/avatar"
android:layout_below="@+id/authorName"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:gravity="bottom"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_tiny"
tools:text="yesterday"/>
<org.briarproject.android.util.TrustIndicatorView
android:id="@+id/trustIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@+id/authorName"
tools:src="@drawable/trust_indicator_verified"/>
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/avatar"
android:layout_marginTop="@dimen/margin_medium"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_large"
android:textStyle="bold"
tools:text="This Is A Blog Post Title"/>
<TextView
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/title"
android:layout_marginTop="@dimen/margin_medium"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_medium"
tools:text="Body of Blog Post. This could be insanely large or just a short text as well."/>
</RelativeLayout>
</FrameLayout>
</ScrollView>

View File

@@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.ViewPager
android:id="@+id/pager"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".android.blogs.BlogActivity"/>
android:layout_height="match_parent"/>
<ProgressBar
android:id="@+id/progressBar"

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
style="@style/BriarTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blogs_feed"/>
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blogs_my_blogs"/>
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blogs_blog_list"/>
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blogs_available_blogs"/>
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/blogs_drafts"/>
</android.support.design.widget.TabLayout>
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<org.briarproject.android.util.BriarRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/list_item_blog"/>

View File

@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
android:id="@+id/scrollView"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -15,22 +17,6 @@
android:paddingStart="@dimen/margin_activity_horizontal"
android:paddingTop="@dimen/margin_activity_vertical">
<TextView
style="@style/BriarTextBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/your_nickname"
android:visibility="gone"/>
<Spinner
android:id="@+id/spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:background="@drawable/border_spinner"
android:spinnerMode="dropdown"
android:visibility="gone"/>
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
@@ -38,7 +24,8 @@
android:layout_marginTop="@dimen/margin_xlarge"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/qr_code_intro"/>
android:src="@drawable/qr_code_intro"
tools:ignore="ContentDescription"/>
<LinearLayout
android:layout_width="match_parent"

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/margin_large">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/link_warning_title"
android:textColor="@color/briar_primary"
android:textSize="@dimen/text_size_large"
android:textStyle="bold"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:text="@string/link_warning_intro"
android:textColor="@color/briar_primary"
android:textSize="@dimen/text_size_medium"/>
<TextView
android:id="@+id/urlView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:textIsSelectable="true"
android:typeface="monospace"
tools:text="http://very.bad.site.com"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_large"
android:text="@string/link_warning_text"
android:textColor="@color/briar_primary"
android:textSize="@dimen/text_size_medium"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/cancelButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:text="@string/cancel"/>
<Button
android:id="@+id/openButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:text="@string/link_warning_open_link"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
android:id="@+id/scrollView"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/window_background">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_small">
<include
android:id="@+id/postLayout"
layout="@layout/list_item_blog_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
<EditText
android:id="@+id/inputText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/postLayout"
android:layout_margin="@dimen/listitem_vertical_margin"
android:gravity="bottom"
android:hint="@string/blogs_reblog_comment_hint"
android:inputType="textShortMessage|textMultiLine|textCapSentences|textAutoCorrect"/>
<Button
android:id="@+id/publishButton"
style="@style/BriarButton"
android:layout_below="@+id/inputText"
android:enabled="false"
android:text="@string/blogs_reblog_button"/>
</RelativeLayout>
</ScrollView>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:showIn="@layout/list_item_blog_post">
<View
android:id="@+id/inputDivider"
style="@style/Divider.Horizontal"/>
<org.briarproject.android.util.AuthorView
android:id="@+id/authorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:padding="@dimen/listitem_vertical_margin"
app:persona="commenter"/>
<TextView
android:id="@+id/bodyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/authorView"
android:paddingBottom="@dimen/listitem_vertical_margin"
android:paddingLeft="@dimen/listitem_vertical_margin"
android:paddingRight="@dimen/listitem_vertical_margin"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_small"
tools:text="This is a comment that appears below a blog post. Usually, it is expected to be rather short. Not much longer than this one."/>
</RelativeLayout>

View File

@@ -1,118 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
<android.support.v7.widget.CardView
android:id="@+id/postLayout"
style="@style/BriarCard"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:layout_marginTop="@dimen/listitem_vertical_margin"
android:background="?attr/selectableItemBackground">
android:foreground="?android:attr/selectableItemBackground">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatar"
style="@style/BriarAvatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_marginRight="@dimen/margin_medium"
tools:src="@drawable/ic_launcher"/>
<TextView
android:id="@+id/authorName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_tiny"
tools:text="Author Name"/>
<TextView
android:id="@+id/dateView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/avatar"
android:layout_below="@+id/authorName"
android:layout_toEndOf="@+id/avatar"
android:layout_toRightOf="@+id/avatar"
android:gravity="bottom"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_tiny"
tools:text="yesterday"/>
<TextView
android:id="@+id/newView"
style="@style/BriarTag"
android:layout_alignBottom="@+id/dateView"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@+id/dateView"
android:text="@string/tag_new"
android:visibility="gone"/>
<org.briarproject.android.util.TrustIndicatorView
android:id="@+id/trustIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/authorName"
android:layout_alignTop="@+id/authorName"
android:layout_marginLeft="@dimen/margin_small"
android:layout_toRightOf="@+id/authorName"
android:scaleType="center"
tools:src="@drawable/trust_indicator_verified"/>
<ImageView
android:id="@+id/chatView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/commentView"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_chat"
android:visibility="gone"/>
<ImageView
android:id="@+id/commentView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_repeat"
android:visibility="gone"/>
<TextView
android:id="@+id/titleView"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/avatar"
android:layout_marginBottom="@dimen/margin_medium"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:ellipsize="end"
android:maxLines="3"
android:textColor="@color/briar_text_primary"
android:textSize="@dimen/text_size_large"
android:visibility="gone"
tools:text="This is a blog post title which can also be longer"/>
android:orientation="vertical">
<TextView
android:id="@+id/bodyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/titleView"
android:layout_marginEnd="@dimen/margin_medium"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_medium"
tools:text="This is a body text that shows the content of a blog post. This one is not short, but it is also not too long."/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/listitem_vertical_margin">
<View
style="@style/Divider.ForumList"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/bodyView"
android:layout_marginTop="@dimen/listitem_vertical_margin"/>
<org.briarproject.android.util.AuthorView
android:id="@+id/rebloggerView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="@dimen/listitem_horizontal_margin"
android:layout_toLeftOf="@+id/commentView"
app:persona="reblogger"/>
</RelativeLayout>
<org.briarproject.android.util.AuthorView
android:id="@+id/authorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_below="@+id/rebloggerView"
android:layout_marginBottom="@dimen/listitem_vertical_margin"
android:layout_toLeftOf="@+id/commentView"/>
<ImageView
android:id="@+id/commentView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:contentDescription="@string/blogs_reblog_comment_hint"
android:padding="@dimen/margin_small"
android:src="@drawable/ic_repeat"/>
<TextView
android:id="@+id/bodyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/authorView"
android:textColor="@color/briar_text_secondary"
android:textSize="@dimen/text_size_medium"
tools:text="This is a body text that shows the content of a blog post.\n\nThis one is not short, but it is also not too long."/>
</RelativeLayout>
<LinearLayout
android:id="@+id/commentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
layout="@layout/list_item_blog_comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>

View File

@@ -6,37 +6,45 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/messageLayout"
layout="@layout/list_item_msg_in"/>
<TextView
android:id="@+id/msgBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:background="@drawable/msg_in_top"
android:textColor="@color/briar_text_primary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
tools:text="Short message"/>
<RelativeLayout
android:layout_width="wrap_content"
android:id="@+id/noticeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="left|start"
android:background="@drawable/notice_in"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:background="@drawable/notice_in_bottom">
<TextView
android:id="@+id/introductionText"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="80dp"
android:textColor="@color/briar_text_secondary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
android:textColor="@color/briar_text_secondary"
tools:text="@string/introduction_request_received"/>
<TextView
android:id="@+id/introductionTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_below="@+id/declineButton"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
@@ -56,8 +64,8 @@
style="@style/BriarButtonFlat.Negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="-15dp"
android:layout_below="@+id/introductionText"
android:layout_marginBottom="-15dp"
android:layout_toLeftOf="@+id/acceptButton"
android:layout_toStartOf="@+id/acceptButton"
android:text="@string/decline"/>

View File

@@ -14,9 +14,9 @@
android:id="@+id/msgBody"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/briar_text_primary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textColor="@color/briar_text_primary"
tools:text="Short message"/>
<TextView

View File

@@ -6,36 +6,44 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/messageLayout"
layout="@layout/list_item_msg_out"/>
<TextView
android:id="@+id/msgBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_tail"
android:background="@drawable/msg_out_top"
android:textColor="@color/briar_text_primary_inverse"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
<RelativeLayout
android:layout_width="wrap_content"
android:id="@+id/noticeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
android:background="@drawable/notice_out"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_tail">
android:layout_marginRight="@dimen/message_bubble_margin_tail"
android:background="@drawable/notice_out_bottom">
<TextView
android:id="@+id/introductionText"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/briar_text_secondary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
android:textColor="@color/briar_text_secondary"
tools:text="@string/introduction_request_received"/>
<TextView
android:id="@+id/introductionTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/introductionText"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
@@ -44,10 +52,10 @@
android:id="@+id/introductionStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/introductionTime"
android:layout_toRightOf="@+id/introductionTime"
android:layout_alignBottom="@+id/introductionTime"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_toEndOf="@+id/introductionTime"
android:layout_toRightOf="@+id/introductionTime"
tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered"/>

View File

@@ -11,9 +11,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
android:background="@drawable/msg_out"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_tail">
android:layout_marginRight="@dimen/message_bubble_margin_tail"
android:background="@drawable/msg_out">
<TextView
android:id="@+id/msgBody"
@@ -28,11 +28,11 @@
android:id="@+id/msgTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/msgBody"
android:singleLine="true"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:maxLines="1"
android:textColor="@color/private_message_date_inverse"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>

View File

@@ -6,37 +6,46 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/messageLayout"
layout="@layout/list_item_msg_in"/>
<RelativeLayout
android:layout_width="wrap_content"
<TextView
android:id="@+id/msgBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="left|start"
android:background="@drawable/notice_in"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:background="@drawable/msg_in_top"
android:textColor="@color/briar_text_primary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
tools:text="Short message"/>
<RelativeLayout
android:id="@+id/noticeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
android:background="@drawable/notice_in_bottom">
<TextView
android:id="@+id/introductionText"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="80dp"
android:textColor="@color/briar_text_secondary"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
android:textColor="@color/briar_text_secondary"
tools:text="@string/forum_invitation_received"/>
<TextView
android:id="@+id/introductionTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_below="@+id/showInvitationsButton"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
@@ -46,10 +55,10 @@
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="-15dp"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_below="@+id/introductionText"
android:layout_marginBottom="-15dp"
tools:text="@string/forum_show_invitations"/>
</RelativeLayout>

View File

@@ -1,7 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/action_write_blog_post"
android:icon="@drawable/forum_item_create_white"
android:title="@string/blogs_write_blog_post"
android:visible="false"
app:showAsAction="ifRoom"
tools:visible="true"/>
<item
android:id="@+id/action_blog_share"
@@ -18,6 +27,8 @@
android:id="@+id/action_blog_delete"
android:icon="@drawable/action_delete_white"
android:title="@string/blogs_remove_blog"
app:showAsAction="never"/>
android:visible="false"
app:showAsAction="never"
tools:visible="true"/>
</menu>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_create_blog"
android:icon="@drawable/ic_add_white"
android:title="@string/blogs_my_blogs_create"
app:showAsAction="ifRoom"/>
</menu>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_write_blog_post"
android:icon="@drawable/forum_item_create_white"
android:title="@string/blogs_write_blog_post"
app:showAsAction="ifRoom"/>
</menu>

View File

@@ -1,8 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BriarRecyclerView">
<attr name="scrollToEnd" format="boolean" />
<attr name="emptyText" format="string" />
</declare-styleable>
<declare-styleable name="AuthorView">
<attr name="persona" format="enum">
<enum name="normal" value="0"/>
<enum name="reblogger" value="1"/>
<enum name="commenter" value="2"/>
</attr>
</declare-styleable>
</resources>

View File

@@ -35,7 +35,6 @@
<color name="menu_background">#FFFFFF</color>
<color name="spinner_border">#61000000</color> <!-- 38% Black -->
<color name="spinner_arrow">@color/briar_blue_dark</color>
<color name="forum_discussion_nested_line">#cfd2d4</color>
<color name="forum_cell_highlight">#ffffff</color>
</resources>

View File

@@ -27,7 +27,6 @@
<dimen name="listitem_picture_frame_size">53dp</dimen>
<dimen name="listitem_picture_frame_offset">2dp</dimen>
<dimen name="listitem_selectable_picture_size">40dp</dimen>
<dimen name="dropdown_picture_size">32dp</dimen>
<dimen name="avatar_forum_size">48dp</dimen>
<dimen name="avatar_border_width">2dp</dimen>
<dimen name="avatar_text_size">30sp</dimen>
@@ -36,11 +35,15 @@
<dimen name="unread_bubble_padding_horizontal">6dp</dimen>
<dimen name="unread_bubble_size">19dp</dimen>
<dimen name="message_bubble_margin_tail">14dp</dimen>
<dimen name="message_bubble_margin_non_tail">51dp</dimen>
<dimen name="message_bubble_timestamp_margin">15dp</dimen>
<dimen name="message_bubble_margin_tail">3dp</dimen>
<dimen name="message_bubble_margin_non_tail">30dp</dimen>
<dimen name="message_bubble_timestamp_margin">7dp</dimen>
<dimen name="forum_nested_line_width">2dp</dimen>
<dimen name="forum_nested_indicator">24dp</dimen>
<dimen name="forum_avatar_size">20dp</dimen>
<dimen name="blogs_avatar_normal_size">30dp</dimen>
<dimen name="blogs_avatar_icon_size">15dp</dimen>
<dimen name="blogs_avatar_comment_size">20dp</dimen>
</resources>

View File

@@ -35,7 +35,7 @@
<string name="nav_drawer_close_description">Close the navigation drawer</string>
<string name="contact_list_button">Contacts</string>
<string name="forums_button">Forums</string>
<string name="blogs_button">Micro Blogs</string>
<string name="blogs_button">Blogs</string>
<string name="settings_button">Settings</string>
<string name="sign_out_button">Sign Out</string>
@@ -73,6 +73,7 @@
<string name="offline">Offline</string>
<string name="send">Send</string>
<string name="no_data">No data</string>
<string name="ellipsis"></string>
<!-- Contacts and Private Conversations-->
<string name="no_contacts">It seems that you are new here and have no contacts yet.\n\nTap the + icon at the top and follow the instructions to add some friends to your list.\n\nPlease remember: You can only add new contacts face-to-face to prevent anyone from impersonating you or reading your messages in the future.</string>
@@ -134,8 +135,10 @@
<string name="introduction_response_accepted_received">%1$s accepted the introduction to %2$s.</string>
<string name="introduction_response_declined_received">%1$s declined the introduction to %2$s.</string>
<string name="introduction_response_declined_received_by_introducee">%1$s says that %2$s declined the introduction.</string>
<string name="introduction_success_title">Introduced contact was added</string>
<string name="introduction_success_text">You have been introduced to %1$s.</string>
<plurals name="introduction_notification_text">
<item quantity="one">New contact added.</item>
<item quantity="other">%d new contacts added.</item>
</plurals>
<!-- Forums -->
<string name="no_forums">You don\'t have any forums yet.\n\nWhy don\'t you create a new one yourself by tapping the + icon at the top?\n\nYou can also ask your contacts to share forums with you.</string>
@@ -197,19 +200,16 @@
<string name="nobody">Nobody</string>
<!-- Blogs -->
<string name="blogs_feed">Feed</string>
<string name="blogs_my_blogs">My Blogs</string>
<string name="blogs_my_blogs_create">Create Blog</string>
<string name="blogs_my_blogs_label">Add new Blog</string>
<string name="blogs_my_blogs_create_hint_title">Blog title (cannot be changed later)</string>
<string name="blogs_my_blogs_create_hint_desc">A short description of your new blog</string>
<string name="blogs_my_blogs_create_hint_desc_explanation">Potential readers may or may not subscribe to your blog based on the content of the description.</string>
<string name="blogs_my_blogs_empty_state">You don\'t have any blogs.\n\nWhy don\'t you create one now by clicking the plus in the top right screen corner?</string>
<string name="blogs_my_blogs_blog_empty_state">This is the place for content of your blog.\n\nIt seems like you haven\'t written anything yet.\n\nPlease tap the pen icon to compose a new blog post.\n\nDon\'t forget to go public and share your blog.</string>
<string name="blogs_my_blogs_created">Blog created</string>
<string name="blogs_blog_is_empty">This blog is empty</string>
<string name="blogs_other_blog_empty_state">This blog is currently empty.\n\nEither the author hasn\'t written anything yet, or the person who shared this blog with you needs to come online, so posts can be synchronized.</string>
<string name="tag_new">NEW</string>
<string name="read_more">read more</string>
<string name="blogs_write_blog_post">Write Blog Post</string>
<string name="blogs_write_blog_post_title_hint">Add a title (optional)</string>
<string name="blogs_write_blog_post_body_hint">Type your blog post here</string>
@@ -225,10 +225,11 @@
<string name="blogs_remove_blog_dialog_message">Are you sure that you want to remove this blog and all posts?\nNote that this will not remove the blog from other people\'s devices.</string>
<string name="blogs_remove_blog_ok">Remove Blog</string>
<string name="blogs_blog_removed">Blog Removed</string>
<string name="blogs_reblog_comment_hint">Add an optional comment</string>
<string name="blogs_reblog_button">Reblog</string>
<string name="blogs_blog_list">Blog List</string>
<string name="blogs_available_blogs">Available Blogs</string>
<string name="blogs_drafts">Drafts</string>
<!-- Blog Sharing -->
<string name="blogs_sharing_share">Share Blog</string>
@@ -307,9 +308,14 @@
<string name="feedback_settings_title">Feedback</string>
<string name="send_feedback">Send feedback</string>
<!-- Link Warning -->
<string name="link_warning_title">Link Warning</string>
<string name="link_warning_intro">You are about to open the following link with an external app.</string>
<string name="link_warning_text">This can be used to identify you. Think about whether you trust the person that sent you this link and consider opening it with Orfox.</string>
<string name="link_warning_open_link">Open Link</string>
<!-- Multiple Identities -->
<string name="anonymous">Anonymous</string>
<string name="new_identity_item">New identity\u2026</string>
<string name="new_identity_title">New Identity</string>
<string name="create_identity_button">Create Identity</string>
<string name="identity_created_toast">Identity created</string>

View File

@@ -124,6 +124,11 @@
<item name="tabTextColor">@color/briar_text_primary_inverse</item>
</style>
<style name="BriarCard" parent="CardView">
<item name="cardUseCompatPadding">true</item>
<item name="android:layout_margin">@dimen/margin_small</item>
</style>
<!-- This fixes the missing TextAppearance.Design.Counter.Overflow style -->
<style name="BriarTextCounter.Overflow" parent="TextAppearance.Design.Counter">
<item name="android:textColor">@color/briar_button_negative</item>

View File

@@ -6,10 +6,13 @@ import org.briarproject.android.blogs.BlogActivity;
import org.briarproject.android.blogs.BlogFragment;
import org.briarproject.android.blogs.BlogListFragment;
import org.briarproject.android.blogs.BlogPostFragment;
import org.briarproject.android.blogs.BlogsFragment;
import org.briarproject.android.blogs.BlogPostPagerFragment;
import org.briarproject.android.blogs.CreateBlogActivity;
import org.briarproject.android.blogs.FeedPostFragment;
import org.briarproject.android.blogs.FeedFragment;
import org.briarproject.android.blogs.MyBlogsFragment;
import org.briarproject.android.blogs.FeedPostPagerFragment;
import org.briarproject.android.blogs.ReblogActivity;
import org.briarproject.android.blogs.ReblogFragment;
import org.briarproject.android.blogs.RssFeedImportActivity;
import org.briarproject.android.blogs.RssFeedManageActivity;
import org.briarproject.android.blogs.WriteBlogPostActivity;
@@ -23,7 +26,7 @@ import org.briarproject.android.introduction.ContactChooserFragment;
import org.briarproject.android.introduction.IntroductionActivity;
import org.briarproject.android.introduction.IntroductionMessageFragment;
import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.keyagreement.ChooseIdentityFragment;
import org.briarproject.android.keyagreement.IntroFragment;
import org.briarproject.android.keyagreement.KeyAgreementActivity;
import org.briarproject.android.keyagreement.ShowQrCodeFragment;
import org.briarproject.android.panic.PanicPreferencesActivity;
@@ -92,6 +95,14 @@ public interface ActivityComponent {
void inject(BlogFragment fragment);
void inject(BlogPostFragment fragment);
void inject(FeedPostFragment fragment);
void inject(BlogPostPagerFragment fragment);
void inject(FeedPostPagerFragment fragment);
void inject(ReblogFragment fragment);
void inject(ReblogActivity activity);
void inject(SettingsActivity activity);
@@ -106,11 +117,9 @@ public interface ActivityComponent {
// Fragments
void inject(ContactListFragment fragment);
void inject(ForumListFragment fragment);
void inject(BlogsFragment fragment);
void inject(BlogListFragment fragment);
void inject(FeedFragment fragment);
void inject(MyBlogsFragment fragment);
void inject(ChooseIdentityFragment fragment);
void inject(IntroFragment fragment);
void inject(ShowQrCodeFragment fragment);
void inject(ContactChooserFragment fragment);
void inject(ContactSelectorFragment fragment);

View File

@@ -2,10 +2,14 @@ package org.briarproject.android;
import android.app.Application;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.UiThread;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
@@ -14,7 +18,6 @@ import org.briarproject.android.api.AndroidExecutor;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.contact.ConversationActivity;
import org.briarproject.android.forum.ForumActivity;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException;
@@ -59,20 +62,38 @@ import static android.support.v4.app.NotificationCompat.CATEGORY_SOCIAL;
import static android.support.v4.app.NotificationCompat.VISIBILITY_SECRET;
import static java.util.logging.Level.WARNING;
import static org.briarproject.android.BriarActivity.GROUP_ID;
import static org.briarproject.android.NavDrawerActivity.INTENT_BLOGS;
import static org.briarproject.android.NavDrawerActivity.INTENT_CONTACTS;
import static org.briarproject.android.NavDrawerActivity.INTENT_FORUMS;
import static org.briarproject.android.fragment.SettingsFragment.PREF_NOTIFY_BLOG;
import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE;
class AndroidNotificationManagerImpl implements AndroidNotificationManager,
Service, EventListener {
// Notification IDs
private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3;
private static final int FORUM_POST_NOTIFICATION_ID = 4;
private static final int BLOG_POST_NOTIFICATION_ID = 5;
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 6;
// Content URIs to differentiate between pending intents
private static final String CONTACT_URI =
"content://org.briarproject/contact";
private static final String FORUM_URI =
"content://org.briarproject/forum";
private static final String BLOG_URI =
"content://org.briarproject/blog";
// Actions for intents that are broadcast when notifications are dismissed
private static final String CLEAR_PRIVATE_MESSAGE_ACTION =
"org.briarproject.briar.CLEAR_PRIVATE_MESSAGE_NOTIFICATION";
private static final String CLEAR_FORUM_ACTION =
"org.briarproject.briar.CLEAR_FORUM_NOTIFICATION";
private static final String CLEAR_BLOG_ACTION =
"org.briarproject.briar.CLEAR_BLOG_NOTIFICATION";
private static final String CLEAR_INTRODUCTION_ACTION =
"org.briarproject.briar.CLEAR_INTRODUCTION_NOTIFICATION";
private static final Logger LOG =
Logger.getLogger(AndroidNotificationManagerImpl.class.getName());
@@ -82,16 +103,19 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private final MessagingManager messagingManager;
private final AndroidExecutor androidExecutor;
private final Context appContext;
private final BroadcastReceiver receiver = new DeleteIntentReceiver();
private final AtomicBoolean used = new AtomicBoolean(false);
// The following must only be accessed on the main UI thread
private final Map<GroupId, Integer> contactCounts = new HashMap<>();
private final Map<GroupId, Integer> forumCounts = new HashMap<>();
private final AtomicBoolean used = new AtomicBoolean(false);
private final Map<GroupId, Integer> blogCounts = new HashMap<>();
private int contactTotal = 0, forumTotal = 0, blogTotal = 0;
private int introductionTotal = 0;
private int nextRequestId = 0;
private GroupId visibleGroup = null;
private boolean blogBlocked = false;
private GroupId blockedGroup = null;
private boolean blockContacts = false, blockForums = false;
private boolean blockBlogs = false, blockIntroductions = false;
private volatile Settings settings = new Settings();
@@ -109,21 +133,22 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
@Override
public void startService() throws ServiceException {
if (used.getAndSet(true)) throw new IllegalStateException();
// Load settings
try {
settings = settingsManager.getSettings(SETTINGS_NAMESPACE);
} catch (DbException e) {
throw new ServiceException(e);
}
}
@Override
public void stopService() throws ServiceException {
Future<Void> f = androidExecutor.submit(new Callable<Void>() {
// Register a broadcast receiver for notifications being dismissed
Future<Void> f = androidExecutor.runOnUiThread(new Callable<Void>() {
@Override
public Void call() {
clearPrivateMessageNotification();
clearForumPostNotification();
clearIntroductionSuccessNotification();
IntentFilter filter = new IntentFilter();
filter.addAction(CLEAR_PRIVATE_MESSAGE_ACTION);
filter.addAction(CLEAR_FORUM_ACTION);
filter.addAction(CLEAR_BLOG_ACTION);
filter.addAction(CLEAR_INTRODUCTION_ACTION);
appContext.registerReceiver(receiver, filter);
return null;
}
});
@@ -134,19 +159,57 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
}
}
@Override
public void stopService() throws ServiceException {
// Clear all notifications and unregister the broadcast receiver
Future<Void> f = androidExecutor.runOnUiThread(new Callable<Void>() {
@Override
public Void call() {
clearPrivateMessageNotification();
clearForumPostNotification();
clearBlogPostNotification();
clearIntroductionSuccessNotification();
appContext.unregisterReceiver(receiver);
return null;
}
});
try {
f.get();
} catch (InterruptedException | ExecutionException e) {
throw new ServiceException(e);
}
}
@UiThread
private void clearPrivateMessageNotification() {
contactCounts.clear();
contactTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID);
}
@UiThread
private void clearForumPostNotification() {
forumCounts.clear();
forumTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(FORUM_POST_NOTIFICATION_ID);
}
@UiThread
private void clearBlogPostNotification() {
blogCounts.clear();
blogTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(BLOG_POST_NOTIFICATION_ID);
}
@UiThread
private void clearIntroductionSuccessNotification() {
introductionTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(INTRODUCTION_SUCCESS_NOTIFICATION_ID);
@@ -158,29 +221,28 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
if (s.getNamespace().equals(SETTINGS_NAMESPACE)) loadSettings();
} else if (e instanceof PrivateMessageReceivedEvent) {
PrivateMessageReceivedEvent m = (PrivateMessageReceivedEvent) e;
showPrivateMessageNotification(m.getGroupId());
PrivateMessageReceivedEvent p = (PrivateMessageReceivedEvent) e;
showPrivateMessageNotification(p.getGroupId());
} else if (e instanceof ForumPostReceivedEvent) {
ForumPostReceivedEvent m = (ForumPostReceivedEvent) e;
showForumPostNotification(m.getGroupId());
ForumPostReceivedEvent f = (ForumPostReceivedEvent) e;
showForumPostNotification(f.getGroupId());
} else if (e instanceof BlogPostAddedEvent) {
BlogPostAddedEvent be = (BlogPostAddedEvent) e;
showBlogPostNotification(be.getGroupId());
BlogPostAddedEvent b = (BlogPostAddedEvent) e;
showBlogPostNotification(b.getGroupId());
} else if (e instanceof IntroductionRequestReceivedEvent) {
ContactId c = ((IntroductionRequestReceivedEvent) e).getContactId();
showNotificationForPrivateConversation(c);
} else if (e instanceof IntroductionResponseReceivedEvent) {
ContactId c = ((IntroductionResponseReceivedEvent) e).getContactId();
showNotificationForPrivateConversation(c);
} else if (e instanceof IntroductionSucceededEvent) {
Contact c = ((IntroductionSucceededEvent) e).getContact();
showIntroductionSucceededNotification(c);
} else if (e instanceof InvitationReceivedEvent) {
ContactId c = ((InvitationReceivedEvent) e).getContactId();
showNotificationForPrivateConversation(c);
} else if (e instanceof InvitationResponseReceivedEvent) {
ContactId c = ((InvitationResponseReceivedEvent) e).getContactId();
showNotificationForPrivateConversation(c);
} else if (e instanceof IntroductionSucceededEvent) {
showIntroductionNotification();
}
}
@@ -198,35 +260,35 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
});
}
@Override
public void showPrivateMessageNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
private void showPrivateMessageNotification(final GroupId g) {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
if (blockContacts) return;
if (g.equals(blockedGroup)) return;
Integer count = contactCounts.get(g);
if (count == null) contactCounts.put(g, 1);
else contactCounts.put(g, count + 1);
contactTotal++;
if (!g.equals(visibleGroup))
updatePrivateMessageNotification();
updatePrivateMessageNotification();
}
});
}
@Override
public void clearPrivateMessageNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
Integer count = contactCounts.remove(g);
if (count == null) return; // Already cleared
contactTotal -= count;
// FIXME: If the notification isn't showing, this may show it
updatePrivateMessageNotification();
}
});
}
@UiThread
private void updatePrivateMessageNotification() {
if (contactTotal == 0) {
clearPrivateMessageNotification();
@@ -245,7 +307,13 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
// Clear the counters if the notification is dismissed
Intent clear = new Intent(CLEAR_PRIVATE_MESSAGE_ACTION);
PendingIntent delete = PendingIntent.getBroadcast(appContext, 0,
clear, 0);
b.setDeleteIntent(delete);
if (contactCounts.size() == 1) {
// Touching the notification shows the relevant conversation
Intent i = new Intent(appContext, ConversationActivity.class);
GroupId g = contactCounts.keySet().iterator().next();
i.putExtra(GROUP_ID, g.getBytes());
@@ -257,9 +325,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
} else {
// Touching the notification shows the contact list
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(NavDrawerActivity.INTENT_CONTACTS, true);
i.putExtra(INTENT_CONTACTS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setData(Uri.parse(CONTACT_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
@@ -275,6 +345,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
}
}
@UiThread
private int getDefaults() {
int defaults = DEFAULT_LIGHTS;
boolean sound = settings.getBoolean("notifySound", true);
@@ -287,34 +358,46 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
}
@Override
public void showForumPostNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
public void clearAllContactNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
clearPrivateMessageNotification();
clearIntroductionSuccessNotification();
}
});
}
@UiThread
private void showForumPostNotification(final GroupId g) {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
if (blockForums) return;
if (g.equals(blockedGroup)) return;
Integer count = forumCounts.get(g);
if (count == null) forumCounts.put(g, 1);
else forumCounts.put(g, count + 1);
forumTotal++;
if (!g.equals(visibleGroup))
updateForumPostNotification();
updateForumPostNotification();
}
});
}
@Override
public void clearForumPostNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
Integer count = forumCounts.remove(g);
if (count == null) return; // Already cleared
forumTotal -= count;
// FIXME: If the notification isn't showing, this may show it
updateForumPostNotification();
}
});
}
@UiThread
private void updateForumPostNotification() {
if (forumTotal == 0) {
clearForumPostNotification();
@@ -332,7 +415,13 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
// Clear the counters if the notification is dismissed
Intent clear = new Intent(CLEAR_FORUM_ACTION);
PendingIntent delete = PendingIntent.getBroadcast(appContext, 0,
clear, 0);
b.setDeleteIntent(delete);
if (forumCounts.size() == 1) {
// Touching the notification shows the relevant forum
Intent i = new Intent(appContext, ForumActivity.class);
GroupId g = forumCounts.keySet().iterator().next();
i.putExtra(GROUP_ID, g.getBytes());
@@ -344,9 +433,11 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
} else {
// Touching the notification shows the forum list
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(NavDrawerActivity.INTENT_FORUMS, true);
i.putExtra(INTENT_FORUMS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setData(Uri.parse(FORUM_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
@@ -363,28 +454,49 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
}
@Override
public void showBlogPostNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
public void clearAllForumPostNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
if (!blogBlocked) {
blogTotal++;
updateBlogPostNotification();
}
clearForumPostNotification();
}
});
}
@UiThread
private void showBlogPostNotification(final GroupId g) {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
if (blockBlogs) return;
if (g.equals(blockedGroup)) return;
Integer count = blogCounts.get(g);
if (count == null) blogCounts.put(g, 1);
else blogCounts.put(g, count + 1);
blogTotal++;
updateBlogPostNotification();
}
});
}
@Override
public void clearBlogPostNotification() {
blogTotal = 0;
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(BLOG_POST_NOTIFICATION_ID);
public void clearBlogPostNotification(final GroupId g) {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
Integer count = blogCounts.remove(g);
if (count == null) return; // Already cleared
blogTotal -= count;
updateBlogPostNotification();
}
});
}
@UiThread
private void updateBlogPostNotification() {
if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) {
if (blogTotal == 0) {
clearBlogPostNotification();
} else if (settings.getBoolean(PREF_NOTIFY_BLOG, true)) {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
b.setSmallIcon(R.drawable.message_notification_icon);
@@ -398,15 +510,20 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
// Clear the counters if the notification is dismissed
Intent clear = new Intent(CLEAR_BLOG_ACTION);
PendingIntent delete = PendingIntent.getBroadcast(appContext, 0,
clear, 0);
b.setDeleteIntent(delete);
// Touching the notification shows the combined blog feed
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(NavDrawerActivity.INTENT_BLOGS, true);
i.putExtra(INTENT_BLOGS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setData(Uri.parse(BLOG_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_SOCIAL);
b.setVisibility(VISIBILITY_SECRET);
@@ -418,52 +535,153 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
}
@Override
public void blockNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
public void clearAllBlogPostNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
visibleGroup = g;
clearBlogPostNotification();
}
});
}
private void showIntroductionNotification() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
if (blockIntroductions) return;
introductionTotal++;
updateIntroductionNotification();
}
});
}
@UiThread
private void updateIntroductionNotification() {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
b.setSmallIcon(R.drawable.introduction_notification);
b.setContentTitle(appContext.getText(R.string.app_name));
b.setContentText(appContext.getResources().getQuantityString(
R.plurals.introduction_notification_text, introductionTotal,
introductionTotal));
String ringtoneUri = settings.get("notifyRingtoneUri");
if (!StringUtils.isNullOrEmpty(ringtoneUri))
b.setSound(Uri.parse(ringtoneUri));
b.setDefaults(getDefaults());
b.setOnlyAlertOnce(true);
b.setAutoCancel(true);
// Clear the counter if the notification is dismissed
Intent clear = new Intent(CLEAR_INTRODUCTION_ACTION);
PendingIntent delete = PendingIntent.getBroadcast(appContext, 0,
clear, 0);
b.setDeleteIntent(delete);
// Touching the notification shows the contact list
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(INTENT_CONTACTS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
i.setData(Uri.parse(CONTACT_URI));
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
if (Build.VERSION.SDK_INT >= 21) {
b.setCategory(CATEGORY_MESSAGE);
b.setVisibility(VISIBILITY_SECRET);
}
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build());
}
@Override
public void blockNotification(final GroupId g) {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
blockedGroup = g;
}
});
}
@Override
public void unblockNotification(final GroupId g) {
androidExecutor.execute(new Runnable() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
if (g.equals(visibleGroup)) visibleGroup = null;
if (g.equals(blockedGroup)) blockedGroup = null;
}
});
}
@Override
public void blockBlogNotification() {
androidExecutor.execute(new Runnable() {
public void blockAllContactNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
blogBlocked = true;
blockContacts = true;
blockIntroductions = true;
}
});
}
@Override
public void unblockBlogNotification() {
androidExecutor.execute(new Runnable() {
public void unblockAllContactNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
blogBlocked = false;
blockContacts = false;
blockIntroductions = false;
}
});
}
@Override
public void blockAllForumPostNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
blockForums = true;
}
});
}
@Override
public void unblockAllForumPostNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
blockForums = false;
}
});
}
@Override
public void blockAllBlogPostNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
blockBlogs = true;
}
});
}
@Override
public void unblockAllBlogPostNotifications() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
blockBlogs = false;
}
});
}
private void showNotificationForPrivateConversation(final ContactId c) {
androidExecutor.execute(new Runnable() {
dbExecutor.execute(new Runnable() {
@Override
public void run() {
try {
GroupId group = messagingManager.getConversationId(c);
showPrivateMessageNotification(group);
GroupId g = messagingManager.getConversationId(c);
showPrivateMessageNotification(g);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
@@ -472,35 +690,25 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
});
}
private void showIntroductionSucceededNotification(final Contact c) {
androidExecutor.execute(new Runnable() {
@Override
public void run() {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
b.setSmallIcon(R.drawable.introduction_notification);
private class DeleteIntentReceiver extends BroadcastReceiver {
b.setContentTitle(appContext
.getString(R.string.introduction_success_title));
b.setContentText(appContext
.getString(R.string.introduction_success_text,
c.getAuthor().getName()));
b.setDefaults(getDefaults());
b.setAutoCancel(true);
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(NavDrawerActivity.INTENT_CONTACTS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build());
}
});
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
if (CLEAR_PRIVATE_MESSAGE_ACTION.equals(action)) {
clearPrivateMessageNotification();
} else if (CLEAR_FORUM_ACTION.equals(action)) {
clearForumPostNotification();
} else if (CLEAR_BLOG_ACTION.equals(action)) {
clearBlogPostNotification();
} else if (CLEAR_INTRODUCTION_ACTION.equals(action)) {
clearIntroductionSuccessNotification();
}
}
});
}
}
}

View File

@@ -2,6 +2,7 @@ package org.briarproject.android;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.UiThread;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -16,12 +17,14 @@ import static android.view.inputmethod.InputMethodManager.SHOW_FORCED;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static org.briarproject.android.TestingConstants.PREVENT_SCREENSHOTS;
public abstract class BaseActivity extends AppCompatActivity {
public abstract class BaseActivity extends AppCompatActivity
implements DestroyableActivity {
protected ActivityComponent activityComponent;
private final List<ActivityLifecycleController> lifecycleControllers =
new ArrayList<>();
private boolean destroyed = false;
public abstract void injectActivity(ActivityComponent component);
@@ -78,11 +81,17 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
super.onDestroy();
destroyed = true;
for (ActivityLifecycleController alc : lifecycleControllers) {
alc.onActivityDestroy();
}
}
@UiThread
public boolean hasBeenDestroyed() {
return destroyed;
}
public void showSoftKeyboardForced(View view) {
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).showSoftInput(view, SHOW_FORCED);

View File

@@ -59,7 +59,6 @@ public abstract class BriarActivity extends BaseActivity {
protected void signOut(final boolean removeFromRecentApps) {
briarController.signOut(new UiResultHandler<Void>(this) {
@Override
public void onResultUi(Void result) {
if (removeFromRecentApps) startExitActivity();

View File

@@ -7,7 +7,7 @@ import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import org.briarproject.R;
import org.briarproject.android.blogs.BlogsFragment;
import org.briarproject.android.blogs.FeedFragment;
import org.briarproject.android.contact.ContactListFragment;
import org.briarproject.android.forum.ForumListFragment;
import org.briarproject.android.fragment.BaseFragment;
@@ -27,7 +27,7 @@ public abstract class BriarFragmentActivity extends BriarActivity {
actionBar.setTitle(R.string.contact_list_button);
} else if (fragmentTag.equals(ForumListFragment.TAG)) {
actionBar.setTitle(R.string.forums_button);
} else if (fragmentTag.equals(BlogsFragment.TAG)) {
} else if (fragmentTag.equals(FeedFragment.TAG)) {
actionBar.setTitle(R.string.blogs_button);
}
}

View File

@@ -108,7 +108,8 @@ public class BriarService extends Service {
}
private void showStartupFailureNotification(final StartResult result) {
androidExecutor.execute(new Runnable() {
androidExecutor.runOnUiThread(new Runnable() {
@Override
public void run() {
NotificationCompat.Builder b =
new NotificationCompat.Builder(BriarService.this);
@@ -197,11 +198,13 @@ public class BriarService extends Service {
private volatile IBinder binder = null;
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
this.binder = binder;
binderLatch.countDown();
}
@Override
public void onServiceDisconnected(ComponentName name) {}
/** Waits for the service to connect and returns its binder. */

View File

@@ -0,0 +1,12 @@
package org.briarproject.android;
import android.support.annotation.UiThread;
public interface DestroyableActivity {
void runOnUiThread(Runnable runnable);
@UiThread
boolean hasBeenDestroyed();
}

View File

@@ -21,7 +21,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.briarproject.R;
import org.briarproject.android.blogs.BlogsFragment;
import org.briarproject.android.blogs.FeedFragment;
import org.briarproject.android.contact.ContactListFragment;
import org.briarproject.android.controller.NavDrawerController;
import org.briarproject.android.controller.TransportStateListener;
@@ -74,7 +74,9 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
super.onNewIntent(intent);
exitIfStartupFailed(intent);
checkAuthorHandle(intent);
clearBackStack();
// FIXME why was the stack cleared here?
// This prevents state from being restored properly
// clearBackStack();
if (intent.getBooleanExtra(INTENT_FORUMS, false)) {
startFragment(ForumListFragment.newInstance());
}
@@ -82,7 +84,7 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
startFragment(ContactListFragment.newInstance());
}
else if (intent.getBooleanExtra(INTENT_BLOGS, false)) {
startFragment(BlogsFragment.newInstance());
startFragment(FeedFragment.newInstance());
}
setIntent(null);
}
@@ -186,7 +188,7 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
startFragment(ForumListFragment.newInstance());
break;
case R.id.nav_btn_blogs:
startFragment(BlogsFragment.newInstance());
startFragment(FeedFragment.newInstance());
break;
case R.id.nav_btn_settings:
startActivity(new Intent(this, SettingsActivity.class));
@@ -248,7 +250,6 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
@Override
public void hideLoadingScreen() {
drawerLayout.setDrawerLockMode(LOCK_MODE_UNLOCKED);
CustomAnimations.animateHeight(toolbar, true, 250);
progressViewGroup.setVisibility(INVISIBLE);
}

View File

@@ -87,7 +87,7 @@ public class SplashScreenActivity extends BaseActivity {
}
private void setPreferencesDefaults() {
androidExecutor.execute(new Runnable() {
androidExecutor.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
PreferenceManager.setDefaultValues(SplashScreenActivity.this,

View File

@@ -13,10 +13,21 @@ public interface AndroidExecutor {
* Runs the given task on a background thread with a message queue and
* returns a Future for getting the result.
*/
<V> Future<V> submit(Callable<V> c);
<V> Future<V> runOnBackgroundThread(Callable<V> c);
/**
* Runs the given task on a background thread with a message queue.
*/
void execute(Runnable r);
void runOnBackgroundThread(Runnable r);
/**
* Runs the given task on the main UI thread and returns a Future for
* getting the result.
*/
<V> Future<V> runOnUiThread(Callable<V> c);
/**
* Runs the given task on the main UI thread.
*/
void runOnUiThread(Runnable r);
}

View File

@@ -2,26 +2,37 @@ package org.briarproject.android.api;
import org.briarproject.api.sync.GroupId;
/** Manages notifications for private messages and forum posts. */
/**
* Manages notifications for private messages, forum posts, blog posts and
* introductions.
*/
public interface AndroidNotificationManager {
void showPrivateMessageNotification(GroupId g);
void clearPrivateMessageNotification(GroupId g);
void showForumPostNotification(GroupId g);
void clearAllContactNotifications();
void clearForumPostNotification(GroupId g);
void showBlogPostNotification(GroupId g);
void clearAllForumPostNotifications();
void clearBlogPostNotification();
void clearBlogPostNotification(GroupId g);
void clearAllBlogPostNotifications();
void blockNotification(GroupId g);
void unblockNotification(GroupId g);
void blockBlogNotification();
void blockAllContactNotifications();
void unblockBlogNotification();
void unblockAllContactNotifications();
void blockAllForumPostNotifications();
void unblockAllForumPostNotifications();
void blockAllBlogPostNotifications();
void unblockAllBlogPostNotifications();
}

View File

@@ -0,0 +1,41 @@
package org.briarproject.android.blogs;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.Collection;
public interface BaseController {
@UiThread
void onStart();
@UiThread
void onStop();
void loadBlogPosts(GroupId g,
ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler);
void loadBlogPost(BlogPostHeader header,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void loadBlogPost(GroupId g, MessageId m,
ResultExceptionHandler<BlogPostItem, DbException> handler);
void repeatPost(BlogPostItem item, @Nullable String comment,
ResultExceptionHandler<Void, DbException> resultHandler);
void setOnBlogPostAddedListener(OnBlogPostAddedListener listener);
interface OnBlogPostAddedListener {
@UiThread
void onBlogPostAdded(BlogPostHeader header, boolean local);
}
}

View File

@@ -0,0 +1,251 @@
package org.briarproject.android.blogs;
import android.app.Activity;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.controller.DbControllerImpl;
import org.briarproject.android.controller.handler.ResultExceptionHandler;
import org.briarproject.api.blogs.Blog;
import org.briarproject.api.blogs.BlogCommentHeader;
import org.briarproject.api.blogs.BlogManager;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.event.BlogPostAddedEvent;
import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
abstract class BaseControllerImpl extends DbControllerImpl
implements BaseController, EventListener {
private static final Logger LOG =
Logger.getLogger(BaseControllerImpl.class.getName());
@Inject
protected Activity activity;
@Inject
protected EventBus eventBus;
@Inject
protected AndroidNotificationManager notificationManager;
@Inject
protected IdentityManager identityManager;
@Inject
protected volatile BlogManager blogManager;
private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
private final Map<MessageId, BlogPostHeader> headerCache =
new ConcurrentHashMap<>();
protected volatile OnBlogPostAddedListener listener;
@Override
@CallSuper
public void onStart() {
if (listener == null)
throw new IllegalStateException(
"OnBlogPostAddedListener needs to be attached");
eventBus.addListener(this);
}
@Override
@CallSuper
public void onStop() {
eventBus.removeListener(this);
}
@Override
@CallSuper
public void eventOccurred(Event e) {
if (e instanceof BlogPostAddedEvent) {
final BlogPostAddedEvent m = (BlogPostAddedEvent) e;
LOG.info("New blog post added");
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
listener.onBlogPostAdded(m.getHeader(), m.isLocal());
}
});
}
}
@Override
public void setOnBlogPostAddedListener(OnBlogPostAddedListener listener) {
this.listener = listener;
}
@Override
public void loadBlogPosts(final GroupId groupId,
final ResultExceptionHandler<Collection<BlogPostItem>, DbException> handler) {
if (groupId == null) throw new IllegalStateException();
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
Collection<BlogPostItem> items = loadItems(groupId);
handler.onResult(items);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
protected Collection<BlogPostItem> loadItems(GroupId groupId)
throws DbException {
long now = System.currentTimeMillis();
Collection<BlogPostHeader> headers =
blogManager.getPostHeaders(groupId);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading headers took " + duration + " ms");
Collection<BlogPostItem> items = new ArrayList<>(headers.size());
now = System.currentTimeMillis();
for (BlogPostHeader h : headers) {
headerCache.put(h.getId(), h);
BlogPostItem item = getItem(h);
items.add(item);
}
duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading bodies took " + duration + " ms");
return items;
}
@Override
public void loadBlogPost(final BlogPostHeader header,
final ResultExceptionHandler<BlogPostItem, DbException> handler) {
String body = bodyCache.get(header.getId());
if (body != null) {
LOG.info("Loaded body from cache");
handler.onResult(new BlogPostItem(header, body));
return;
}
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
BlogPostItem item = getItem(header);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading body took " + duration + " ms");
handler.onResult(item);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@Override
public void loadBlogPost(final GroupId g, final MessageId m,
final ResultExceptionHandler<BlogPostItem, DbException> handler) {
BlogPostHeader header = headerCache.get(m);
if (header != null) {
LOG.info("Loaded header from cache");
loadBlogPost(header, handler);
return;
}
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
long now = System.currentTimeMillis();
BlogPostHeader header = getPostHeader(g, m);
BlogPostItem item = getItem(header);
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading post took " + duration + " ms");
handler.onResult(item);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
@Override
public void repeatPost(final BlogPostItem item,
final @Nullable String comment,
final ResultExceptionHandler<Void, DbException> handler) {
runOnDbThread(new Runnable() {
@Override
public void run() {
try {
LocalAuthor a = identityManager.getLocalAuthor();
Blog b = blogManager.getPersonalBlog(a);
BlogPostHeader h = item.getHeader();
blogManager.addLocalComment(a, b.getId(), comment, h);
handler.onResult(null);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
handler.onException(e);
}
}
});
}
private BlogPostHeader getPostHeader(GroupId g, MessageId m)
throws DbException {
if (g == null) throw new IllegalStateException();
BlogPostHeader header = headerCache.get(m);
if (header == null) {
header = blogManager.getPostHeader(g, m);
headerCache.put(m, header);
}
return header;
}
private BlogPostItem getItem(BlogPostHeader h) throws DbException {
String body;
if (h instanceof BlogCommentHeader) {
BlogCommentHeader c = (BlogCommentHeader) h;
BlogCommentItem item = new BlogCommentItem(c);
body = getPostBody(item.getPostHeader().getId());
item.setBody(body);
return item;
} else {
body = getPostBody(h.getId());
return new BlogPostItem(h, body);
}
}
private String getPostBody(MessageId m) throws DbException {
String body = bodyCache.get(m);
if (body == null) {
body = blogManager.getPostBody(m);
if (body != null) bodyCache.put(m, body);
}
//noinspection ConstantConditions
return body;
}
}

View File

@@ -0,0 +1,109 @@
package org.briarproject.android.blogs;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import org.briarproject.R;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.api.db.DbException;
import java.util.logging.Logger;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static org.briarproject.android.util.AndroidUtils.MIN_RESOLUTION;
public abstract class BasePostFragment extends BaseFragment {
private final Logger LOG =
Logger.getLogger(BasePostFragment.class.getName());
private View view;
private ProgressBar progressBar;
private BlogPostViewHolder ui;
private BlogPostItem post;
private Runnable refresher;
@CallSuper
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
setHasOptionsMenu(true);
view = inflater.inflate(R.layout.fragment_blog_post, container,
false);
progressBar = (ProgressBar) view.findViewById(R.id.progressBar);
progressBar.setVisibility(VISIBLE);
ui = new BlogPostViewHolder(view);
return view;
}
@CallSuper
@Override
public void onStart() {
super.onStart();
startPeriodicUpdate();
}
@CallSuper
@Override
public void onStop() {
super.onStop();
stopPeriodicUpdate();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
getActivity().onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@UiThread
protected void onBlogPostLoaded(BlogPostItem post) {
progressBar.setVisibility(INVISIBLE);
this.post = post;
ui.bindItem(post);
}
@UiThread
protected void onBlogPostLoadException(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
}
private void startPeriodicUpdate() {
refresher = new Runnable() {
@Override
public void run() {
if (ui == null) return;
LOG.info("Updating Content...");
ui.updateDate(post.getTimestamp());
view.postDelayed(refresher, MIN_RESOLUTION);
}
};
LOG.info("Adding Handler Callback");
view.postDelayed(refresher, MIN_RESOLUTION);
}
private void stopPeriodicUpdate() {
if (refresher != null && ui != null) {
LOG.info("Removing Handler Callback");
view.removeCallbacks(refresher);
}
}
}

View File

@@ -0,0 +1,177 @@
package org.briarproject.android.blogs;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import org.briarproject.R;
import org.briarproject.android.blogs.BaseController.OnBlogPostAddedListener;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.MessageId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static org.briarproject.android.blogs.BasePostPagerFragment.BlogPostPagerAdapter.INVALID_POSITION;
abstract class BasePostPagerFragment extends BaseFragment
implements OnBlogPostAddedListener {
static final String POST_ID = "briar.POST_ID";
private ViewPager pager;
private ProgressBar progressBar;
private BlogPostPagerAdapter postPagerAdapter;
private MessageId postId;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle state) {
Bundle args;
if (state == null) args = getArguments();
else args = state;
byte[] p = args.getByteArray(POST_ID);
if (p == null)
throw new IllegalStateException("No post ID in args");
postId = new MessageId(p);
View v = inflater.inflate(R.layout.fragment_blog_post_pager, container,
false);
progressBar = (ProgressBar) v.findViewById(R.id.progressBar);
progressBar.setVisibility(VISIBLE);
pager = (ViewPager) v.findViewById(R.id.pager);
postPagerAdapter = new BlogPostPagerAdapter(getChildFragmentManager());
return v;
}
@Override
public void onStart() {
super.onStart();
if (postId == null) {
MessageId selected = getSelectedPost();
if (selected != null) loadBlogPosts(selected);
} else {
loadBlogPosts(postId);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
MessageId selected = getSelectedPost();
if (selected != null)
outState.putByteArray(POST_ID, selected.getBytes());
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
loadBlogPost(header);
}
abstract void loadBlogPosts(final MessageId select);
abstract void loadBlogPost(BlogPostHeader header);
protected void onBlogPostsLoaded(MessageId select,
Collection<BlogPostItem> posts) {
postId = null;
postPagerAdapter.setPosts(posts);
selectPost(select);
}
protected void onBlogPostsLoadedException(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
}
@Nullable
private MessageId getSelectedPost() {
if (postPagerAdapter.getCount() == 0) return null;
int position = pager.getCurrentItem();
return postPagerAdapter.getPost(position).getId();
}
private void selectPost(MessageId m) {
int pos = postPagerAdapter.getPostPosition(m);
if (pos != INVALID_POSITION) {
progressBar.setVisibility(INVISIBLE);
pager.setAdapter(postPagerAdapter);
pager.setCurrentItem(pos);
}
}
protected void addPost(BlogPostItem post) {
MessageId selected = getSelectedPost();
postPagerAdapter.addPost(post);
if (selected != null) selectPost(selected);
}
@UiThread
static class BlogPostPagerAdapter extends FragmentStatePagerAdapter {
static final int INVALID_POSITION = -1;
private final List<BlogPostItem> posts = new ArrayList<>();
private BlogPostPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public int getCount() {
return posts.size();
}
@Override
public Fragment getItem(int position) {
BlogPostItem post = posts.get(position);
return FeedPostFragment.newInstance(post.getGroupId(), post.getId());
}
private BlogPostItem getPost(int position) {
return posts.get(position);
}
private void setPosts(Collection<BlogPostItem> posts) {
this.posts.clear();
this.posts.addAll(posts);
Collections.sort(this.posts);
notifyDataSetChanged();
}
private void addPost(BlogPostItem post) {
posts.add(post);
Collections.sort(posts);
notifyDataSetChanged();
}
private int getPostPosition(MessageId m) {
int count = getCount();
for (int i = 0; i < count; i++) {
if (getPost(i).getId().equals(m)) {
return i;
}
}
return INVALID_POSITION;
}
}
}

View File

@@ -2,56 +2,29 @@ package org.briarproject.android.blogs;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import org.briarproject.R;
import org.briarproject.android.ActivityComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.blogs.BlogController.BlogPostListener;
import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener;
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
import org.briarproject.android.fragment.BaseFragment.BaseFragmentListener;
import org.briarproject.api.blogs.BlogPostHeader;
import org.briarproject.api.db.DbException;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
public class BlogActivity extends BriarActivity implements BlogPostListener,
public class BlogActivity extends BriarActivity implements
OnBlogPostClickListener, BaseFragmentListener {
static final int REQUEST_WRITE_POST = 1;
static final int REQUEST_SHARE = 2;
static final String BLOG_NAME = "briar.BLOG_NAME";
static final String IS_MY_BLOG = "briar.IS_MY_BLOG";
static final String IS_NEW_BLOG = "briar.IS_NEW_BLOG";
private static final String POST_ID = "briar.POST_ID";
private GroupId groupId;
private ProgressBar progressBar;
private ViewPager pager;
private BlogPagerAdapter blogPagerAdapter;
private BlogPostPagerAdapter postPagerAdapter;
private String blogName;
private boolean myBlog, isNew;
private MessageId savedPostId;
@Inject
BlogController blogController;
@@ -64,63 +37,24 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
Intent i = getIntent();
byte[] b = i.getByteArrayExtra(GROUP_ID);
if (b == null) throw new IllegalStateException("No group ID in intent");
groupId = new GroupId(b);
GroupId groupId = new GroupId(b);
blogController.setGroupId(groupId);
// Name of the Blog from Intent
blogName = i.getStringExtra(BLOG_NAME);
// Name of the blog
String blogName = i.getStringExtra(BLOG_NAME);
if (blogName != null) setTitle(blogName);
// Is this our blog and was it just created?
myBlog = i.getBooleanExtra(IS_MY_BLOG, false);
isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
// Was this blog just created?
boolean isNew = i.getBooleanExtra(IS_NEW_BLOG, false);
setContentView(R.layout.activity_blog);
pager = (ViewPager) findViewById(R.id.pager);
setContentView(R.layout.activity_fragment_container);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
blogPagerAdapter = new BlogPagerAdapter(getSupportFragmentManager());
postPagerAdapter = new BlogPostPagerAdapter(
getSupportFragmentManager());
if (state == null || state.getByteArray(POST_ID) == null) {
// The blog fragment has its own progress bar
hideLoadingScreen();
pager.setAdapter(blogPagerAdapter);
savedPostId = null;
} else {
// Adapter will be set in selectPostInPostPager()
savedPostId = new MessageId(state.getByteArray(POST_ID));
}
}
@Override
public void onResume() {
super.onResume();
if (savedPostId == null) {
MessageId selected = getSelectedPostInPostPager();
if (selected != null) loadBlogPosts(selected);
} else {
loadBlogPosts(savedPostId);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
MessageId selected = getSelectedPostInPostPager();
if (selected != null)
outState.putByteArray(POST_ID, selected.getBytes());
}
@Override
public void onBackPressed() {
if (pager.getAdapter() == postPagerAdapter) {
pager.setAdapter(blogPagerAdapter);
savedPostId = null;
} else {
super.onBackPressed();
if (state == null) {
BlogFragment f = BlogFragment.newInstance(groupId, blogName, isNew);
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
.commit();
}
}
@@ -129,6 +63,15 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
component.inject(this);
}
@Override
public void onBlogPostClick(BlogPostItem post) {
BlogPostPagerFragment f = BlogPostPagerFragment.newInstance(post.getId());
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f, f.getUniqueTag())
.addToBackStack(f.getUniqueTag())
.commit();
}
@Override
public void showLoadingScreen(boolean isBlocking, int stringId) {
progressBar.setVisibility(VISIBLE);
@@ -136,176 +79,10 @@ public class BlogActivity extends BriarActivity implements BlogPostListener,
@Override
public void hideLoadingScreen() {
progressBar.setVisibility(GONE);
progressBar.setVisibility(INVISIBLE);
}
@Override
public void onFragmentCreated(String tag) {
}
@Override
public void onBlogPostClick(BlogPostItem post) {
loadBlogPosts(post.getId());
}
private void loadBlogPosts(final MessageId select) {
blogController.loadBlogPosts(
new UiResultExceptionHandler<Collection<BlogPostItem>, DbException>(
this) {
@Override
public void onResultUi(Collection<BlogPostItem> posts) {
hideLoadingScreen();
savedPostId = null;
postPagerAdapter.setPosts(posts);
selectPostInPostPager(select);
}
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
}
});
}
@Override
public void onBlogPostAdded(BlogPostHeader header, boolean local) {
if (pager.getAdapter() == postPagerAdapter) {
loadBlogPost(header);
} else {
BlogFragment f = blogPagerAdapter.getFragment();
if (f != null && f.isVisible()) f.onBlogPostAdded(header, local);
}
}
private void loadBlogPost(BlogPostHeader header) {
blogController.loadBlogPost(header,
new UiResultExceptionHandler<BlogPostItem, DbException>(this) {
@Override
public void onResultUi(BlogPostItem post) {
addPostToPostPager(post);
}
@Override
public void onExceptionUi(DbException exception) {
// TODO: Decide how to handle errors in the UI
finish();
}
});
}
@Nullable
private MessageId getSelectedPostInPostPager() {
if (pager.getAdapter() != postPagerAdapter) return null;
if (postPagerAdapter.getCount() == 0) return null;
int position = pager.getCurrentItem();
return postPagerAdapter.getPost(position).getId();
}
private void selectPostInPostPager(MessageId m) {
int count = postPagerAdapter.getCount();
for (int i = 0; i < count; i++) {
if (postPagerAdapter.getPost(i).getId().equals(m)) {
pager.setAdapter(postPagerAdapter);
pager.setCurrentItem(i);
return;
}
}
}
private void addPostToPostPager(BlogPostItem post) {
MessageId selected = getSelectedPostInPostPager();
postPagerAdapter.addPost(post);
if (selected != null) selectPostInPostPager(selected);
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
// The BlogPostAddedEvent arrives when the controller is not listening,
// so we need to manually reload the blog posts :(
if (requestCode == REQUEST_WRITE_POST && resultCode == RESULT_OK) {
if (pager.getAdapter() == postPagerAdapter) {
MessageId selected = getSelectedPostInPostPager();
if (selected != null) loadBlogPosts(selected);
} else {
BlogFragment f = blogPagerAdapter.getFragment();
if (f != null && f.isVisible()) f.loadBlogPosts(true);
}
}
}
@UiThread
private class BlogPagerAdapter extends FragmentStatePagerAdapter {
private BlogFragment fragment = null;
private BlogPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public int getCount() {
return 1;
}
@Override
public Fragment getItem(int position) {
return BlogFragment.newInstance(groupId, blogName, myBlog, isNew);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
// save a reference to the single fragment here for later
fragment =
(BlogFragment) super.instantiateItem(container, position);
return fragment;
}
private BlogFragment getFragment() {
return fragment;
}
}
@UiThread
private static class BlogPostPagerAdapter
extends FragmentStatePagerAdapter {
private final List<BlogPostItem> posts = new ArrayList<>();
private BlogPostPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public int getCount() {
return posts.size();
}
@Override
public Fragment getItem(int position) {
return BlogPostFragment.newInstance(posts.get(position).getId());
}
private BlogPostItem getPost(int position) {
return posts.get(position);
}
private void setPosts(Collection<BlogPostItem> posts) {
this.posts.clear();
this.posts.addAll(posts);
Collections.sort(this.posts);
notifyDataSetChanged();
}
private void addPost(BlogPostItem post) {
posts.add(post);
Collections.sort(posts);
notifyDataSetChanged();
}
}
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.android.blogs;
import android.support.annotation.UiThread;
import org.briarproject.api.blogs.BlogCommentHeader;
import org.briarproject.api.blogs.BlogPostHeader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@UiThread
class BlogCommentItem extends BlogPostItem {
private static final BlogCommentComparator COMPARATOR =
new BlogCommentComparator();
private final BlogPostHeader postHeader;
private final List<BlogCommentHeader> comments = new ArrayList<>();
BlogCommentItem(BlogCommentHeader header) {
super(header, null);
postHeader = collectComments(header);
Collections.sort(comments, COMPARATOR);
}
private BlogPostHeader collectComments(BlogPostHeader header) {
if (header instanceof BlogCommentHeader) {
BlogCommentHeader comment = (BlogCommentHeader) header;
if (comment.getComment() != null)
comments.add(comment);
return collectComments(comment.getParent());
} else {
return header;
}
}
public void setBody(String body) {
this.body = body;
}
@Override
public BlogCommentHeader getHeader() {
return (BlogCommentHeader) super.getHeader();
}
@Override
BlogPostHeader getPostHeader() {
return postHeader;
}
List<BlogCommentHeader> getComments() {
return comments;
}
private static class BlogCommentComparator
implements Comparator<BlogCommentHeader> {
@Override
public int compare(BlogCommentHeader h1, BlogCommentHeader h2) {
// re-use same comparator used for blog posts, but reverse it
return BlogPostItem.compare(h2, h1);
}
}
}

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