mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 03:09:04 +01:00
Compare commits
31 Commits
beta-2016-
...
278-bqp-ui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e32139a89 | ||
|
|
0d16dd0358 | ||
|
|
3f2b85ac0d | ||
|
|
d0852d2265 | ||
|
|
9149b82cc4 | ||
|
|
b13bf66165 | ||
|
|
8c2fd905a1 | ||
|
|
38a4f73cdc | ||
|
|
1a175beac9 | ||
|
|
7e9ff40837 | ||
|
|
bd01c3732e | ||
|
|
9532a60f43 | ||
|
|
d2722eed92 | ||
|
|
886c8feb34 | ||
|
|
c7f73f9247 | ||
|
|
35156d698f | ||
|
|
1366972449 | ||
|
|
3d25c41e7a | ||
|
|
ba928875df | ||
|
|
cac0f30816 | ||
|
|
aad9f5142b | ||
|
|
9d686e16e0 | ||
|
|
2880043c07 | ||
|
|
fb85345392 | ||
|
|
0b67ec9201 | ||
|
|
fcf21b7ed7 | ||
|
|
6e545d0100 | ||
|
|
69026054cd | ||
|
|
db4b79fcae | ||
|
|
7412ca59ac | ||
|
|
ec083d617e |
@@ -2,10 +2,6 @@ apply plugin: 'com.android.library'
|
||||
apply plugin: 'witness'
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
|
||||
repositories {
|
||||
maven { url 'http://repo1.maven.org/maven2' }
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.3"
|
||||
|
||||
@@ -0,0 +1,890 @@
|
||||
package org.briarproject;
|
||||
|
||||
import net.jodah.concurrentunit.Waiter;
|
||||
|
||||
import org.briarproject.api.clients.MessageQueueManager;
|
||||
import org.briarproject.api.clients.PrivateGroupFactory;
|
||||
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.SecretKey;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Metadata;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.ForumInvitationReceivedEvent;
|
||||
import org.briarproject.api.event.ForumInvitationResponseReceivedEvent;
|
||||
import org.briarproject.api.event.MessageValidatedEvent;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumInvitationMessage;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.SyncSession;
|
||||
import org.briarproject.api.sync.SyncSessionFactory;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.contact.ContactModule;
|
||||
import org.briarproject.crypto.CryptoModule;
|
||||
import org.briarproject.forum.ForumModule;
|
||||
import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.properties.PropertiesModule;
|
||||
import org.briarproject.sync.SyncModule;
|
||||
import org.briarproject.transport.TransportModule;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.TestPluginsModule.MAX_LATENCY;
|
||||
import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
|
||||
import static org.briarproject.api.forum.ForumConstants.SHARE_MSG_TYPE_INVITATION;
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ForumSharingIntegrationTest extends BriarTestCase {
|
||||
|
||||
LifecycleManager lifecycleManager0, lifecycleManager1, lifecycleManager2;
|
||||
SyncSessionFactory sync0, sync1, sync2;
|
||||
ForumManager forumManager0, forumManager1, forumManager2;
|
||||
ContactManager contactManager0, contactManager1, contactManager2;
|
||||
ContactId contactId0, contactId2, contactId1, contactId21;
|
||||
IdentityManager identityManager0, identityManager1, identityManager2;
|
||||
LocalAuthor author0, author1, author2;
|
||||
Forum forum0;
|
||||
SharerListener listener0, listener2;
|
||||
InviteeListener listener1;
|
||||
|
||||
@Inject
|
||||
Clock clock;
|
||||
@Inject
|
||||
AuthorFactory authorFactory;
|
||||
|
||||
// objects accessed from background threads need to be volatile
|
||||
private volatile ForumSharingManager forumSharingManager0;
|
||||
private volatile ForumSharingManager forumSharingManager1;
|
||||
private volatile ForumSharingManager forumSharingManager2;
|
||||
private volatile Waiter eventWaiter;
|
||||
private volatile Waiter msgWaiter;
|
||||
|
||||
private final File testDir = TestUtils.getTestDirectory();
|
||||
private final SecretKey master = TestUtils.getSecretKey();
|
||||
private final int TIMEOUT = 15000;
|
||||
private final String SHARER = "Sharer";
|
||||
private final String INVITEE = "Invitee";
|
||||
private final String SHARER2 = "Sharer2";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ForumSharingIntegrationTest.class.getName());
|
||||
|
||||
private ForumSharingIntegrationTestComponent t0, t1, t2;
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown=ExpectedException.none();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
ForumSharingIntegrationTestComponent component =
|
||||
DaggerForumSharingIntegrationTestComponent.builder().build();
|
||||
component.inject(this);
|
||||
injectEagerSingletons(component);
|
||||
|
||||
assertTrue(testDir.mkdirs());
|
||||
File t0Dir = new File(testDir, SHARER);
|
||||
t0 = DaggerForumSharingIntegrationTestComponent.builder()
|
||||
.testDatabaseModule(new TestDatabaseModule(t0Dir)).build();
|
||||
injectEagerSingletons(t0);
|
||||
File t1Dir = new File(testDir, INVITEE);
|
||||
t1 = DaggerForumSharingIntegrationTestComponent.builder()
|
||||
.testDatabaseModule(new TestDatabaseModule(t1Dir)).build();
|
||||
injectEagerSingletons(t1);
|
||||
File t2Dir = new File(testDir, SHARER2);
|
||||
t2 = DaggerForumSharingIntegrationTestComponent.builder()
|
||||
.testDatabaseModule(new TestDatabaseModule(t2Dir)).build();
|
||||
injectEagerSingletons(t2);
|
||||
|
||||
identityManager0 = t0.getIdentityManager();
|
||||
identityManager1 = t1.getIdentityManager();
|
||||
identityManager2 = t2.getIdentityManager();
|
||||
contactManager0 = t0.getContactManager();
|
||||
contactManager1 = t1.getContactManager();
|
||||
contactManager2 = t2.getContactManager();
|
||||
forumManager0 = t0.getForumManager();
|
||||
forumManager1 = t1.getForumManager();
|
||||
forumManager2 = t2.getForumManager();
|
||||
forumSharingManager0 = t0.getForumSharingManager();
|
||||
forumSharingManager1 = t1.getForumSharingManager();
|
||||
forumSharingManager2 = t2.getForumSharingManager();
|
||||
sync0 = t0.getSyncSessionFactory();
|
||||
sync1 = t1.getSyncSessionFactory();
|
||||
sync2 = t2.getSyncSessionFactory();
|
||||
|
||||
// initialize waiters fresh for each test
|
||||
eventWaiter = new Waiter();
|
||||
msgWaiter = new Waiter();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccessfulSharing() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize and let invitee accept all requests
|
||||
defaultInit(true);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, "Hi!");
|
||||
|
||||
// sync first request message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// sync response back
|
||||
syncToSharer();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener0.responseReceived);
|
||||
|
||||
// forum was added successfully
|
||||
assertEquals(0, forumSharingManager0.getAvailableForums().size());
|
||||
assertEquals(1, forumManager1.getForums().size());
|
||||
|
||||
// invitee has one invitation message from sharer
|
||||
List<ForumInvitationMessage> list =
|
||||
new ArrayList<>(forumSharingManager1
|
||||
.getForumInvitationMessages(contactId0));
|
||||
assertEquals(1, list.size());
|
||||
// check other things are alright with the forum message
|
||||
ForumInvitationMessage invitation = list.get(0);
|
||||
assertFalse(invitation.isAvailable());
|
||||
assertEquals(forum0.getName(), invitation.getForumName());
|
||||
assertEquals(contactId1, invitation.getContactId());
|
||||
assertEquals("Hi!", invitation.getMessage());
|
||||
// sharer has own invitation message
|
||||
assertEquals(1,
|
||||
forumSharingManager0.getForumInvitationMessages(contactId1)
|
||||
.size());
|
||||
// forum can not be shared again
|
||||
Contact c1 = contactManager0.getContact(contactId1);
|
||||
assertFalse(forumSharingManager0.canBeShared(forum0.getId(), c1));
|
||||
Contact c0 = contactManager1.getContact(contactId0);
|
||||
assertFalse(forumSharingManager1.canBeShared(forum0.getId(), c0));
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeclinedSharing() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize and let invitee accept all requests
|
||||
defaultInit(false);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, null);
|
||||
|
||||
// sync first request message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// sync response back
|
||||
syncToSharer();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener0.responseReceived);
|
||||
|
||||
// forum was not added
|
||||
assertEquals(0, forumSharingManager0.getAvailableForums().size());
|
||||
assertEquals(0, forumManager1.getForums().size());
|
||||
// forum is no longer available to invitee who declined
|
||||
assertEquals(0, forumSharingManager1.getAvailableForums().size());
|
||||
|
||||
// invitee has one invitation message from sharer
|
||||
List<ForumInvitationMessage> list =
|
||||
new ArrayList<>(forumSharingManager1
|
||||
.getForumInvitationMessages(contactId0));
|
||||
assertEquals(1, list.size());
|
||||
// check other things are alright with the forum message
|
||||
ForumInvitationMessage invitation = list.get(0);
|
||||
assertFalse(invitation.isAvailable());
|
||||
assertEquals(forum0.getName(), invitation.getForumName());
|
||||
assertEquals(contactId1, invitation.getContactId());
|
||||
assertEquals(null, invitation.getMessage());
|
||||
// sharer has own invitation message
|
||||
assertEquals(1,
|
||||
forumSharingManager0.getForumInvitationMessages(contactId1)
|
||||
.size());
|
||||
// forum can be shared again
|
||||
Contact c1 = contactManager0.getContact(contactId1);
|
||||
assertTrue(forumSharingManager0.canBeShared(forum0.getId(), c1));
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInviteeLeavesAfterFinished() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize and let invitee accept all requests
|
||||
defaultInit(true);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, "Hi!");
|
||||
|
||||
// sync first request message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// sync response back
|
||||
syncToSharer();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener0.responseReceived);
|
||||
|
||||
// forum was added successfully
|
||||
assertEquals(0, forumSharingManager0.getAvailableForums().size());
|
||||
assertEquals(1, forumManager1.getForums().size());
|
||||
assertTrue(forumManager1.getForums().contains(forum0));
|
||||
|
||||
// sharer shares forum with invitee
|
||||
Contact c1 = contactManager0.getContact(contactId1);
|
||||
assertTrue(forumSharingManager0.getSharedWith(forum0.getId())
|
||||
.contains(c1));
|
||||
// invitee gets forum shared by sharer
|
||||
Contact contact0 = contactManager1.getContact(contactId1);
|
||||
assertTrue(forumSharingManager1.getSharedBy(forum0.getId())
|
||||
.contains(contact0));
|
||||
|
||||
// invitee un-subscribes from forum
|
||||
forumManager1.removeForum(forum0);
|
||||
|
||||
// send leave message to sharer
|
||||
syncToSharer();
|
||||
|
||||
// forum is gone
|
||||
assertEquals(0, forumSharingManager0.getAvailableForums().size());
|
||||
assertEquals(0, forumManager1.getForums().size());
|
||||
|
||||
// sharer no longer shares forum with invitee
|
||||
assertFalse(forumSharingManager0.getSharedWith(forum0.getId())
|
||||
.contains(c1));
|
||||
// invitee no longer gets forum shared by sharer
|
||||
assertFalse(forumSharingManager1.getSharedBy(forum0.getId())
|
||||
.contains(contact0));
|
||||
// forum can be shared again
|
||||
assertTrue(forumSharingManager0.canBeShared(forum0.getId(), c1));
|
||||
Contact c0 = contactManager1.getContact(contactId0);
|
||||
assertTrue(forumSharingManager1.canBeShared(forum0.getId(), c0));
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSharerLeavesAfterFinished() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize and let invitee accept all requests
|
||||
defaultInit(true);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, null);
|
||||
|
||||
// sync first request message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// sync response back
|
||||
syncToSharer();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener0.responseReceived);
|
||||
|
||||
// forum was added successfully
|
||||
assertEquals(0, forumSharingManager0.getAvailableForums().size());
|
||||
assertEquals(1, forumManager1.getForums().size());
|
||||
assertTrue(forumManager1.getForums().contains(forum0));
|
||||
|
||||
// sharer shares forum with invitee
|
||||
Contact c1 = contactManager0.getContact(contactId1);
|
||||
assertTrue(forumSharingManager0.getSharedWith(forum0.getId())
|
||||
.contains(c1));
|
||||
// invitee gets forum shared by sharer
|
||||
Contact contact0 = contactManager1.getContact(contactId1);
|
||||
assertTrue(forumSharingManager1.getSharedBy(forum0.getId())
|
||||
.contains(contact0));
|
||||
|
||||
// sharer un-subscribes from forum
|
||||
forumManager0.removeForum(forum0);
|
||||
|
||||
// send leave message to invitee
|
||||
syncToInvitee();
|
||||
|
||||
// forum is gone for sharer, but not invitee
|
||||
assertEquals(0, forumManager0.getForums().size());
|
||||
assertEquals(1, forumManager1.getForums().size());
|
||||
|
||||
// invitee no longer shares forum with sharer
|
||||
Contact c0 = contactManager1.getContact(contactId0);
|
||||
assertFalse(forumSharingManager1.getSharedWith(forum0.getId())
|
||||
.contains(c0));
|
||||
// sharer no longer gets forum shared by invitee
|
||||
assertFalse(forumSharingManager1.getSharedBy(forum0.getId())
|
||||
.contains(contact0));
|
||||
// forum can be shared again
|
||||
assertTrue(forumSharingManager1.canBeShared(forum0.getId(), c0));
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSharerLeavesBeforeResponse() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize and let invitee accept all requests
|
||||
defaultInit(true);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, null);
|
||||
|
||||
// sharer un-subscribes from forum
|
||||
forumManager0.removeForum(forum0);
|
||||
|
||||
// from her on expect the response to fail with a DbException
|
||||
thrown.expect(DbException.class);
|
||||
|
||||
// sync first request message and leave message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// invitee has no forums available
|
||||
assertEquals(0, forumSharingManager1.getAvailableForums().size());
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSessionIdReuse() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize and let invitee accept all requests
|
||||
defaultInit(true);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, "Hi!");
|
||||
|
||||
// sync first request message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// sync response back
|
||||
syncToSharer();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener0.responseReceived);
|
||||
|
||||
// forum was added successfully
|
||||
assertEquals(1, forumManager1.getForums().size());
|
||||
|
||||
// reset event received state
|
||||
listener1.requestReceived = false;
|
||||
|
||||
// get SessionId from invitation
|
||||
List<ForumInvitationMessage> list = new ArrayList<>(
|
||||
forumSharingManager1
|
||||
.getForumInvitationMessages(contactId0));
|
||||
assertEquals(1, list.size());
|
||||
ForumInvitationMessage msg = list.get(0);
|
||||
SessionId sessionId = msg.getSessionId();
|
||||
|
||||
// get all sorts of stuff needed to send a message
|
||||
DatabaseComponent db = t0.getDatabaseComponent();
|
||||
MessageQueueManager queue = t0.getMessageQueueManager();
|
||||
Contact c1 = contactManager0.getContact(contactId1);
|
||||
PrivateGroupFactory groupFactory = t0.getPrivateGroupFactory();
|
||||
Group group = groupFactory
|
||||
.createPrivateGroup(forumSharingManager0.getClientId(), c1);
|
||||
long time = clock.currentTimeMillis();
|
||||
BdfList bodyList = BdfList.of(SHARE_MSG_TYPE_INVITATION,
|
||||
sessionId.getBytes(),
|
||||
TestUtils.getRandomString(42),
|
||||
TestUtils.getRandomBytes(FORUM_SALT_LENGTH)
|
||||
);
|
||||
byte[] body = t0.getClientHelper().toByteArray(bodyList);
|
||||
|
||||
// add the message to the queue
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
queue.sendMessage(txn, group, time, body, new Metadata());
|
||||
txn.setComplete();
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
|
||||
// actually send the message
|
||||
syncToInvitee();
|
||||
// make sure there was no new request received
|
||||
assertFalse(listener1.requestReceived);
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSharingSameForumWithEachOther() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize and let invitee accept all requests
|
||||
defaultInit(true);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, "Hi!");
|
||||
|
||||
// sync first request message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// sync response back
|
||||
syncToSharer();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener0.responseReceived);
|
||||
|
||||
// forum was added successfully
|
||||
assertEquals(1, forumManager1.getForums().size());
|
||||
|
||||
// invitee now shares same forum back
|
||||
forumSharingManager1.sendForumInvitation(forum0.getId(),
|
||||
contactId0,
|
||||
"I am re-sharing this forum with you.");
|
||||
|
||||
// sync re-share invitation
|
||||
syncToSharer();
|
||||
|
||||
// make sure that no new request was received
|
||||
assertFalse(listener0.requestReceived);
|
||||
assertEquals(1,
|
||||
forumSharingManager0.getForumInvitationMessages(contactId1)
|
||||
.size());
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContactRemoved() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize and let invitee accept all requests
|
||||
defaultInit(true);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, "Hi!");
|
||||
|
||||
// sync first request message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// sync response back
|
||||
syncToSharer();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener0.responseReceived);
|
||||
|
||||
// forum was added successfully
|
||||
assertEquals(1, forumManager1.getForums().size());
|
||||
assertEquals(1,
|
||||
forumSharingManager0.getSharedWith(forum0.getId()).size());
|
||||
|
||||
// remember SessionId from invitation
|
||||
List<ForumInvitationMessage> list = new ArrayList<>(
|
||||
forumSharingManager1
|
||||
.getForumInvitationMessages(contactId0));
|
||||
assertEquals(1, list.size());
|
||||
ForumInvitationMessage msg = list.get(0);
|
||||
SessionId sessionId = msg.getSessionId();
|
||||
|
||||
// contacts now remove each other
|
||||
contactManager0.removeContact(contactId1);
|
||||
contactManager2.removeContact(contactId21);
|
||||
contactManager1.removeContact(contactId0);
|
||||
contactManager1.removeContact(contactId2);
|
||||
|
||||
// make sure sharer does share the forum with nobody now
|
||||
assertEquals(0,
|
||||
forumSharingManager0.getSharedWith(forum0.getId()).size());
|
||||
|
||||
// contacts add each other again
|
||||
addDefaultContacts();
|
||||
|
||||
// get all sorts of stuff needed to send a message
|
||||
DatabaseComponent db = t0.getDatabaseComponent();
|
||||
MessageQueueManager queue = t0.getMessageQueueManager();
|
||||
Contact c1 = contactManager0.getContact(contactId1);
|
||||
PrivateGroupFactory groupFactory = t0.getPrivateGroupFactory();
|
||||
Group group = groupFactory
|
||||
.createPrivateGroup(forumSharingManager0.getClientId(), c1);
|
||||
long time = clock.currentTimeMillis();
|
||||
|
||||
// construct a new message re-using the old SessionId
|
||||
BdfList bodyList = BdfList.of(SHARE_MSG_TYPE_INVITATION,
|
||||
sessionId.getBytes(),
|
||||
TestUtils.getRandomString(42),
|
||||
TestUtils.getRandomBytes(FORUM_SALT_LENGTH)
|
||||
);
|
||||
byte[] body = t0.getClientHelper().toByteArray(bodyList);
|
||||
|
||||
// add the message to the queue
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
queue.sendMessage(txn, group, time, body, new Metadata());
|
||||
txn.setComplete();
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
|
||||
// actually send the message
|
||||
syncToInvitee();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
// make sure the new request was received with the same sessionId
|
||||
// as proof that the state got deleted along with contacts
|
||||
assertTrue(listener1.requestReceived);
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTwoContactsShareSameForum() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// initialize
|
||||
addDefaultIdentities();
|
||||
addDefaultContacts();
|
||||
addForumForSharer();
|
||||
|
||||
// second sharer adds the same forum
|
||||
DatabaseComponent db2 = t2.getDatabaseComponent();
|
||||
Transaction txn = db2.startTransaction(false);
|
||||
db2.addGroup(txn, forum0.getGroup());
|
||||
txn.setComplete();
|
||||
db2.endTransaction(txn);
|
||||
|
||||
// add listeners
|
||||
listener0 = new SharerListener();
|
||||
t0.getEventBus().addListener(listener0);
|
||||
listener1 = new InviteeListener(true, false);
|
||||
t1.getEventBus().addListener(listener1);
|
||||
listener2 = new SharerListener();
|
||||
t2.getEventBus().addListener(listener2);
|
||||
|
||||
// send invitation
|
||||
forumSharingManager0
|
||||
.sendForumInvitation(forum0.getId(), contactId1, "Hi!");
|
||||
// sync first request message
|
||||
syncToInvitee();
|
||||
|
||||
// second sharer sends invitation for same forum
|
||||
forumSharingManager2
|
||||
.sendForumInvitation(forum0.getId(), contactId1, null);
|
||||
// sync second request message
|
||||
deliverMessage(sync2, contactId2, sync1, contactId1,
|
||||
"Sharer2 to Invitee");
|
||||
|
||||
// make sure we have only one forum available
|
||||
Collection<Forum> forums =
|
||||
forumSharingManager1.getAvailableForums();
|
||||
assertEquals(1, forums.size());
|
||||
|
||||
// make sure both sharers actually share the forum
|
||||
Collection<Contact> contacts =
|
||||
forumSharingManager1.getSharedBy(forum0.getId());
|
||||
assertEquals(2, contacts.size());
|
||||
|
||||
// answer second request
|
||||
Contact c2 = contactManager1.getContact(contactId2);
|
||||
forumSharingManager1.respondToInvitation(forum0, c2, true);
|
||||
// sync response
|
||||
deliverMessage(sync1, contactId21, sync2, contactId2,
|
||||
"Invitee to Sharer2");
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener2.responseReceived);
|
||||
|
||||
// answer first request
|
||||
Contact c0 =
|
||||
contactManager1.getContact(contactId0);
|
||||
forumSharingManager1.respondToInvitation(forum0, c0, true);
|
||||
// sync response
|
||||
syncToSharer();
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener0.responseReceived);
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@After
|
||||
public void tearDown() throws InterruptedException {
|
||||
TestUtils.deleteTestDirectory(testDir);
|
||||
}
|
||||
|
||||
private class SharerListener implements EventListener {
|
||||
|
||||
public volatile boolean requestReceived = false;
|
||||
public volatile boolean responseReceived = false;
|
||||
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof MessageValidatedEvent) {
|
||||
MessageValidatedEvent event = (MessageValidatedEvent) e;
|
||||
if (event.getClientId()
|
||||
.equals(forumSharingManager0.getClientId()) &&
|
||||
!event.isLocal()) {
|
||||
LOG.info("TEST: Sharer received message in group " +
|
||||
((MessageValidatedEvent) e).getMessage()
|
||||
.getGroupId().hashCode());
|
||||
msgWaiter.resume();
|
||||
}
|
||||
} else if (e instanceof ForumInvitationResponseReceivedEvent) {
|
||||
ForumInvitationResponseReceivedEvent event =
|
||||
(ForumInvitationResponseReceivedEvent) e;
|
||||
eventWaiter.assertEquals(contactId1, event.getContactId());
|
||||
responseReceived = true;
|
||||
eventWaiter.resume();
|
||||
}
|
||||
// this is only needed for tests where a forum is re-shared
|
||||
else if (e instanceof ForumInvitationReceivedEvent) {
|
||||
ForumInvitationReceivedEvent event =
|
||||
(ForumInvitationReceivedEvent) e;
|
||||
eventWaiter.assertEquals(contactId1, event.getContactId());
|
||||
requestReceived = true;
|
||||
Forum f = event.getForum();
|
||||
try {
|
||||
Contact c = contactManager0.getContact(contactId1);
|
||||
forumSharingManager0.respondToInvitation(f, c, true);
|
||||
} catch (DbException ex) {
|
||||
eventWaiter.rethrow(ex);
|
||||
} finally {
|
||||
eventWaiter.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class InviteeListener implements EventListener {
|
||||
|
||||
public volatile boolean requestReceived = false;
|
||||
public volatile boolean responseReceived = false;
|
||||
|
||||
private final boolean accept, answer;
|
||||
|
||||
InviteeListener(boolean accept, boolean answer) {
|
||||
this.accept = accept;
|
||||
this.answer = answer;
|
||||
}
|
||||
|
||||
InviteeListener(boolean accept) {
|
||||
this(accept, true);
|
||||
}
|
||||
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof MessageValidatedEvent) {
|
||||
MessageValidatedEvent event = (MessageValidatedEvent) e;
|
||||
if (event.getClientId()
|
||||
.equals(forumSharingManager1.getClientId()) &&
|
||||
!event.isLocal()) {
|
||||
LOG.info("TEST: Invitee received message in group " +
|
||||
((MessageValidatedEvent) e).getMessage()
|
||||
.getGroupId().hashCode());
|
||||
msgWaiter.resume();
|
||||
}
|
||||
} else if (e instanceof ForumInvitationReceivedEvent) {
|
||||
ForumInvitationReceivedEvent event =
|
||||
(ForumInvitationReceivedEvent) e;
|
||||
requestReceived = true;
|
||||
if (!answer) return;
|
||||
Forum f = event.getForum();
|
||||
try {
|
||||
Contact c =
|
||||
contactManager1.getContact(event.getContactId());
|
||||
forumSharingManager1.respondToInvitation(f, c, accept);
|
||||
} catch (DbException ex) {
|
||||
eventWaiter.rethrow(ex);
|
||||
} finally {
|
||||
eventWaiter.resume();
|
||||
}
|
||||
}
|
||||
// this is only needed for tests where a forum is re-shared
|
||||
else if (e instanceof ForumInvitationResponseReceivedEvent) {
|
||||
ForumInvitationResponseReceivedEvent event =
|
||||
(ForumInvitationResponseReceivedEvent) e;
|
||||
eventWaiter.assertEquals(contactId0, event.getContactId());
|
||||
responseReceived = true;
|
||||
eventWaiter.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startLifecycles() throws InterruptedException {
|
||||
// Start the lifecycle manager and wait for it to finish
|
||||
lifecycleManager0 = t0.getLifecycleManager();
|
||||
lifecycleManager1 = t1.getLifecycleManager();
|
||||
lifecycleManager2 = t2.getLifecycleManager();
|
||||
lifecycleManager0.startServices();
|
||||
lifecycleManager1.startServices();
|
||||
lifecycleManager2.startServices();
|
||||
lifecycleManager0.waitForStartup();
|
||||
lifecycleManager1.waitForStartup();
|
||||
lifecycleManager2.waitForStartup();
|
||||
}
|
||||
|
||||
private void stopLifecycles() throws InterruptedException {
|
||||
// Clean up
|
||||
lifecycleManager0.stopServices();
|
||||
lifecycleManager1.stopServices();
|
||||
lifecycleManager2.stopServices();
|
||||
lifecycleManager0.waitForShutdown();
|
||||
lifecycleManager1.waitForShutdown();
|
||||
lifecycleManager2.waitForShutdown();
|
||||
}
|
||||
|
||||
private void defaultInit(boolean accept) throws DbException {
|
||||
addDefaultIdentities();
|
||||
addDefaultContacts();
|
||||
addForumForSharer();
|
||||
listenToEvents(accept);
|
||||
}
|
||||
|
||||
private void addDefaultIdentities() throws DbException {
|
||||
author0 = authorFactory.createLocalAuthor(SHARER,
|
||||
TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
|
||||
TestUtils.getRandomBytes(123));
|
||||
identityManager0.addLocalAuthor(author0);
|
||||
author1 = authorFactory.createLocalAuthor(INVITEE,
|
||||
TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
|
||||
TestUtils.getRandomBytes(123));
|
||||
identityManager1.addLocalAuthor(author1);
|
||||
author2 = authorFactory.createLocalAuthor(SHARER2,
|
||||
TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
|
||||
TestUtils.getRandomBytes(123));
|
||||
identityManager2.addLocalAuthor(author2);
|
||||
}
|
||||
|
||||
private void addDefaultContacts() throws DbException {
|
||||
// sharer adds invitee as contact
|
||||
contactId1 = contactManager0.addContact(author1,
|
||||
author0.getId(), master, clock.currentTimeMillis(), true,
|
||||
true
|
||||
);
|
||||
// second sharer does the same
|
||||
contactId21 = contactManager2.addContact(author1,
|
||||
author2.getId(), master, clock.currentTimeMillis(), true,
|
||||
true
|
||||
);
|
||||
// invitee adds sharers back
|
||||
contactId0 = contactManager1.addContact(author0,
|
||||
author1.getId(), master, clock.currentTimeMillis(), true,
|
||||
true
|
||||
);
|
||||
contactId2 = contactManager1.addContact(author2,
|
||||
author1.getId(), master, clock.currentTimeMillis(), true,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private void addForumForSharer() throws DbException {
|
||||
// sharer creates forum
|
||||
forum0 = forumManager0.addForum("Test Forum");
|
||||
}
|
||||
|
||||
private void listenToEvents(boolean accept) {
|
||||
listener0 = new SharerListener();
|
||||
t0.getEventBus().addListener(listener0);
|
||||
listener1 = new InviteeListener(accept);
|
||||
t1.getEventBus().addListener(listener1);
|
||||
listener2 = new SharerListener();
|
||||
t2.getEventBus().addListener(listener2);
|
||||
}
|
||||
|
||||
private void syncToInvitee() throws IOException, TimeoutException {
|
||||
deliverMessage(sync0, contactId0, sync1, contactId1,
|
||||
"Sharer to Invitee");
|
||||
}
|
||||
|
||||
private void syncToSharer() throws IOException, TimeoutException {
|
||||
deliverMessage(sync1, contactId1, sync0, contactId0,
|
||||
"Invitee to Sharer");
|
||||
}
|
||||
|
||||
private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId,
|
||||
SyncSessionFactory toSync, ContactId toId, String debug)
|
||||
throws IOException, TimeoutException {
|
||||
|
||||
if (debug != null) LOG.info("TEST: Sending message from " + debug);
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
// Create an outgoing sync session
|
||||
SyncSession sessionFrom =
|
||||
fromSync.createSimplexOutgoingSession(toId, MAX_LATENCY, out);
|
||||
// Write whatever needs to be written
|
||||
sessionFrom.run();
|
||||
out.close();
|
||||
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
// Create an incoming sync session
|
||||
SyncSession sessionTo = toSync.createIncomingSession(fromId, in);
|
||||
// Read whatever needs to be read
|
||||
sessionTo.run();
|
||||
in.close();
|
||||
|
||||
// wait for message to actually arrive
|
||||
msgWaiter.await(TIMEOUT, 1);
|
||||
}
|
||||
|
||||
private void injectEagerSingletons(
|
||||
ForumSharingIntegrationTestComponent component) {
|
||||
|
||||
component.inject(new LifecycleModule.EagerSingletons());
|
||||
component.inject(new ForumModule.EagerSingletons());
|
||||
component.inject(new CryptoModule.EagerSingletons());
|
||||
component.inject(new ContactModule.EagerSingletons());
|
||||
component.inject(new TransportModule.EagerSingletons());
|
||||
component.inject(new SyncModule.EagerSingletons());
|
||||
component.inject(new PropertiesModule.EagerSingletons());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.briarproject;
|
||||
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.MessageQueueManager;
|
||||
import org.briarproject.api.clients.PrivateGroupFactory;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.sync.SyncSessionFactory;
|
||||
import org.briarproject.clients.ClientsModule;
|
||||
import org.briarproject.contact.ContactModule;
|
||||
import org.briarproject.crypto.CryptoModule;
|
||||
import org.briarproject.data.DataModule;
|
||||
import org.briarproject.db.DatabaseModule;
|
||||
import org.briarproject.event.EventModule;
|
||||
import org.briarproject.forum.ForumModule;
|
||||
import org.briarproject.identity.IdentityModule;
|
||||
import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.properties.PropertiesModule;
|
||||
import org.briarproject.sync.SyncModule;
|
||||
import org.briarproject.system.SystemModule;
|
||||
import org.briarproject.transport.TransportModule;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {
|
||||
TestDatabaseModule.class,
|
||||
TestPluginsModule.class,
|
||||
TestSeedProviderModule.class,
|
||||
ClientsModule.class,
|
||||
ContactModule.class,
|
||||
CryptoModule.class,
|
||||
DataModule.class,
|
||||
DatabaseModule.class,
|
||||
EventModule.class,
|
||||
ForumModule.class,
|
||||
IdentityModule.class,
|
||||
LifecycleModule.class,
|
||||
PropertiesModule.class,
|
||||
SyncModule.class,
|
||||
SystemModule.class,
|
||||
TransportModule.class
|
||||
})
|
||||
public interface ForumSharingIntegrationTestComponent {
|
||||
|
||||
void inject(ForumSharingIntegrationTest testCase);
|
||||
|
||||
void inject(ContactModule.EagerSingletons init);
|
||||
|
||||
void inject(CryptoModule.EagerSingletons init);
|
||||
|
||||
void inject(ForumModule.EagerSingletons init);
|
||||
|
||||
void inject(LifecycleModule.EagerSingletons init);
|
||||
|
||||
void inject(PropertiesModule.EagerSingletons init);
|
||||
|
||||
void inject(SyncModule.EagerSingletons init);
|
||||
|
||||
void inject(TransportModule.EagerSingletons init);
|
||||
|
||||
LifecycleManager getLifecycleManager();
|
||||
|
||||
EventBus getEventBus();
|
||||
|
||||
IdentityManager getIdentityManager();
|
||||
|
||||
ContactManager getContactManager();
|
||||
|
||||
ForumSharingManager getForumSharingManager();
|
||||
|
||||
ForumManager getForumManager();
|
||||
|
||||
SyncSessionFactory getSyncSessionFactory();
|
||||
|
||||
/* the following methods are only needed to manually construct messages */
|
||||
|
||||
DatabaseComponent getDatabaseComponent();
|
||||
|
||||
PrivateGroupFactory getPrivateGroupFactory();
|
||||
|
||||
ClientHelper getClientHelper();
|
||||
|
||||
MessageQueueManager getMessageQueueManager();
|
||||
|
||||
}
|
||||
@@ -832,6 +832,106 @@ public class IntroductionIntegrationTest extends BriarTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntroduceesRemovedCleanup() throws Exception {
|
||||
startLifecycles();
|
||||
try {
|
||||
// Add Identities
|
||||
addDefaultIdentities();
|
||||
|
||||
// Add Transport Properties
|
||||
addTransportProperties();
|
||||
|
||||
// Add introducees as contacts
|
||||
contactId1 = contactManager0.addContact(author1,
|
||||
author0.getId(), master, clock.currentTimeMillis(), true,
|
||||
true
|
||||
);
|
||||
contactId2 = contactManager0.addContact(author2,
|
||||
author0.getId(), master, clock.currentTimeMillis(), true,
|
||||
true
|
||||
);
|
||||
// Add introducer back
|
||||
contactId0 = contactManager1.addContact(author0,
|
||||
author1.getId(), master, clock.currentTimeMillis(), true,
|
||||
true
|
||||
);
|
||||
ContactId contactId02 = contactManager2.addContact(author0,
|
||||
author2.getId(), master, clock.currentTimeMillis(), true,
|
||||
true
|
||||
);
|
||||
assertTrue(contactId0.equals(contactId02));
|
||||
|
||||
// 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 first request message
|
||||
deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1");
|
||||
eventWaiter.await(TIMEOUT, 1);
|
||||
assertTrue(listener1.requestReceived);
|
||||
|
||||
// get database and local group for introducee
|
||||
DatabaseComponent db0 = t0.getDatabaseComponent();
|
||||
IntroductionGroupFactory groupFactory0 =
|
||||
t0.getIntroductionGroupFactory();
|
||||
Group group1 = groupFactory0.createLocalGroup();
|
||||
|
||||
// get local session state messages
|
||||
Map<MessageId, Metadata> map;
|
||||
Transaction txn = db0.startTransaction(false);
|
||||
try {
|
||||
map = db0.getMessageMetadata(txn, group1.getId());
|
||||
txn.setComplete();
|
||||
} finally {
|
||||
db0.endTransaction(txn);
|
||||
}
|
||||
// check that we have one session state
|
||||
assertEquals(1, map.size());
|
||||
|
||||
// introducer removes introducee1
|
||||
contactManager0.removeContact(contactId1);
|
||||
|
||||
// get local session state messages again
|
||||
txn = db0.startTransaction(false);
|
||||
try {
|
||||
map = db0.getMessageMetadata(txn, group1.getId());
|
||||
txn.setComplete();
|
||||
} finally {
|
||||
db0.endTransaction(txn);
|
||||
}
|
||||
// make sure local state is still there
|
||||
assertEquals(1, map.size());
|
||||
|
||||
// introducer removes other introducee
|
||||
contactManager0.removeContact(contactId2);
|
||||
|
||||
// get local session state messages again
|
||||
txn = db0.startTransaction(false);
|
||||
try {
|
||||
map = db0.getMessageMetadata(txn, group1.getId());
|
||||
txn.setComplete();
|
||||
} finally {
|
||||
db0.endTransaction(txn);
|
||||
}
|
||||
// make sure local state is gone now
|
||||
assertEquals(0, map.size());
|
||||
} finally {
|
||||
stopLifecycles();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO add a test for faking responses when #256 is implemented
|
||||
|
||||
@After
|
||||
|
||||
@@ -2,6 +2,11 @@ package org.briarproject;
|
||||
|
||||
import org.briarproject.api.UniqueId;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.PrivateKey;
|
||||
import org.briarproject.api.forum.ForumConstants;
|
||||
import org.briarproject.api.forum.ForumPost;
|
||||
import org.briarproject.api.forum.ForumPostFactory;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.messaging.MessagingConstants;
|
||||
import org.briarproject.api.messaging.PrivateMessage;
|
||||
@@ -13,6 +18,9 @@ import org.junit.Test;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
|
||||
import static org.briarproject.api.sync.SyncConstants.MAX_PACKET_PAYLOAD_LENGTH;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -25,6 +33,8 @@ public class MessageSizeIntegrationTest extends BriarTestCase {
|
||||
AuthorFactory authorFactory;
|
||||
@Inject
|
||||
PrivateMessageFactory privateMessageFactory;
|
||||
@Inject
|
||||
ForumPostFactory forumPostFactory;
|
||||
|
||||
public MessageSizeIntegrationTest() throws Exception {
|
||||
MessageSizeIntegrationTestComponent component =
|
||||
@@ -51,4 +61,30 @@ public class MessageSizeIntegrationTest extends BriarTestCase {
|
||||
+ MAX_PRIVATE_MESSAGE_BODY_LENGTH);
|
||||
assertTrue(length <= MAX_PACKET_PAYLOAD_LENGTH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForumPostFitsIntoPacket() throws Exception {
|
||||
// Create a maximum-length author
|
||||
String authorName = TestUtils.getRandomString(
|
||||
MAX_AUTHOR_NAME_LENGTH);
|
||||
byte[] authorPublic = new byte[MAX_PUBLIC_KEY_LENGTH];
|
||||
Author author = authorFactory.createAuthor(authorName, authorPublic);
|
||||
// Create a maximum-length forum post
|
||||
GroupId groupId = new GroupId(TestUtils.getRandomId());
|
||||
long timestamp = Long.MAX_VALUE;
|
||||
MessageId parent = new MessageId(TestUtils.getRandomId());
|
||||
String contentType = TestUtils.getRandomString(
|
||||
ForumConstants.MAX_CONTENT_TYPE_LENGTH);
|
||||
byte[] body = new byte[MAX_FORUM_POST_BODY_LENGTH];
|
||||
PrivateKey privateKey = crypto.generateSignatureKeyPair().getPrivate();
|
||||
ForumPost post = forumPostFactory.createPseudonymousPost(groupId,
|
||||
timestamp, parent, author, contentType, body, privateKey);
|
||||
// Check the size of the serialised message
|
||||
int length = post.getMessage().getRaw().length;
|
||||
assertTrue(length > UniqueId.LENGTH + 8 + UniqueId.LENGTH
|
||||
+ MAX_AUTHOR_NAME_LENGTH + MAX_PUBLIC_KEY_LENGTH
|
||||
+ ForumConstants.MAX_CONTENT_TYPE_LENGTH
|
||||
+ MAX_FORUM_POST_BODY_LENGTH);
|
||||
assertTrue(length <= MAX_PACKET_PAYLOAD_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.briarproject.crypto.CryptoModule;
|
||||
import org.briarproject.data.DataModule;
|
||||
import org.briarproject.db.DatabaseModule;
|
||||
import org.briarproject.event.EventModule;
|
||||
import org.briarproject.forum.ForumModule;
|
||||
import org.briarproject.identity.IdentityModule;
|
||||
import org.briarproject.messaging.MessagingModule;
|
||||
import org.briarproject.sync.SyncModule;
|
||||
@@ -24,6 +25,7 @@ import dagger.Component;
|
||||
DataModule.class,
|
||||
DatabaseModule.class,
|
||||
EventModule.class,
|
||||
ForumModule.class,
|
||||
IdentityModule.class,
|
||||
MessagingModule.class,
|
||||
SyncModule.class,
|
||||
|
||||
@@ -98,6 +98,78 @@
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.forum.AvailableForumsActivity"
|
||||
android:label="@string/available_forums_title"
|
||||
android:parentActivityName=".android.NavDrawerActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".android.NavDrawerActivity"
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.forum.CreateForumActivity"
|
||||
android:label="@string/create_forum_title"
|
||||
android:parentActivityName=".android.NavDrawerActivity"
|
||||
android:windowSoftInputMode="stateVisible">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".android.NavDrawerActivity"
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.forum.ForumActivity"
|
||||
android:label="@string/app_name"
|
||||
android:parentActivityName=".android.NavDrawerActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".android.NavDrawerActivity"
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.forum.ReadForumPostActivity"
|
||||
android:label="@string/app_name"
|
||||
android:parentActivityName=".android.NavDrawerActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".android.NavDrawerActivity"
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.forum.ShareForumActivity"
|
||||
android:label="@string/forums_share_toolbar_header"
|
||||
android:parentActivityName=".android.forum.ForumActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".android.forum.ForumActivity"
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.forum.ForumSharingStatusActivity"
|
||||
android:label="@string/forum_sharing_status"
|
||||
android:parentActivityName=".android.forum.ForumActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".android.forum.ForumActivity"
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.forum.WriteForumPostActivity"
|
||||
android:label="@string/app_name"
|
||||
android:parentActivityName=".android.NavDrawerActivity"
|
||||
android:windowSoftInputMode="stateVisible">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".android.NavDrawerActivity"
|
||||
/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".android.identity.CreateIdentityActivity"
|
||||
android:label="@string/new_identity_title"
|
||||
|
||||
@@ -6,11 +6,6 @@ apply plugin: 'witness'
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
apply plugin: 'de.undercouch.download'
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def supportVersion = '23.2.1'
|
||||
compile project(':briar-api')
|
||||
|
||||
21
briar-android/res/drawable/bubble.xml
Normal file
21
briar-android/res/drawable/bubble.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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"/>
|
||||
|
||||
<padding
|
||||
android:left="@dimen/unread_bubble_padding_horizontal"
|
||||
android:right="@dimen/unread_bubble_padding_horizontal"/>
|
||||
|
||||
<solid
|
||||
android:color="@color/briar_primary"/>
|
||||
|
||||
<stroke
|
||||
android:color="@color/briar_text_primary_inverse"
|
||||
android:width="@dimen/avatar_border_width"/>
|
||||
|
||||
</shape>
|
||||
|
||||
9
briar-android/res/drawable/forum_item_create_white.xml
Normal file
9
briar-android/res/drawable/forum_item_create_white.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
||||
6
briar-android/res/layout/activity_available_forums.xml
Normal file
6
briar-android/res/layout/activity_available_forums.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.briarproject.android.util.BriarRecyclerView
|
||||
android:id="@+id/availableForumsView"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
49
briar-android/res/layout/activity_create_forum.xml
Normal file
49
briar-android/res/layout/activity_create_forum.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="20dp" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
android:text="@string/choose_forum_name" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/createForumNameEntry"
|
||||
android:maxLines="1"
|
||||
android:inputType="text|textCapSentences" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/createForumFeedback"
|
||||
android:gravity="center"
|
||||
android:paddingLeft="50dp"
|
||||
android:paddingRight="50dp" />
|
||||
|
||||
<Button
|
||||
style="@style/BriarButton"
|
||||
android:id="@+id/createForumButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="@string/create_forum_button" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/createForumProgressBar"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
52
briar-android/res/layout/activity_create_identity.xml
Normal file
52
briar-android/res/layout/activity_create_identity.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/margin_activity_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@string/choose_nickname"
|
||||
android:textSize="@dimen/text_size_large"/>
|
||||
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/nicknameInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:errorEnabled="true">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/nicknameEntry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:ems="10"
|
||||
android:inputType="textPersonName"
|
||||
android:maxLines="1"/>
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/createIdentityButton"
|
||||
style="@style/BriarButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:enabled="false"
|
||||
android:text="@string/create_identity_button"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
9
briar-android/res/layout/activity_expired.xml
Normal file
9
briar-android/res/layout/activity_expired.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:text="@string/expiry_warning"
|
||||
android:textSize="@dimen/text_size_large"/>
|
||||
48
briar-android/res/layout/activity_forum_sharing_status.xml
Normal file
48
briar-android/res/layout/activity_forum_sharing_status.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/default_separator_inverted"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:text="@string/forum_shared_by"
|
||||
android:textSize="@dimen/text_size_large"/>
|
||||
|
||||
<View style="@style/Divider.ForumList"/>
|
||||
|
||||
<org.briarproject.android.util.BriarRecyclerView
|
||||
android:id="@+id/sharedByView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/margin_medium"
|
||||
android:paddingTop="@dimen/margin_medium"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/default_separator_inverted"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:text="@string/forum_shared_with"
|
||||
android:textSize="@dimen/text_size_large"/>
|
||||
|
||||
<View style="@style/Divider.ForumList"/>
|
||||
|
||||
<org.briarproject.android.util.BriarRecyclerView
|
||||
android:id="@+id/sharedWithView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/margin_medium"
|
||||
android:paddingTop="@dimen/margin_medium"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
6
briar-android/res/layout/activity_share_forum.xml
Normal file
6
briar-android/res/layout/activity_share_forum.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
android:id="@+id/shareForumContainer"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
@@ -1,19 +1,19 @@
|
||||
<?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="match_parent">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatarView"
|
||||
style="@style/BriarAvatar"
|
||||
android:layout_width="@dimen/listitem_picture_size"
|
||||
android:layout_height="@dimen/listitem_picture_size"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin"
|
||||
app:civ_border_width="@dimen/avatar_border_width"
|
||||
app:civ_border_color="@color/briar_primary"/>
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/statusView"
|
||||
|
||||
@@ -9,11 +9,10 @@
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/contactAvatar"
|
||||
style="@style/BriarAvatar"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:transitionName="avatar"
|
||||
app:civ_border_color="@color/action_bar_text"
|
||||
app:civ_border_width="@dimen/avatar_border_width"
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
|
||||
<ImageView
|
||||
@@ -22,7 +21,7 @@
|
||||
android:layout_height="15dp"
|
||||
android:layout_gravity="bottom|right"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/contact_online"
|
||||
tools:ignore="ContentDescription"/>
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/contact_online"/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
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="match_parent"
|
||||
@@ -9,11 +8,11 @@
|
||||
|
||||
<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"
|
||||
app:civ_border_color="@color/briar_primary"
|
||||
app:civ_border_width="@dimen/avatar_border_width"/>
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nameView"
|
||||
|
||||
16
briar-android/res/layout/fragment_forum_list.xml
Normal file
16
briar-android/res/layout/fragment_forum_list.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:id="@+id/coordinatorLayout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.briarproject.android.util.BriarRecyclerView
|
||||
android:id="@+id/forumList"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="org.briarproject.android.util.BriarRecyclerViewBehavior"/>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
@@ -10,6 +10,11 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<org.briarproject.android.util.ViewfinderView
|
||||
android:id="@+id/viewfinder_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatarContact1"
|
||||
style="@style/BriarAvatar"
|
||||
android:layout_width="@dimen/listitem_picture_size"
|
||||
android:layout_height="@dimen/listitem_picture_size"
|
||||
android:layout_centerHorizontal="true"
|
||||
@@ -31,8 +32,6 @@
|
||||
android:layout_marginRight="@dimen/listitem_horizontal_margin"
|
||||
android:layout_toLeftOf="@+id/introductionIcon"
|
||||
android:layout_toStartOf="@+id/introductionIcon"
|
||||
app:civ_border_color="@color/briar_primary"
|
||||
app:civ_border_width="@dimen/avatar_border_width"
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
|
||||
<ImageView
|
||||
@@ -45,6 +44,7 @@
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatarContact2"
|
||||
style="@style/BriarAvatar"
|
||||
android:layout_width="@dimen/listitem_picture_size"
|
||||
android:layout_height="@dimen/listitem_picture_size"
|
||||
android:layout_centerHorizontal="true"
|
||||
@@ -53,8 +53,6 @@
|
||||
android:layout_toEndOf="@+id/introductionIcon"
|
||||
android:layout_toRightOf="@+id/introductionIcon"
|
||||
android:transitionName="avatar"
|
||||
app:civ_border_color="@color/briar_primary"
|
||||
app:civ_border_width="@dimen/avatar_border_width"
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
71
briar-android/res/layout/list_item_available_forum.xml
Normal file
71
briar-android/res/layout/list_item_available_forum.xml
Normal file
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
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_marginLeft="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin"
|
||||
android:paddingTop="@dimen/listitem_horizontal_margin"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<org.briarproject.android.util.TextAvatarView
|
||||
android:id="@+id/avatarView"
|
||||
android:layout_width="@dimen/avatar_forum_size"
|
||||
android:layout_height="@dimen/avatar_forum_size"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginRight="@dimen/listitem_horizontal_margin"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/forumNameView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@+id/avatarView"
|
||||
android:layout_toRightOf="@+id/avatarView"
|
||||
android:maxLines="2"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
tools:text="This is a name of a forum that is available"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sharedByView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/forumNameView"
|
||||
android:layout_marginBottom="-8dp"
|
||||
android:layout_toEndOf="@+id/avatarView"
|
||||
android:layout_toRightOf="@+id/avatarView"
|
||||
android:paddingTop="@dimen/margin_medium"
|
||||
android:textColor="@android:color/secondary_text_light"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
tools:text="Shared by Megalox"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/acceptButton"
|
||||
style="@style/BriarButtonFlat.Positive"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_button_accept"
|
||||
android:layout_below="@+id/sharedByView"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/declineButton"
|
||||
style="@style/BriarButtonFlat.Negative"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/dialog_button_decline"
|
||||
android:layout_below="@+id/sharedByView"
|
||||
android:layout_toLeftOf="@+id/acceptButton"
|
||||
android:layout_toStartOf="@+id/acceptButton"/>
|
||||
|
||||
<View style="@style/Divider.ForumList"
|
||||
android:layout_below="@+id/acceptButton"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -10,36 +10,55 @@
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/listitem_horizontal_margin"
|
||||
android:paddingBottom="@dimen/listitem_horizontal_margin"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingBottom="@dimen/listitem_horizontal_margin"
|
||||
android:paddingTop="@dimen/listitem_horizontal_margin"
|
||||
>
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatarView"
|
||||
android:layout_width="@dimen/listitem_picture_size"
|
||||
android:layout_height="@dimen/listitem_picture_size"
|
||||
<FrameLayout
|
||||
android:id="@+id/avatarFrameView"
|
||||
android:layout_width="@dimen/listitem_picture_frame_size"
|
||||
android:layout_height="@dimen/listitem_picture_frame_size"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin"
|
||||
android:transitionName="avatar"
|
||||
app:civ_border_color="@color/briar_primary"
|
||||
app:civ_border_width="@dimen/avatar_border_width"
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatarView"
|
||||
style="@style/BriarAvatar"
|
||||
android:layout_width="@dimen/listitem_picture_size"
|
||||
android:layout_height="@dimen/listitem_picture_size"
|
||||
android:layout_gravity="bottom|left"
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unreadCountView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/unread_bubble_size"
|
||||
android:layout_gravity="right|top"
|
||||
android:background="@drawable/bubble"
|
||||
android:gravity="center"
|
||||
android:minWidth="@dimen/unread_bubble_size"
|
||||
android:textColor="@color/briar_text_primary_inverse"
|
||||
android:textSize="@dimen/unread_bubble_text_size"
|
||||
android:textStyle="bold"
|
||||
tools:text="123"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/textViews"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin"
|
||||
android:layout_toEndOf="@+id/avatarFrameView"
|
||||
android:layout_toLeftOf="@+id/bulbView"
|
||||
android:layout_toRightOf="@+id/avatarView"
|
||||
android:layout_toEndOf="@+id/avatarView">
|
||||
android:layout_toRightOf="@+id/avatarFrameView"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nameView"
|
||||
|
||||
32
briar-android/res/layout/list_item_contact_small.xml
Normal file
32
briar-android/res/layout/list_item_contact_small.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="@dimen/margin_small"
|
||||
android:paddingTop="@dimen/margin_small">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatarView"
|
||||
style="@style/BriarAvatar"
|
||||
android:layout_width="@dimen/listitem_picture_size_small"
|
||||
android:layout_height="@dimen/listitem_picture_size_small"
|
||||
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin"
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nameView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/briar_text_primary"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
tools:text="This is a name of a contact"/>
|
||||
|
||||
</LinearLayout>
|
||||
68
briar-android/res/layout/list_item_forum.xml
Normal file
68
briar-android/res/layout/list_item_forum.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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"
|
||||
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin"
|
||||
android:paddingTop="@dimen/listitem_horizontal_margin"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<org.briarproject.android.util.TextAvatarView
|
||||
android:id="@+id/avatarView"
|
||||
android:layout_height="@dimen/avatar_forum_size"
|
||||
android:layout_width="@dimen/avatar_forum_size"
|
||||
android:layout_marginRight="@dimen/listitem_horizontal_margin"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/forumNameView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:textColor="@color/briar_text_primary"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
tools:text="This is a name of a forum"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toRightOf="@+id/avatarView"
|
||||
android:layout_toEndOf="@+id/avatarView"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/unreadView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/margin_medium"
|
||||
android:paddingBottom="@dimen/listitem_horizontal_margin"
|
||||
android:textColor="@color/briar_text_secondary"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
android:text="@string/no_unread_posts"
|
||||
android:layout_below="@+id/forumNameView"
|
||||
android:layout_toRightOf="@+id/avatarView"
|
||||
android:layout_toEndOf="@+id/avatarView"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dateView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@+id/forumNameView"
|
||||
android:paddingTop="@dimen/margin_medium"
|
||||
android:paddingBottom="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
|
||||
android:textColor="@color/briar_text_secondary"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
tools:text="Dec 24"/>
|
||||
|
||||
<View style="@style/Divider.ForumList"
|
||||
android:layout_below="@+id/unreadView"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
57
briar-android/res/layout/list_item_forum_invitation_in.xml
Normal file
57
briar-android/res/layout/list_item_forum_invitation_in.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/messageLayout"
|
||||
layout="@layout/list_item_msg_in"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/introductionLayout"
|
||||
android:layout_width="wrap_content"
|
||||
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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/introductionText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="80dp"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
android:textStyle="italic"
|
||||
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/showForumsButton"
|
||||
android:textColor="@color/private_message_date"
|
||||
android:textSize="@dimen/text_size_tiny"
|
||||
tools:text="Dec 24, 13:37"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/showForumsButton"
|
||||
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:text="@string/forum_show_available"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
56
briar-android/res/layout/list_item_forum_invitation_out.xml
Normal file
56
briar-android/res/layout/list_item_forum_invitation_out.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/messageLayout"
|
||||
layout="@layout/list_item_msg_out"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/introductionLayout"
|
||||
android:layout_width="wrap_content"
|
||||
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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/introductionText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
android:textStyle="italic"
|
||||
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:textColor="@color/private_message_date"
|
||||
android:textSize="@dimen/text_size_tiny"
|
||||
tools:text="Dec 24, 13:37"/>
|
||||
|
||||
<ImageView
|
||||
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"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/message_delivered"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatarView"
|
||||
style="@style/BriarAvatar"
|
||||
android:layout_width="@dimen/listitem_selectable_picture_size"
|
||||
android:layout_height="@dimen/listitem_selectable_picture_size"
|
||||
android:layout_alignParentLeft="true"
|
||||
@@ -22,8 +23,6 @@
|
||||
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
|
||||
android:layout_marginStart="@dimen/listitem_horizontal_margin"
|
||||
android:transitionName="avatar"
|
||||
app:civ_border_color="@color/briar_primary"
|
||||
app:civ_border_width="@dimen/avatar_border_width"
|
||||
tools:src="@drawable/ic_launcher"/>
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -46,6 +46,21 @@
|
||||
android:layout_height="@dimen/nav_separator_height"
|
||||
android:layout_marginLeft="@dimen/margin_large"/>
|
||||
|
||||
<android.support.v7.widget.AppCompatButton
|
||||
android:id="@+id/nav_btn_forums"
|
||||
style="@style/NavMenuButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableLeft="@drawable/social_chat"
|
||||
android:onClick="onNavigationClick"
|
||||
android:text="@string/forums_button"/>
|
||||
|
||||
<View
|
||||
style="@style/Divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/margin_separator"
|
||||
android:layout_marginLeft="@dimen/margin_large"/>
|
||||
|
||||
<android.support.v7.widget.AppCompatButton
|
||||
android:id="@+id/nav_btn_settings"
|
||||
style="@style/NavMenuButton"
|
||||
|
||||
43
briar-android/res/layout/share_forum_message.xml
Normal file
43
briar-android/res/layout/share_forum_message.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v4.widget.NestedScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="@dimen/margin_activity_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/introductionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:layout_weight="1"
|
||||
android:gravity="top"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
android:text="@string/forum_share_message"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/invitationMessageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:gravity="bottom"
|
||||
android:hint="@string/introduction_message_hint"
|
||||
android:inputType="text|textMultiLine|textCapSentences"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/shareForumButton"
|
||||
style="@style/BriarButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/forum_share_button"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.v4.widget.NestedScrollView>
|
||||
30
briar-android/res/layout/text_avatar_view.xml
Normal file
30
briar-android/res/layout/text_avatar_view.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
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">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatarBackground"
|
||||
style="@style/BriarAvatar"
|
||||
android:layout_width="@dimen/avatar_forum_size"
|
||||
android:layout_height="@dimen/avatar_forum_size"
|
||||
android:layout_gravity="center"
|
||||
android:src="@android:color/transparent"
|
||||
app:civ_fill_color="@color/briar_button_positive"/>
|
||||
|
||||
<android.support.v7.widget.AppCompatTextView
|
||||
android:id="@+id/textAvatarView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:maxLength="1"
|
||||
android:shadowColor="@color/forum_avatar_shadow"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="1.5"
|
||||
android:shadowRadius="1.5"
|
||||
android:textColor="@color/briar_text_primary_inverse"
|
||||
android:textSize="30sp"
|
||||
tools:text="T"/>
|
||||
|
||||
</merge>
|
||||
29
briar-android/res/menu/forum_actions.xml
Normal file
29
briar-android/res/menu/forum_actions.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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_forum_compose_post"
|
||||
android:icon="@drawable/forum_item_create_white"
|
||||
android:title="@string/forum_compose_post"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_forum_share"
|
||||
android:icon="@drawable/social_share_white"
|
||||
android:title="@string/forum_share_button"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_forum_sharing_status"
|
||||
android:title="@string/forum_sharing_status"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_forum_delete"
|
||||
android:icon="@drawable/action_delete_white"
|
||||
android:title="@string/forum_leave"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
||||
12
briar-android/res/menu/forum_list_actions.xml
Normal file
12
briar-android/res/menu/forum_list_actions.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?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_forum"
|
||||
android:icon="@drawable/ic_add_white"
|
||||
android:title="@string/create_forum_button"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
</menu>
|
||||
12
briar-android/res/menu/forum_share_actions.xml
Normal file
12
briar-android/res/menu/forum_share_actions.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?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_share_forum"
|
||||
android:icon="@drawable/ic_check_white"
|
||||
android:title="@string/forum_share_action"
|
||||
app:showAsAction="always"/>
|
||||
|
||||
</menu>
|
||||
@@ -16,7 +16,9 @@
|
||||
<color name="private_message_date_inverse">#e0e0e0</color>
|
||||
<color name="unread_background">#FFFFFF</color>
|
||||
<color name="horizontal_border">#CCCCCC</color>
|
||||
<color name="forums_available_background">@color/briar_gold</color>
|
||||
<color name="no_private_messages">#AAAAAA</color>
|
||||
<color name="forum_avatar_shadow">#99000000</color>
|
||||
|
||||
<color name="briar_primary">@color/briar_blue</color>
|
||||
<color name="briar_primary_dark">@color/briar_blue_dark</color>
|
||||
@@ -34,10 +36,17 @@
|
||||
|
||||
<!-- this is needed as preference_category_material layout uses this color as the text color -->
|
||||
<color name="preference_fallback_accent_color">@color/briar_accent</color>
|
||||
<color name="divider">#c1c1c1</color>
|
||||
<color name="default_separator">#000000</color>
|
||||
<color name="default_separator_inverted">#ffffff</color>
|
||||
<color name="menu_background">#FFFFFF</color>
|
||||
|
||||
<color name="spinner_border">#61000000</color> <!-- 38% Black -->
|
||||
<color name="spinner_arrow">@color/briar_blue_dark</color>
|
||||
|
||||
<!-- ViewfinderView -->
|
||||
<color name="possible_result_points">#c0ffbd21</color> <!-- Material Yellow 700 with alpha -->
|
||||
<color name="result_view">#b0000000</color>
|
||||
<color name="viewfinder_laser">#d50000</color> <!-- Red accent 700 -->
|
||||
<color name="viewfinder_mask">#60000000</color>
|
||||
</resources>
|
||||
@@ -26,10 +26,17 @@
|
||||
<dimen name="listitem_height_one_line_avatar">56dp</dimen>
|
||||
<dimen name="listitem_height_contact_selector">68dp</dimen>
|
||||
<dimen name="listitem_picture_size">48dp</dimen>
|
||||
<dimen name="listitem_picture_size_small">23dp</dimen>
|
||||
<dimen name="listitem_picture_frame_size">50dp</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">1dp</dimen>
|
||||
<dimen name="avatar_border_width">2dp</dimen>
|
||||
|
||||
<dimen name="unread_bubble_text_size">12sp</dimen>
|
||||
<dimen name="unread_bubble_border_width">2dp</dimen>
|
||||
<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>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<string name="contact_list_button">Contacts</string>
|
||||
<string name="delete_contact">Delete contact</string>
|
||||
<string name="contact_deleted_toast">Contact deleted</string>
|
||||
<string name="forums_button">Forums</string>
|
||||
<string name="settings_button">Settings</string>
|
||||
<string name="sign_out_button">Sign Out</string>
|
||||
<string name="contact_list_title">Contacts</string>
|
||||
@@ -65,26 +66,67 @@
|
||||
<string name="qr_code_invalid">The QR code is invalid</string>
|
||||
<string name="connecting_to_device">Connecting to device\u2026</string>
|
||||
<string name="authenticating_with_device">Authenticating with device\u2026</string>
|
||||
<string name="connection_aborted_local">Connection failed</string>
|
||||
<string name="connection_aborted_remote">Connection failed</string>
|
||||
<string name="connection_aborted_local">Connection aborted by us! This could mean that someone is trying to interfere with your connection</string>
|
||||
<string name="connection_aborted_remote">Connection aborted by your contact! This could mean that someone is trying to interfere with your connection</string>
|
||||
<string name="no_private_messages">No messages</string>
|
||||
<string name="private_message_hint">Type message</string>
|
||||
<string name="message_sent_toast">Message sent</string>
|
||||
<string name="forums_title">Forums</string>
|
||||
<string name="no_forums">You don\'t have any forums.\n\nWhy don\'t you create a new one yourself or ask your contacts to share one with you?</string>
|
||||
<plurals name="forums_shared">
|
||||
<item quantity="one">%d forum shared by contacts</item>
|
||||
<item quantity="other">%d forums shared by contacts</item>
|
||||
</plurals>
|
||||
<string name="show_forums">Show</string>
|
||||
<string name="forum_leave">Leave Forum</string>
|
||||
<string name="forum_left_toast">Left Forum</string>
|
||||
<string name="forum_sharing_status">Sharing Status</string>
|
||||
<string name="no_forum_posts">No posts</string>
|
||||
<string name="no_unread_posts">no unread posts</string>
|
||||
<plurals name="unread_posts">
|
||||
<item quantity="one">%d unread post</item>
|
||||
<item quantity="other">%d unread posts</item>
|
||||
</plurals>
|
||||
<string name="create_forum_title">New Forum</string>
|
||||
<string name="choose_forum_name">Choose a name for your forum:</string>
|
||||
<string name="create_forum_button">Create Forum</string>
|
||||
<string name="forum_created_toast">Forum created</string>
|
||||
<string name="forum_share_action">Share this forum with chosen contacts</string>
|
||||
<string name="forum_share_button">Share Forum</string>
|
||||
<string name="forum_shared_snackbar">Forum shared with chosen contacts</string>
|
||||
<string name="forum_share_message">You may compose an optional invitation message that will be sent to the selected contacts.</string>
|
||||
<string name="forum_invitation_received">%1$s has shared the forum \"%2$s\" with you.</string>
|
||||
<string name="forum_invitation_sent">You have shared the forum \"%1$s\" with %2$s.</string>
|
||||
<string name="forum_show_available">Show Available Forums</string>
|
||||
<string name="forum_compose_post">New Forum Post</string>
|
||||
<string name="from">From:</string>
|
||||
<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>
|
||||
<string name="forum_post_hint">Type forum post</string>
|
||||
<string name="available_forums_title">Available Forums</string>
|
||||
<string name="forum_joined_toast">Joined Forum</string>
|
||||
<string name="forum_declined_toast">Forum Invitation Declined</string>
|
||||
<string name="shared_by_format">Shared by %s</string>
|
||||
<string name="forum_shared_by">Shared by</string>
|
||||
<string name="forum_shared_with">Shared with</string>
|
||||
<string name="nobody">Nobody</string>
|
||||
<string name="no_contacts_prompt">You don\'t have any contacts. Add a contact now?</string>
|
||||
<string name="add_button">Add</string>
|
||||
<string name="cancel_button">Cancel</string>
|
||||
<string name="done_button">Done</string>
|
||||
<string name="delete_button">Delete</string>
|
||||
<string name="post_sent_toast">Forum post sent</string>
|
||||
<plurals name="private_message_notification_text">
|
||||
<item quantity="one">New private message.</item>
|
||||
<item quantity="other">%d new private messages.</item>
|
||||
</plurals>
|
||||
<plurals name="forum_post_notification_text">
|
||||
<item quantity="one">New forum post.</item>
|
||||
<item quantity="other">%d new forum posts.</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="settings_title">Settings</string>
|
||||
@@ -100,6 +142,7 @@
|
||||
<string name="panic_setting_hint">Configure how Briar will react when you use a panic button app</string>
|
||||
<string name="notification_settings_title">Notifications</string>
|
||||
<string name="notify_private_messages_setting">Show alerts for private messages</string>
|
||||
<string name="notify_forum_posts_setting">Show alerts for forum posts</string>
|
||||
<string name="notify_vibration_setting">Vibrate</string>
|
||||
<string name="notify_sound_setting">Sound</string>
|
||||
<string name="notify_sound_setting_default">Default ringtone</string>
|
||||
@@ -166,6 +209,8 @@
|
||||
<string name="dialog_welcome_message">Add a contact to start communicating securely or press the icon in the upper left corner of the screen for more options.</string>
|
||||
<string name="dialog_title_share_crash_report">Briar has crashed</string>
|
||||
<string name="dialog_message_share_crash_report">Would you like to review the crash report and send it to the developers? It will be stored encrypted on your device until the next time you log into Briar, and then sent securely to the developers.</string>
|
||||
<string name="dialog_title_leave_forum">Confirm Leaving Forum</string>
|
||||
<string name="dialog_message_leave_forum">Are you sure that you want to leave this forum? Contacts you have shared this forum with might get cut off from receiving updates for this forum.</string>
|
||||
<string name="dialog_button_ok">OK</string>
|
||||
<string name="dialog_button_leave">Leave</string>
|
||||
<string name="dialog_button_introduce">Introduce</string>
|
||||
@@ -175,6 +220,8 @@
|
||||
<string name="dashboard_toolbar_header">Briar</string>
|
||||
<string name="settings_toolbar_header">Settings</string>
|
||||
<string name="contacts_toolbar_header">Contacts</string>
|
||||
<string name="forums_toolbar_header">Forums</string>
|
||||
<string name="forums_share_toolbar_header">Choose Contacts</string>
|
||||
<!-- Progress titles -->
|
||||
<string name="progress_title_logout">Signing out of Briar..</string>
|
||||
<string name="progress_title_please_wait">Please wait..</string>
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
</style>
|
||||
|
||||
<style name="Divider">
|
||||
<item name="android:background">?android:attr/listDivider</item>
|
||||
<item name="android:background">@color/divider</item>
|
||||
</style>
|
||||
|
||||
<style name="Divider.Horizontal" parent="Divider">
|
||||
@@ -112,6 +112,14 @@
|
||||
<item name="android:layout_height">1dp</item>
|
||||
</style>
|
||||
|
||||
<style name="BriarAvatar">
|
||||
<item name="civ_border_width">@dimen/avatar_border_width</item>
|
||||
<item name="civ_border_color">@color/briar_primary</item>
|
||||
|
||||
<!-- Remove when we are using 'de.hdodenhof:circleimageview:2.1.0' -->
|
||||
<item name="civ_border_overlay">true</item>
|
||||
</style>
|
||||
|
||||
<style name="NavMenuButton" parent="Widget.AppCompat.Button.Borderless.Colored">
|
||||
<item name="android:textSize">@dimen/text_size_medium</item>
|
||||
<item name="android:textColor">@android:color/tertiary_text_light</item>
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
android:persistent="false"
|
||||
android:title="@string/notify_private_messages_setting"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="true"
|
||||
android:key="pref_key_notify_forum_posts"
|
||||
android:persistent="false"
|
||||
android:title="@string/notify_forum_posts_setting"/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="true"
|
||||
android:key="pref_key_notify_vibration"
|
||||
|
||||
@@ -3,6 +3,15 @@ package org.briarproject.android;
|
||||
import android.app.Activity;
|
||||
|
||||
import org.briarproject.android.contact.ConversationActivity;
|
||||
import org.briarproject.android.forum.AvailableForumsActivity;
|
||||
import org.briarproject.android.forum.ContactSelectorFragment;
|
||||
import org.briarproject.android.forum.CreateForumActivity;
|
||||
import org.briarproject.android.forum.ForumActivity;
|
||||
import org.briarproject.android.forum.ForumSharingStatusActivity;
|
||||
import org.briarproject.android.forum.ReadForumPostActivity;
|
||||
import org.briarproject.android.forum.ShareForumActivity;
|
||||
import org.briarproject.android.forum.ShareForumMessageFragment;
|
||||
import org.briarproject.android.forum.WriteForumPostActivity;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.android.identity.CreateIdentityActivity;
|
||||
import org.briarproject.android.introduction.IntroductionActivity;
|
||||
@@ -43,6 +52,20 @@ public interface ActivityComponent {
|
||||
|
||||
void inject(CreateIdentityActivity activity);
|
||||
|
||||
void inject(AvailableForumsActivity activity);
|
||||
|
||||
void inject(WriteForumPostActivity activity);
|
||||
|
||||
void inject(CreateForumActivity activity);
|
||||
|
||||
void inject(ShareForumActivity activity);
|
||||
|
||||
void inject(ForumSharingStatusActivity activity);
|
||||
|
||||
void inject(ReadForumPostActivity activity);
|
||||
|
||||
void inject(ForumActivity activity);
|
||||
|
||||
void inject(SettingsActivity activity);
|
||||
|
||||
void inject(IntroductionActivity activity);
|
||||
@@ -50,6 +73,9 @@ public interface ActivityComponent {
|
||||
@Named("ContactListFragment")
|
||||
BaseFragment newContactListFragment();
|
||||
|
||||
@Named("ForumListFragment")
|
||||
BaseFragment newForumListFragment();
|
||||
|
||||
@Named("ChooseIdentityFragment")
|
||||
BaseFragment newChooseIdentityFragment();
|
||||
|
||||
@@ -59,6 +85,12 @@ public interface ActivityComponent {
|
||||
@Named("ContactChooserFragment")
|
||||
BaseFragment newContactChooserFragment();
|
||||
|
||||
@Named("ContactSelectorFragment")
|
||||
ContactSelectorFragment newContactSelectorFragment();
|
||||
|
||||
@Named("ShareForumMessageFragment")
|
||||
ShareForumMessageFragment newShareForumMessageFragment();
|
||||
|
||||
@Named("IntroductionMessageFragment")
|
||||
IntroductionMessageFragment newIntroductionMessageFragment();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ import org.briarproject.android.controller.PasswordControllerImpl;
|
||||
import org.briarproject.android.controller.SetupController;
|
||||
import org.briarproject.android.controller.SetupControllerImpl;
|
||||
import org.briarproject.android.controller.TransportStateListener;
|
||||
import org.briarproject.android.forum.ContactSelectorFragment;
|
||||
import org.briarproject.android.forum.ForumListFragment;
|
||||
import org.briarproject.android.forum.ShareForumMessageFragment;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.android.introduction.ContactChooserFragment;
|
||||
import org.briarproject.android.introduction.IntroductionMessageFragment;
|
||||
@@ -113,6 +116,13 @@ public class ActivityModule {
|
||||
return new BriarServiceConnection();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("ForumListFragment")
|
||||
BaseFragment provideForumListFragment(ForumListFragment fragment) {
|
||||
fragment.setArguments(new Bundle());
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("ContactListFragment")
|
||||
BaseFragment provideContactListFragment(ContactListFragment fragment) {
|
||||
@@ -143,6 +153,22 @@ public class ActivityModule {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("ContactSelectorFragment")
|
||||
ContactSelectorFragment provideContactSelectorFragment(
|
||||
ContactSelectorFragment fragment) {
|
||||
fragment.setArguments(new Bundle());
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("ShareForumMessageFragment")
|
||||
ShareForumMessageFragment provideShareForumMessageFragment(
|
||||
ShareForumMessageFragment fragment) {
|
||||
fragment.setArguments(new Bundle());
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("IntroductionMessageFragment")
|
||||
IntroductionMessageFragment provideIntroductionMessageFragment(
|
||||
|
||||
@@ -14,6 +14,9 @@ import org.briarproject.api.crypto.PasswordStrengthEstimator;
|
||||
import org.briarproject.api.db.DatabaseConfig;
|
||||
import org.briarproject.api.db.DatabaseExecutor;
|
||||
import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumPostFactory;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.introduction.IntroductionManager;
|
||||
@@ -86,6 +89,12 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
||||
|
||||
TransportPropertyManager transportPropertyManager();
|
||||
|
||||
ForumManager forumManager();
|
||||
|
||||
ForumSharingManager forumSharingManager();
|
||||
|
||||
ForumPostFactory forumPostFactory();
|
||||
|
||||
SettingsManager settingsManager();
|
||||
|
||||
ContactExchangeTask contactExchangeTask();
|
||||
|
||||
@@ -13,17 +13,20 @@ import org.briarproject.R;
|
||||
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;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.ForumInvitationReceivedEvent;
|
||||
import org.briarproject.api.event.IntroductionRequestReceivedEvent;
|
||||
import org.briarproject.api.event.IntroductionResponseReceivedEvent;
|
||||
import org.briarproject.api.event.IntroductionSucceededEvent;
|
||||
import org.briarproject.api.event.MessageValidatedEvent;
|
||||
import org.briarproject.api.event.SettingsUpdatedEvent;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.lifecycle.Service;
|
||||
import org.briarproject.api.lifecycle.ServiceException;
|
||||
import org.briarproject.api.messaging.MessagingManager;
|
||||
@@ -51,6 +54,7 @@ import static android.content.Context.NOTIFICATION_SERVICE;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
|
||||
import static android.support.v4.app.NotificationCompat.CATEGORY_MESSAGE;
|
||||
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;
|
||||
@@ -60,9 +64,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
Service, EventListener {
|
||||
|
||||
private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3;
|
||||
private static final int FORUM_POST_NOTIFICATION_ID = 4;
|
||||
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 5;
|
||||
private static final String CONTACT_URI =
|
||||
"content://org.briarproject/contact";
|
||||
private static final String FORUM_URI =
|
||||
"content://org.briarproject/forum";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AndroidNotificationManagerImpl.class.getName());
|
||||
@@ -70,14 +77,16 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
private final Executor dbExecutor;
|
||||
private final SettingsManager settingsManager;
|
||||
private final MessagingManager messagingManager;
|
||||
private final ForumManager forumManager;
|
||||
private final AndroidExecutor androidExecutor;
|
||||
private final Context appContext;
|
||||
|
||||
// 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 int contactTotal = 0;
|
||||
private int contactTotal = 0, forumTotal = 0;
|
||||
private int nextRequestId = 0;
|
||||
private GroupId visibleGroup = null;
|
||||
|
||||
@@ -86,10 +95,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
@Inject
|
||||
public AndroidNotificationManagerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
SettingsManager settingsManager, MessagingManager messagingManager,
|
||||
AndroidExecutor androidExecutor, Application app) {
|
||||
ForumManager forumManager, AndroidExecutor androidExecutor,
|
||||
Application app) {
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.settingsManager = settingsManager;
|
||||
this.messagingManager = messagingManager;
|
||||
this.forumManager = forumManager;
|
||||
this.androidExecutor = androidExecutor;
|
||||
appContext = app.getApplicationContext();
|
||||
}
|
||||
@@ -110,6 +121,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
@Override
|
||||
public Void call() {
|
||||
clearPrivateMessageNotification();
|
||||
clearForumPostNotification();
|
||||
clearIntroductionSuccessNotification();
|
||||
return null;
|
||||
}
|
||||
@@ -127,6 +139,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
nm.cancel(PRIVATE_MESSAGE_NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
private void clearForumPostNotification() {
|
||||
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
nm.cancel(FORUM_POST_NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
private void clearIntroductionSuccessNotification() {
|
||||
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
@@ -144,6 +162,8 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
ClientId c = m.getClientId();
|
||||
if (c.equals(messagingManager.getClientId()))
|
||||
showPrivateMessageNotification(m.getMessage().getGroupId());
|
||||
else if (c.equals(forumManager.getClientId()))
|
||||
showForumPostNotification(m.getMessage().getGroupId());
|
||||
}
|
||||
} else if (e instanceof IntroductionRequestReceivedEvent) {
|
||||
ContactId c = ((IntroductionRequestReceivedEvent) e).getContactId();
|
||||
@@ -154,6 +174,9 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
} else if (e instanceof IntroductionSucceededEvent) {
|
||||
Contact c = ((IntroductionSucceededEvent) e).getContact();
|
||||
showIntroductionSucceededNotification(c);
|
||||
} else if (e instanceof ForumInvitationReceivedEvent) {
|
||||
ContactId c = ((ForumInvitationReceivedEvent) e).getContactId();
|
||||
showNotificationForPrivateConversation(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +282,82 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
return defaults;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showForumPostNotification(final GroupId g) {
|
||||
androidExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Integer count = forumCounts.get(g);
|
||||
if (count == null) forumCounts.put(g, 1);
|
||||
else forumCounts.put(g, count + 1);
|
||||
forumTotal++;
|
||||
if (!g.equals(visibleGroup))
|
||||
updateForumPostNotification();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearForumPostNotification(final GroupId g) {
|
||||
androidExecutor.execute(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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateForumPostNotification() {
|
||||
if (forumTotal == 0) {
|
||||
clearForumPostNotification();
|
||||
} else if (settings.getBoolean("notifyForumPosts", true)) {
|
||||
NotificationCompat.Builder b =
|
||||
new NotificationCompat.Builder(appContext);
|
||||
b.setSmallIcon(R.drawable.message_notification_icon);
|
||||
b.setContentTitle(appContext.getText(R.string.app_name));
|
||||
b.setContentText(appContext.getResources().getQuantityString(
|
||||
R.plurals.forum_post_notification_text, forumTotal,
|
||||
forumTotal));
|
||||
String ringtoneUri = settings.get("notifyRingtoneUri");
|
||||
if (!StringUtils.isNullOrEmpty(ringtoneUri))
|
||||
b.setSound(Uri.parse(ringtoneUri));
|
||||
b.setDefaults(getDefaults());
|
||||
b.setOnlyAlertOnce(true);
|
||||
b.setAutoCancel(true);
|
||||
if (forumCounts.size() == 1) {
|
||||
Intent i = new Intent(appContext, ForumActivity.class);
|
||||
GroupId g = forumCounts.keySet().iterator().next();
|
||||
i.putExtra(GROUP_ID, g.getBytes());
|
||||
String idHex = StringUtils.toHexString(g.getBytes());
|
||||
i.setData(Uri.parse(FORUM_URI + "/" + idHex));
|
||||
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
|
||||
TaskStackBuilder t = TaskStackBuilder.create(appContext);
|
||||
t.addParentStack(ForumActivity.class);
|
||||
t.addNextIntent(i);
|
||||
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
|
||||
} else {
|
||||
Intent i = new Intent(appContext, NavDrawerActivity.class);
|
||||
i.putExtra(NavDrawerActivity.INTENT_FORUMS, 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));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
b.setCategory(CATEGORY_SOCIAL);
|
||||
b.setVisibility(VISIBILITY_SECRET);
|
||||
}
|
||||
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
|
||||
NotificationManager nm = (NotificationManager) o;
|
||||
nm.notify(FORUM_POST_NOTIFICATION_ID, b.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void blockNotification(final GroupId g) {
|
||||
androidExecutor.execute(new Runnable() {
|
||||
@@ -324,4 +423,5 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.support.v7.app.AlertDialog;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.contact.ContactListFragment;
|
||||
import org.briarproject.android.forum.ForumListFragment;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,8 @@ public abstract class BriarFragmentActivity extends BriarActivity {
|
||||
|
||||
if (fragmentTag.equals(ContactListFragment.TAG)) {
|
||||
actionBar.setTitle(R.string.contacts_toolbar_header);
|
||||
} else if (fragmentTag.equals(ForumListFragment.TAG)) {
|
||||
actionBar.setTitle(R.string.forums_toolbar_header);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package org.briarproject.android;
|
||||
|
||||
import static android.view.Gravity.CENTER;
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
|
||||
import static org.briarproject.android.TestingConstants.PREVENT_SCREENSHOTS;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.util.LayoutUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
|
||||
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
|
||||
import static org.briarproject.android.TestingConstants.PREVENT_SCREENSHOTS;
|
||||
|
||||
public class ExpiredActivity extends Activity {
|
||||
|
||||
@@ -21,19 +16,6 @@ public class ExpiredActivity extends Activity {
|
||||
|
||||
if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE);
|
||||
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
layout.setLayoutParams(MATCH_MATCH);
|
||||
layout.setGravity(CENTER);
|
||||
|
||||
int pad = LayoutUtils.getPadding(this);
|
||||
|
||||
TextView warning = new TextView(this);
|
||||
warning.setGravity(CENTER);
|
||||
warning.setTextSize(18);
|
||||
warning.setPadding(pad, pad, pad, pad);
|
||||
warning.setText(R.string.expiry_warning);
|
||||
layout.addView(warning);
|
||||
|
||||
setContentView(layout);
|
||||
setContentView(R.layout.activity_expired);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
|
||||
public final static String PREF_SEEN_WELCOME_MESSAGE = "welcome_message";
|
||||
|
||||
public static final String INTENT_CONTACTS = "intent_contacts";
|
||||
public static final String INTENT_FORUMS = "intent_forums";
|
||||
|
||||
private static final String KEY_CURRENT_FRAGMENT_ID = "key_current_id";
|
||||
|
||||
@@ -69,7 +70,9 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
|
||||
exitIfStartupFailed(intent);
|
||||
checkAuthorHandle(intent);
|
||||
clearBackStack();
|
||||
if (intent.getBooleanExtra(INTENT_CONTACTS, false))
|
||||
if (intent.getBooleanExtra(INTENT_FORUMS, false))
|
||||
startFragment(activityComponent.newForumListFragment());
|
||||
else if (intent.getBooleanExtra(INTENT_CONTACTS, false))
|
||||
startFragment(activityComponent.newContactListFragment());
|
||||
}
|
||||
|
||||
@@ -168,6 +171,9 @@ public class NavDrawerActivity extends BriarFragmentActivity implements
|
||||
case R.id.nav_btn_contacts:
|
||||
startFragment(activityComponent.newContactListFragment());
|
||||
break;
|
||||
case R.id.nav_btn_forums:
|
||||
startFragment(activityComponent.newForumListFragment());
|
||||
break;
|
||||
case R.id.nav_btn_settings:
|
||||
startActivity(new Intent(this, SettingsActivity.class));
|
||||
break;
|
||||
|
||||
@@ -2,13 +2,17 @@ package org.briarproject.android.api;
|
||||
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
/** Manages notifications for private messages and introductions. */
|
||||
/** Manages notifications for private messages and forum posts. */
|
||||
public interface AndroidNotificationManager {
|
||||
|
||||
void showPrivateMessageNotification(GroupId g);
|
||||
|
||||
void clearPrivateMessageNotification(GroupId g);
|
||||
|
||||
void showForumPostNotification(GroupId g);
|
||||
|
||||
void clearForumPostNotification(GroupId g);
|
||||
|
||||
void blockNotification(GroupId g);
|
||||
|
||||
void unblockNotification(GroupId g);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.android.contact;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.util.SortedList;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
@@ -11,6 +12,7 @@ import android.widget.TextView;
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -48,6 +50,9 @@ public abstract class BaseContactListAdapter<VH extends BaseContactListAdapter.B
|
||||
if (listener != null) listener.onItemClick(ui.avatar, item);
|
||||
}
|
||||
});
|
||||
|
||||
ViewCompat.setTransitionName(ui.avatar, "avatar" +
|
||||
StringUtils.toHexString(item.getGroupId().getBytes()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.android.contact;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -10,6 +11,7 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
public class ContactListAdapter
|
||||
extends BaseContactListAdapter<ContactListAdapter.ContactHolder> {
|
||||
@@ -32,18 +34,13 @@ public class ContactListAdapter
|
||||
|
||||
ContactListItem item = getItem(position);
|
||||
|
||||
// name and unread count
|
||||
String contactName = item.getContact().getAuthor().getName();
|
||||
// unread count
|
||||
int unread = item.getUnreadCount();
|
||||
if (unread > 0) {
|
||||
// TODO show these in a bubble on top of the avatar
|
||||
ui.name.setText(contactName + " (" + unread + ")");
|
||||
|
||||
// different background for contacts with unread messages
|
||||
ui.layout.setBackgroundColor(
|
||||
ContextCompat.getColor(ctx, R.color.unread_background));
|
||||
ui.unread.setText(String.valueOf(unread));
|
||||
ui.unread.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
ui.name.setText(contactName);
|
||||
ui.unread.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
// date of last message
|
||||
@@ -62,12 +59,16 @@ public class ContactListAdapter
|
||||
} else {
|
||||
ui.bulb.setImageResource(R.drawable.contact_disconnected);
|
||||
}
|
||||
|
||||
ViewCompat.setTransitionName(ui.bulb,
|
||||
"bulb" + StringUtils.toHexString(item.getGroupId().getBytes()));
|
||||
}
|
||||
|
||||
protected static class ContactHolder
|
||||
extends BaseContactListAdapter.BaseContactHolder {
|
||||
|
||||
public final ImageView bulb;
|
||||
public final TextView unread;
|
||||
public final TextView date;
|
||||
public final TextView identity;
|
||||
|
||||
@@ -75,6 +76,7 @@ public class ContactListAdapter
|
||||
super(v);
|
||||
|
||||
bulb = (ImageView) v.findViewById(R.id.bulbView);
|
||||
unread = (TextView) v.findViewById(R.id.unreadCountView);
|
||||
date = (TextView) v.findViewById(R.id.dateView);
|
||||
identity = (TextView) v.findViewById(R.id.identityView);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.briarproject.android.contact;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.util.Pair;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -29,6 +31,8 @@ import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.MessageValidatedEvent;
|
||||
import org.briarproject.api.forum.ForumInvitationMessage;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.introduction.IntroductionManager;
|
||||
@@ -46,6 +50,7 @@ import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.BriarActivity.GROUP_ID;
|
||||
@@ -74,6 +79,8 @@ public class ContactListFragment extends BaseFragment implements EventListener {
|
||||
protected volatile MessagingManager messagingManager;
|
||||
@Inject
|
||||
protected volatile IntroductionManager introductionManager;
|
||||
@Inject
|
||||
protected volatile ForumSharingManager forumSharingManager;
|
||||
|
||||
@Inject
|
||||
public ContactListFragment() {
|
||||
@@ -103,16 +110,22 @@ public class ContactListFragment extends BaseFragment implements EventListener {
|
||||
ConversationActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 16) {
|
||||
ActivityOptionsCompat options =
|
||||
ActivityOptionsCompat.
|
||||
makeSceneTransitionAnimation(
|
||||
getActivity(),
|
||||
view, "avatar");
|
||||
getActivity().startActivity(i, options.toBundle());
|
||||
} else {
|
||||
startActivity(i);
|
||||
}
|
||||
ContactListAdapter.ContactHolder holder =
|
||||
(ContactListAdapter.ContactHolder) list
|
||||
.getRecyclerView()
|
||||
.findViewHolderForAdapterPosition(
|
||||
adapter.findItemPosition(item));
|
||||
Pair<View, String> avatar =
|
||||
Pair.create((View) holder.avatar, ViewCompat
|
||||
.getTransitionName(holder.avatar));
|
||||
Pair<View, String> bulb =
|
||||
Pair.create((View) holder.bulb, ViewCompat
|
||||
.getTransitionName(holder.bulb));
|
||||
ActivityOptionsCompat options =
|
||||
makeSceneTransitionAnimation(getActivity(),
|
||||
avatar, bulb);
|
||||
ActivityCompat.startActivity(getActivity(), i,
|
||||
options.toBundle());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -221,7 +234,8 @@ public class ContactListFragment extends BaseFragment implements EventListener {
|
||||
MessageValidatedEvent m = (MessageValidatedEvent) e;
|
||||
ClientId c = m.getClientId();
|
||||
if (m.isValid() && (c.equals(messagingManager.getClientId()) ||
|
||||
c.equals(introductionManager.getClientId()))) {
|
||||
c.equals(introductionManager.getClientId()) ||
|
||||
c.equals(forumSharingManager.getClientId()))) {
|
||||
LOG.info("Message added, reloading");
|
||||
reloadConversation(m.getMessage().getGroupId());
|
||||
}
|
||||
@@ -315,6 +329,16 @@ public class ContactListFragment extends BaseFragment implements EventListener {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading introduction messages took " + duration + " ms");
|
||||
|
||||
now = System.currentTimeMillis();
|
||||
Collection<ForumInvitationMessage> invitations =
|
||||
forumSharingManager.getForumInvitationMessages(id);
|
||||
for (ForumInvitationMessage i : invitations) {
|
||||
messages.add(ConversationItem.from(i));
|
||||
}
|
||||
duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading forum invitations took " + duration + " ms");
|
||||
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ContactListItem {
|
||||
}
|
||||
|
||||
void setMessages(Collection<ConversationItem> messages) {
|
||||
empty = messages.isEmpty();
|
||||
empty = messages == null || messages.isEmpty();
|
||||
timestamp = 0;
|
||||
unread = 0;
|
||||
if (!empty) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Bundle;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
@@ -43,11 +44,14 @@ import org.briarproject.api.event.ContactRemovedEvent;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.ForumInvitationReceivedEvent;
|
||||
import org.briarproject.api.event.IntroductionRequestReceivedEvent;
|
||||
import org.briarproject.api.event.IntroductionResponseReceivedEvent;
|
||||
import org.briarproject.api.event.MessageValidatedEvent;
|
||||
import org.briarproject.api.event.MessagesAckedEvent;
|
||||
import org.briarproject.api.event.MessagesSentEvent;
|
||||
import org.briarproject.api.forum.ForumInvitationMessage;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.introduction.IntroductionManager;
|
||||
import org.briarproject.api.introduction.IntroductionMessage;
|
||||
import org.briarproject.api.introduction.IntroductionRequest;
|
||||
@@ -119,6 +123,8 @@ public class ConversationActivity extends BriarActivity
|
||||
protected volatile PrivateMessageFactory privateMessageFactory;
|
||||
@Inject
|
||||
protected volatile IntroductionManager introductionManager;
|
||||
@Inject
|
||||
protected volatile ForumSharingManager forumSharingManager;
|
||||
|
||||
private volatile GroupId groupId = null;
|
||||
private volatile ContactId contactId = null;
|
||||
@@ -151,6 +157,10 @@ public class ConversationActivity extends BriarActivity
|
||||
ab.setDisplayShowTitleEnabled(false);
|
||||
}
|
||||
|
||||
String hexGroupId = StringUtils.toHexString(b);
|
||||
ViewCompat.setTransitionName(toolbarAvatar, "avatar" + hexGroupId);
|
||||
ViewCompat.setTransitionName(toolbarStatus, "bulb" + hexGroupId);
|
||||
|
||||
adapter = new ConversationAdapter(this, this);
|
||||
list = (BriarRecyclerView) findViewById(R.id.conversationView);
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
@@ -202,7 +212,7 @@ public class ConversationActivity extends BriarActivity
|
||||
// Handle presses on the action bar items
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
supportFinishAfterTransition();
|
||||
onBackPressed();
|
||||
return true;
|
||||
case R.id.action_introduction:
|
||||
if (contactId == null) return false;
|
||||
@@ -222,6 +232,13 @@ public class ConversationActivity extends BriarActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// FIXME disabled exit transition, because it doesn't work for some reason
|
||||
//supportFinishAfterTransition();
|
||||
finish();
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
@@ -292,10 +309,13 @@ public class ConversationActivity extends BriarActivity
|
||||
Collection<IntroductionMessage> introductions =
|
||||
introductionManager
|
||||
.getIntroductionMessages(contactId);
|
||||
Collection<ForumInvitationMessage> invitations =
|
||||
forumSharingManager
|
||||
.getForumInvitationMessages(contactId);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading headers took " + duration + " ms");
|
||||
displayMessages(headers, introductions);
|
||||
displayMessages(headers, introductions, invitations);
|
||||
} catch (NoSuchContactException e) {
|
||||
finishOnUiThread();
|
||||
} catch (DbException e) {
|
||||
@@ -307,12 +327,14 @@ public class ConversationActivity extends BriarActivity
|
||||
}
|
||||
|
||||
private void displayMessages(final Collection<PrivateMessageHeader> headers,
|
||||
final Collection<IntroductionMessage> introductions) {
|
||||
final Collection<IntroductionMessage> introductions,
|
||||
final Collection<ForumInvitationMessage> invitations) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
sendButton.setEnabled(true);
|
||||
if (headers.isEmpty() && introductions.isEmpty()) {
|
||||
if (headers.isEmpty() && introductions.isEmpty() &&
|
||||
invitations.isEmpty()) {
|
||||
// we have no messages,
|
||||
// so let the list know to hide progress bar
|
||||
list.showData();
|
||||
@@ -340,6 +362,10 @@ public class ConversationActivity extends BriarActivity
|
||||
}
|
||||
items.add(item);
|
||||
}
|
||||
for (ForumInvitationMessage i : invitations) {
|
||||
ConversationItem item = ConversationItem.from(i);
|
||||
items.add(item);
|
||||
}
|
||||
adapter.addAll(items);
|
||||
// Scroll to the bottom
|
||||
list.scrollToPosition(adapter.getItemCount() - 1);
|
||||
@@ -493,6 +519,12 @@ public class ConversationActivity extends BriarActivity
|
||||
ConversationItem.from(this, contactName, ir);
|
||||
addIntroduction(item);
|
||||
}
|
||||
} else if (e instanceof ForumInvitationReceivedEvent) {
|
||||
ForumInvitationReceivedEvent event =
|
||||
(ForumInvitationReceivedEvent) e;
|
||||
if (event.getContactId().equals(contactId)) {
|
||||
loadMessages();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.android.contact;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v7.util.SortedList;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.format.DateUtils;
|
||||
@@ -13,7 +14,9 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.forum.AvailableForumsActivity;
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
import org.briarproject.api.forum.ForumInvitationMessage;
|
||||
import org.briarproject.api.introduction.IntroductionRequest;
|
||||
import org.briarproject.api.messaging.PrivateMessageHeader;
|
||||
import org.briarproject.util.StringUtils;
|
||||
@@ -22,6 +25,8 @@ import java.util.List;
|
||||
|
||||
import static android.support.v7.util.SortedList.INVALID_POSITION;
|
||||
import static android.support.v7.widget.RecyclerView.ViewHolder;
|
||||
import static org.briarproject.android.contact.ConversationItem.FORUM_INVITATION_IN;
|
||||
import static org.briarproject.android.contact.ConversationItem.FORUM_INVITATION_OUT;
|
||||
import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_IN;
|
||||
import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_OUT;
|
||||
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
|
||||
@@ -82,6 +87,14 @@ class ConversationAdapter extends RecyclerView.Adapter {
|
||||
v = LayoutInflater.from(viewGroup.getContext()).inflate(
|
||||
R.layout.list_item_notice_out, viewGroup, false);
|
||||
return new NoticeHolder(v, type);
|
||||
} else if (type == FORUM_INVITATION_IN) {
|
||||
v = LayoutInflater.from(viewGroup.getContext()).inflate(
|
||||
R.layout.list_item_forum_invitation_in, viewGroup, false);
|
||||
return new InvitationHolder(v, type);
|
||||
} else if (type == FORUM_INVITATION_OUT) {
|
||||
v = LayoutInflater.from(viewGroup.getContext()).inflate(
|
||||
R.layout.list_item_forum_invitation_out, viewGroup, false);
|
||||
return new InvitationHolder(v, type);
|
||||
}
|
||||
// incoming message (non-local)
|
||||
else {
|
||||
@@ -106,6 +119,12 @@ class ConversationAdapter extends RecyclerView.Adapter {
|
||||
bindNotice((NoticeHolder) ui, (ConversationNoticeOutItem) item);
|
||||
} else if (item instanceof ConversationNoticeInItem) {
|
||||
bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item);
|
||||
} else if (item instanceof ConversationForumInvitationOutItem) {
|
||||
bindInvitation((InvitationHolder) ui,
|
||||
(ConversationForumInvitationOutItem) item);
|
||||
} else if (item instanceof ConversationForumInvitationInItem) {
|
||||
bindInvitation((InvitationHolder) ui,
|
||||
(ConversationForumInvitationInItem) item);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unhandled Conversation Item");
|
||||
}
|
||||
@@ -257,6 +276,65 @@ class ConversationAdapter extends RecyclerView.Adapter {
|
||||
}
|
||||
}
|
||||
|
||||
private void bindInvitation(InvitationHolder ui,
|
||||
final ConversationForumInvitationItem item) {
|
||||
|
||||
ForumInvitationMessage fim = item.getForumInvitationMessage();
|
||||
|
||||
String message = fim.getMessage();
|
||||
if (StringUtils.isNullOrEmpty(message)) {
|
||||
ui.messageLayout.setVisibility(View.GONE);
|
||||
} else {
|
||||
ui.messageLayout.setVisibility(View.VISIBLE);
|
||||
ui.message.body.setText(message);
|
||||
ui.message.date.setText(
|
||||
DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
|
||||
}
|
||||
|
||||
// Outgoing Invitation
|
||||
if (item instanceof ConversationForumInvitationOutItem) {
|
||||
ui.text.setText(ctx.getString(R.string.forum_invitation_sent,
|
||||
fim.getForumName(), contactName));
|
||||
ConversationForumInvitationOutItem i =
|
||||
(ConversationForumInvitationOutItem) item;
|
||||
if (i.isSeen()) {
|
||||
ui.status.setImageResource(R.drawable.message_delivered);
|
||||
ui.message.status.setImageResource(
|
||||
R.drawable.message_delivered_white);
|
||||
} else if (i.isSent()) {
|
||||
ui.status.setImageResource(R.drawable.message_sent);
|
||||
ui.message.status.setImageResource(
|
||||
R.drawable.message_sent_white);
|
||||
} else {
|
||||
ui.status.setImageResource(R.drawable.message_stored);
|
||||
ui.message.status.setImageResource(
|
||||
R.drawable.message_stored_white);
|
||||
}
|
||||
}
|
||||
// Incoming Invitation
|
||||
else {
|
||||
ui.text.setText(ctx.getString(R.string.forum_invitation_received,
|
||||
contactName, fim.getForumName()));
|
||||
|
||||
if (fim.isAvailable()) {
|
||||
ui.showForumsButton.setVisibility(View.VISIBLE);
|
||||
ui.showForumsButton
|
||||
.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(ctx,
|
||||
AvailableForumsActivity.class);
|
||||
ctx.startActivity(intent);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.showForumsButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
ui.date.setText(
|
||||
DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
@@ -395,6 +473,33 @@ class ConversationAdapter extends RecyclerView.Adapter {
|
||||
}
|
||||
}
|
||||
|
||||
private static class InvitationHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final View messageLayout;
|
||||
private final MessageHolder message;
|
||||
private final TextView text;
|
||||
private final Button showForumsButton;
|
||||
private final TextView date;
|
||||
private final ImageView status;
|
||||
|
||||
public InvitationHolder(View v, int type) {
|
||||
super(v);
|
||||
|
||||
messageLayout = v.findViewById(R.id.messageLayout);
|
||||
message = new MessageHolder(messageLayout,
|
||||
type == FORUM_INVITATION_IN ? MSG_IN : MSG_OUT);
|
||||
text = (TextView) v.findViewById(R.id.introductionText);
|
||||
showForumsButton = (Button) v.findViewById(R.id.showForumsButton);
|
||||
date = (TextView) v.findViewById(R.id.introductionTime);
|
||||
|
||||
if (type == FORUM_INVITATION_OUT) {
|
||||
status = (ImageView) v.findViewById(R.id.introductionStatus);
|
||||
} else {
|
||||
status = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ListCallbacks extends SortedList.Callback<ConversationItem> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.briarproject.android.contact;
|
||||
|
||||
import org.briarproject.api.forum.ForumInvitationMessage;
|
||||
|
||||
// This class is not thread-safe
|
||||
public class ConversationForumInvitationInItem
|
||||
extends ConversationForumInvitationItem
|
||||
implements ConversationItem.IncomingItem {
|
||||
|
||||
private boolean read;
|
||||
|
||||
public ConversationForumInvitationInItem(ForumInvitationMessage fim) {
|
||||
super(fim);
|
||||
|
||||
this.read = fim.isRead();
|
||||
}
|
||||
|
||||
@Override
|
||||
int getType() {
|
||||
return FORUM_INVITATION_IN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRead() {
|
||||
return read;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRead(boolean read) {
|
||||
this.read = read;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.briarproject.android.contact;
|
||||
|
||||
import org.briarproject.api.forum.ForumInvitationMessage;
|
||||
|
||||
abstract class ConversationForumInvitationItem extends ConversationItem {
|
||||
|
||||
private final ForumInvitationMessage fim;
|
||||
|
||||
public ConversationForumInvitationItem(ForumInvitationMessage fim) {
|
||||
super(fim.getId(), fim.getTimestamp());
|
||||
|
||||
this.fim = fim;
|
||||
}
|
||||
|
||||
public ForumInvitationMessage getForumInvitationMessage() {
|
||||
return fim;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.briarproject.android.contact;
|
||||
|
||||
import org.briarproject.api.forum.ForumInvitationMessage;
|
||||
|
||||
/**
|
||||
* This class is needed and can not be replaced by an ConversationNoticeOutItem,
|
||||
* because it carries the optional invitation message
|
||||
* to be displayed as a regular private message.
|
||||
* <p/>
|
||||
* This class is not thread-safe
|
||||
*/
|
||||
public class ConversationForumInvitationOutItem
|
||||
extends ConversationForumInvitationItem
|
||||
implements ConversationItem.OutgoingItem {
|
||||
|
||||
private boolean sent, seen;
|
||||
|
||||
public ConversationForumInvitationOutItem(ForumInvitationMessage fim) {
|
||||
super(fim);
|
||||
this.sent = fim.isSent();
|
||||
this.seen = fim.isSeen();
|
||||
}
|
||||
|
||||
@Override
|
||||
int getType() {
|
||||
return FORUM_INVITATION_OUT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSent() {
|
||||
return sent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSent(boolean sent) {
|
||||
this.sent = sent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSeen() {
|
||||
return seen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSeen(boolean seen) {
|
||||
this.seen = seen;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.briarproject.android.contact;
|
||||
import android.content.Context;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.api.forum.ForumInvitationMessage;
|
||||
import org.briarproject.api.introduction.IntroductionMessage;
|
||||
import org.briarproject.api.introduction.IntroductionRequest;
|
||||
import org.briarproject.api.introduction.IntroductionResponse;
|
||||
@@ -20,6 +21,8 @@ public abstract class ConversationItem {
|
||||
final static int INTRODUCTION_OUT = 4;
|
||||
final static int NOTICE_IN = 5;
|
||||
final static int NOTICE_OUT = 6;
|
||||
final static int FORUM_INVITATION_IN = 7;
|
||||
final static int FORUM_INVITATION_OUT = 8;
|
||||
|
||||
private MessageId id;
|
||||
private long time;
|
||||
@@ -92,6 +95,14 @@ public abstract class ConversationItem {
|
||||
}
|
||||
}
|
||||
|
||||
public static ConversationItem from(ForumInvitationMessage fim) {
|
||||
if (fim.isLocal()) {
|
||||
return new ConversationForumInvitationOutItem(fim);
|
||||
} else {
|
||||
return new ConversationForumInvitationInItem(fim);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should not be used to get user-facing objects,
|
||||
* Its purpose is to provider data for the contact list.
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.util.BriarRecyclerView;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.NoSuchGroupException;
|
||||
import org.briarproject.api.event.ContactRemovedEvent;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.ForumInvitationReceivedEvent;
|
||||
import org.briarproject.api.event.GroupAddedEvent;
|
||||
import org.briarproject.api.event.GroupRemovedEvent;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.forum.AvailableForumsAdapter.AvailableForumClickListener;
|
||||
|
||||
public class AvailableForumsActivity extends BriarActivity
|
||||
implements EventListener, AvailableForumClickListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AvailableForumsActivity.class.getName());
|
||||
|
||||
private AvailableForumsAdapter adapter;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
protected volatile ForumManager forumManager;
|
||||
@Inject
|
||||
protected volatile ForumSharingManager forumSharingManager;
|
||||
@Inject
|
||||
protected volatile EventBus eventBus;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
setContentView(R.layout.activity_available_forums);
|
||||
|
||||
adapter = new AvailableForumsAdapter(this, this);
|
||||
BriarRecyclerView list =
|
||||
(BriarRecyclerView) findViewById(R.id.availableForumsView);
|
||||
list.setLayoutManager(new LinearLayoutManager(this));
|
||||
list.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
eventBus.addListener(this);
|
||||
loadForums();
|
||||
}
|
||||
|
||||
private void loadForums() {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Collection<ForumContacts> available = new ArrayList<>();
|
||||
long now = System.currentTimeMillis();
|
||||
for (Forum f : forumSharingManager.getAvailableForums()) {
|
||||
try {
|
||||
Collection<Contact> c =
|
||||
forumSharingManager.getSharedBy(f.getId());
|
||||
available.add(new ForumContacts(f, c));
|
||||
} catch (NoSuchGroupException e) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Load took " + duration + " ms");
|
||||
displayForums(available);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayForums(final Collection<ForumContacts> available) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (available.isEmpty()) {
|
||||
LOG.info("No forums available, finishing");
|
||||
finish();
|
||||
} else {
|
||||
adapter.clear();
|
||||
List<AvailableForumsItem> list =
|
||||
new ArrayList<>(available.size());
|
||||
for (ForumContacts f : available)
|
||||
list.add(new AvailableForumsItem(f));
|
||||
adapter.addAll(list);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof ContactRemovedEvent) {
|
||||
LOG.info("Contact removed, reloading");
|
||||
loadForums();
|
||||
} else if (e instanceof GroupAddedEvent) {
|
||||
GroupAddedEvent g = (GroupAddedEvent) e;
|
||||
if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
|
||||
LOG.info("Forum added, reloading");
|
||||
loadForums();
|
||||
}
|
||||
} else if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
||||
if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
|
||||
LOG.info("Forum removed, reloading");
|
||||
loadForums();
|
||||
}
|
||||
} else if (e instanceof ForumInvitationReceivedEvent) {
|
||||
LOG.info("Available forums updated, reloading");
|
||||
loadForums();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AvailableForumsItem item, boolean accept) {
|
||||
respondToInvitation(item, accept);
|
||||
|
||||
// show toast
|
||||
int res = R.string.forum_declined_toast;
|
||||
if (accept) res = R.string.forum_joined_toast;
|
||||
Toast.makeText(this, res, LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void respondToInvitation(final AvailableForumsItem item,
|
||||
final boolean accept) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Forum f = item.getForum();
|
||||
for (Contact c : item.getContacts()) {
|
||||
forumSharingManager.respondToInvitation(f, c, accept);
|
||||
}
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
loadForums();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.util.SortedList;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.util.TextAvatarView;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
class AvailableForumsAdapter extends
|
||||
RecyclerView.Adapter<AvailableForumsAdapter.AvailableForumViewHolder> {
|
||||
|
||||
private final Context ctx;
|
||||
private final AvailableForumClickListener listener;
|
||||
private final SortedList<AvailableForumsItem> forums =
|
||||
new SortedList<>(AvailableForumsItem.class,
|
||||
new SortedListCallBacks());
|
||||
|
||||
AvailableForumsAdapter(Context ctx, AvailableForumClickListener listener) {
|
||||
this.ctx = ctx;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AvailableForumViewHolder onCreateViewHolder(ViewGroup parent,
|
||||
int viewType) {
|
||||
|
||||
View v = LayoutInflater.from(ctx)
|
||||
.inflate(R.layout.list_item_available_forum, parent, false);
|
||||
return new AvailableForumViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(AvailableForumViewHolder ui, int position) {
|
||||
final AvailableForumsItem item = getItem(position);
|
||||
|
||||
ui.avatar.setText(item.getForum().getName().substring(0, 1));
|
||||
ui.avatar.setBackgroundBytes(item.getForum().getId().getBytes());
|
||||
|
||||
ui.name.setText(item.getForum().getName());
|
||||
|
||||
Collection<String> names = new ArrayList<>();
|
||||
for (Contact c : item.getContacts()) names.add(c.getAuthor().getName());
|
||||
ui.sharedBy.setText(ctx.getString(R.string.shared_by_format,
|
||||
StringUtils.join(names, ", ")));
|
||||
|
||||
ui.accept.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onItemClick(item, true);
|
||||
}
|
||||
});
|
||||
ui.decline.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onItemClick(item, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return forums.size();
|
||||
}
|
||||
|
||||
public AvailableForumsItem getItem(int position) {
|
||||
return forums.get(position);
|
||||
}
|
||||
|
||||
public void add(AvailableForumsItem item) {
|
||||
forums.add(item);
|
||||
}
|
||||
|
||||
public void addAll(Collection<AvailableForumsItem> list) {
|
||||
forums.addAll(list);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
forums.clear();
|
||||
}
|
||||
|
||||
protected static class AvailableForumViewHolder
|
||||
extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextAvatarView avatar;
|
||||
private final TextView name;
|
||||
private final TextView sharedBy;
|
||||
private final Button accept;
|
||||
private final Button decline;
|
||||
|
||||
public AvailableForumViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
|
||||
name = (TextView) v.findViewById(R.id.forumNameView);
|
||||
sharedBy = (TextView) v.findViewById(R.id.sharedByView);
|
||||
accept = (Button) v.findViewById(R.id.acceptButton);
|
||||
decline = (Button) v.findViewById(R.id.declineButton);
|
||||
}
|
||||
}
|
||||
|
||||
private class SortedListCallBacks
|
||||
extends SortedList.Callback<AvailableForumsItem> {
|
||||
|
||||
@Override
|
||||
public int compare(AvailableForumsItem o1,
|
||||
AvailableForumsItem o2) {
|
||||
return String.CASE_INSENSITIVE_ORDER
|
||||
.compare(o1.getForum().getName(),
|
||||
o2.getForum().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInserted(int position, int count) {
|
||||
notifyItemRangeInserted(position, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(int position, int count) {
|
||||
notifyItemRangeRemoved(position, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoved(int fromPosition, int toPosition) {
|
||||
notifyItemMoved(fromPosition, toPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(int position, int count) {
|
||||
notifyItemRangeChanged(position, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(AvailableForumsItem oldItem,
|
||||
AvailableForumsItem newItem) {
|
||||
return oldItem.getForum().equals(newItem.getForum()) &&
|
||||
oldItem.getContacts().equals(newItem.getContacts());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(AvailableForumsItem oldItem,
|
||||
AvailableForumsItem newItem) {
|
||||
return oldItem.getForum().equals(newItem.getForum());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface AvailableForumClickListener {
|
||||
void onItemClick(AvailableForumsItem item, boolean accept);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
class AvailableForumsItem {
|
||||
|
||||
private final ForumContacts forumContacts;
|
||||
|
||||
AvailableForumsItem(ForumContacts forumContacts) {
|
||||
this.forumContacts = forumContacts;
|
||||
}
|
||||
|
||||
Forum getForum() {
|
||||
return forumContacts.getForum();
|
||||
}
|
||||
|
||||
Collection<Contact> getContacts() {
|
||||
return forumContacts.getContacts();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.os.Build;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.contact.BaseContactListAdapter;
|
||||
import org.briarproject.android.contact.ContactListItem;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class ContactSelectorAdapter
|
||||
extends BaseContactListAdapter<ContactSelectorAdapter.SelectableContactHolder> {
|
||||
|
||||
public ContactSelectorAdapter(Context context,
|
||||
OnItemClickListener listener) {
|
||||
|
||||
super(context, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SelectableContactHolder onCreateViewHolder(ViewGroup viewGroup,
|
||||
int i) {
|
||||
View v = LayoutInflater.from(ctx).inflate(
|
||||
R.layout.list_item_selectable_contact, viewGroup, false);
|
||||
|
||||
return new SelectableContactHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SelectableContactHolder ui, int position) {
|
||||
super.onBindViewHolder(ui, position);
|
||||
|
||||
SelectableContactListItem item =
|
||||
(SelectableContactListItem) getItem(position);
|
||||
|
||||
if (item.isSelected()) {
|
||||
ui.checkBox.setChecked(true);
|
||||
} else {
|
||||
ui.checkBox.setChecked(false);
|
||||
}
|
||||
|
||||
if (item.isDisabled()) {
|
||||
// we share this forum already with that contact
|
||||
ui.layout.setEnabled(false);
|
||||
grayOutItem(ui);
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<ContactId> getSelectedContactIds() {
|
||||
Collection<ContactId> selected = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < contacts.size(); i++) {
|
||||
SelectableContactListItem item =
|
||||
(SelectableContactListItem) contacts.get(i);
|
||||
if (item.isSelected()) selected.add(item.getContact().getId());
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
protected static class SelectableContactHolder
|
||||
extends BaseContactListAdapter.BaseContactHolder {
|
||||
|
||||
private final CheckBox checkBox;
|
||||
|
||||
public SelectableContactHolder(View v) {
|
||||
super(v);
|
||||
|
||||
checkBox = (CheckBox) v.findViewById(R.id.checkBox);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareContactListItems(ContactListItem c1, ContactListItem c2) {
|
||||
return compareByName(c1, c2);
|
||||
}
|
||||
|
||||
private void grayOutItem(final SelectableContactHolder ui) {
|
||||
if (Build.VERSION.SDK_INT >= 11) {
|
||||
float alpha = 0.25f;
|
||||
ui.avatar.setAlpha(alpha);
|
||||
ui.name.setAlpha(alpha);
|
||||
ui.checkBox.setAlpha(alpha);
|
||||
} else {
|
||||
ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY,
|
||||
PorterDuff.Mode.MULTIPLY);
|
||||
ui.avatar.setColorFilter(colorFilter);
|
||||
ui.name.setEnabled(false);
|
||||
ui.checkBox.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.transition.Fade;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.contact.BaseContactListAdapter;
|
||||
import org.briarproject.android.contact.ContactListItem;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.android.util.BriarRecyclerView;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.forum.ShareForumActivity.CONTACTS;
|
||||
import static org.briarproject.android.forum.ShareForumActivity.getContactsFromIds;
|
||||
import static org.briarproject.api.forum.ForumConstants.GROUP_ID;
|
||||
|
||||
public class ContactSelectorFragment extends BaseFragment implements
|
||||
BaseContactListAdapter.OnItemClickListener {
|
||||
|
||||
public final static String TAG = "ContactSelectorFragment";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ContactSelectorFragment.class.getName());
|
||||
|
||||
private ShareForumActivity shareForumActivity;
|
||||
private Menu menu;
|
||||
private BriarRecyclerView list;
|
||||
private ContactSelectorAdapter adapter;
|
||||
private Collection<ContactId> selectedContacts;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
protected volatile ContactManager contactManager;
|
||||
@Inject
|
||||
protected volatile IdentityManager identityManager;
|
||||
@Inject
|
||||
protected volatile ForumSharingManager forumSharingManager;
|
||||
|
||||
protected volatile GroupId groupId;
|
||||
|
||||
public void initBundle(GroupId groupId) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putByteArray(GROUP_ID, groupId.getBytes());
|
||||
setArguments(bundle);
|
||||
}
|
||||
|
||||
@Inject
|
||||
public ContactSelectorFragment() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
shareForumActivity = (ShareForumActivity) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new InstantiationError(
|
||||
"This fragment is only meant to be attached to the ShareForumActivity");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
groupId = new GroupId(getArguments().getByteArray(GROUP_ID));
|
||||
if (groupId == null) throw new IllegalStateException("No GroupId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
View contentView = inflater.inflate(
|
||||
R.layout.introduction_contact_chooser, container, false);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
setExitTransition(new Fade());
|
||||
}
|
||||
|
||||
adapter = new ContactSelectorAdapter(getActivity(), this);
|
||||
|
||||
list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
list.setAdapter(adapter);
|
||||
list.setEmptyText(getString(R.string.no_contacts));
|
||||
|
||||
// restore selected contacts if available
|
||||
if (savedInstanceState != null) {
|
||||
ArrayList<Integer> intContacts =
|
||||
savedInstanceState.getIntegerArrayList(CONTACTS);
|
||||
selectedContacts = ShareForumActivity.getContactsFromIntegers(
|
||||
intContacts);
|
||||
}
|
||||
|
||||
return contentView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (selectedContacts != null)
|
||||
loadContacts(Collections.unmodifiableCollection(selectedContacts));
|
||||
else loadContacts(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
if (adapter != null) {
|
||||
selectedContacts = adapter.getSelectedContactIds();
|
||||
outState.putIntegerArrayList(CONTACTS,
|
||||
getContactsFromIds(selectedContacts));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.forum_share_actions, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
this.menu = menu;
|
||||
// hide sharing action initially, if no contact is selected
|
||||
updateMenuItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
// Handle presses on the action bar items
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
shareForumActivity.onBackPressed();
|
||||
return true;
|
||||
case R.id.action_share_forum:
|
||||
selectedContacts = adapter.getSelectedContactIds();
|
||||
shareForumActivity.showMessageScreen(groupId, selectedContacts);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(View view, ContactListItem item) {
|
||||
((SelectableContactListItem) item).toggleSelected();
|
||||
adapter.notifyItemChanged(adapter.findItemPosition(item), item);
|
||||
|
||||
updateMenuItem();
|
||||
}
|
||||
|
||||
private void loadContacts(final Collection<ContactId> selection) {
|
||||
shareForumActivity.runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
List<ContactListItem> contacts = new ArrayList<>();
|
||||
|
||||
for (Contact c : contactManager.getActiveContacts()) {
|
||||
LocalAuthor localAuthor = identityManager
|
||||
.getLocalAuthor(c.getLocalAuthorId());
|
||||
// was this contact already selected?
|
||||
boolean selected = selection != null &&
|
||||
selection.contains(c.getId());
|
||||
// do we have already some sharing with that contact?
|
||||
boolean disabled =
|
||||
!forumSharingManager.canBeShared(groupId, c);
|
||||
contacts.add(new SelectableContactListItem(c,
|
||||
localAuthor, groupId, selected, disabled));
|
||||
}
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Load took " + duration + " ms");
|
||||
displayContacts(Collections.unmodifiableList(contacts));
|
||||
} catch (DbException e) {
|
||||
displayContacts(Collections.<ContactListItem>emptyList());
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayContacts(final List<ContactListItem> contacts) {
|
||||
shareForumActivity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!contacts.isEmpty()) adapter.addAll(contacts);
|
||||
else list.showData();
|
||||
updateMenuItem();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMenuItem() {
|
||||
if (menu == null) return;
|
||||
MenuItem item = menu.findItem(R.id.action_share_forum);
|
||||
if (item == null) return;
|
||||
|
||||
selectedContacts = adapter.getSelectedContactIds();
|
||||
if (selectedContacts.size() > 0) {
|
||||
item.setVisible(true);
|
||||
} else {
|
||||
item.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
|
||||
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
|
||||
|
||||
public class CreateForumActivity extends BriarActivity
|
||||
implements OnEditorActionListener, OnClickListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(CreateForumActivity.class.getName());
|
||||
|
||||
private EditText nameEntry;
|
||||
private Button createForumButton;
|
||||
private ProgressBar progress;
|
||||
private TextView feedback;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
protected volatile ForumManager forumManager;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
setContentView(R.layout.activity_create_forum);
|
||||
|
||||
nameEntry = (EditText) findViewById(R.id.createForumNameEntry);
|
||||
TextWatcher nameEntryWatcher = new TextWatcher() {
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count,
|
||||
int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence text, int start,
|
||||
int lengthBefore, int lengthAfter) {
|
||||
enableOrDisableCreateButton();
|
||||
}
|
||||
};
|
||||
nameEntry.setOnEditorActionListener(this);
|
||||
nameEntry.addTextChangedListener(nameEntryWatcher);
|
||||
|
||||
feedback = (TextView) findViewById(R.id.createForumFeedback);
|
||||
|
||||
createForumButton = (Button) findViewById(R.id.createForumButton);
|
||||
createForumButton.setOnClickListener(this);
|
||||
|
||||
progress = (ProgressBar) findViewById(R.id.createForumProgressBar);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
private void enableOrDisableCreateButton() {
|
||||
if (progress == null) return; // Not created yet
|
||||
createForumButton.setEnabled(validateName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
|
||||
hideSoftKeyboard(textView);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean validateName() {
|
||||
String name = nameEntry.getText().toString();
|
||||
int length = StringUtils.toUtf8(name).length;
|
||||
if (length > MAX_FORUM_NAME_LENGTH) {
|
||||
feedback.setText(R.string.name_too_long);
|
||||
return false;
|
||||
}
|
||||
feedback.setText("");
|
||||
return length > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (view == createForumButton) {
|
||||
hideSoftKeyboard(view);
|
||||
if (!validateName()) return;
|
||||
createForumButton.setVisibility(GONE);
|
||||
progress.setVisibility(VISIBLE);
|
||||
storeForum(nameEntry.getText().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void storeForum(final String name) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
Forum f = forumManager.addForum(name);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Storing forum took " + duration + " ms");
|
||||
displayForum(f);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
finishOnUiThread();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayForum(final Forum f) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Intent i = new Intent(CreateForumActivity.this,
|
||||
ForumActivity.class);
|
||||
i.putExtra(GROUP_ID, f.getId().getBytes());
|
||||
i.putExtra(FORUM_NAME, f.getName());
|
||||
startActivity(i);
|
||||
Toast.makeText(CreateForumActivity.this,
|
||||
R.string.forum_created_toast, LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.AndroidComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.api.AndroidNotificationManager;
|
||||
import org.briarproject.android.util.ListLoadingProgressBar;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.NoSuchGroupException;
|
||||
import org.briarproject.api.db.NoSuchMessageException;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.GroupRemovedEvent;
|
||||
import org.briarproject.api.event.MessageValidatedEvent;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
import org.briarproject.api.identity.Author;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
|
||||
import static android.support.design.widget.Snackbar.LENGTH_LONG;
|
||||
import static android.view.Gravity.CENTER;
|
||||
import static android.view.Gravity.CENTER_HORIZONTAL;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.widget.LinearLayout.VERTICAL;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.forum.ReadForumPostActivity.RESULT_PREV_NEXT;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
|
||||
|
||||
public class ForumActivity extends BriarActivity implements EventListener,
|
||||
OnItemClickListener {
|
||||
|
||||
public static final String FORUM_NAME = "briar.FORUM_NAME";
|
||||
public static final String MIN_TIMESTAMP = "briar.MIN_TIMESTAMP";
|
||||
|
||||
private static final int REQUEST_READ = 2;
|
||||
private static final int REQUEST_FORUM_SHARED = 3;
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ForumActivity.class.getName());
|
||||
|
||||
@Inject protected AndroidNotificationManager notificationManager;
|
||||
private Map<MessageId, byte[]> bodyCache = new HashMap<>();
|
||||
private TextView empty = null;
|
||||
private ForumAdapter adapter = null;
|
||||
private ListView list = null;
|
||||
private ListLoadingProgressBar loading = null;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject protected volatile ForumManager forumManager;
|
||||
@Inject protected volatile EventBus eventBus;
|
||||
private volatile GroupId groupId = null;
|
||||
private volatile Forum forum = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
Intent i = getIntent();
|
||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException();
|
||||
groupId = new GroupId(b);
|
||||
String forumName = i.getStringExtra(FORUM_NAME);
|
||||
if (forumName != null) setTitle(forumName);
|
||||
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
layout.setLayoutParams(MATCH_MATCH);
|
||||
layout.setOrientation(VERTICAL);
|
||||
layout.setGravity(CENTER_HORIZONTAL);
|
||||
|
||||
empty = new TextView(this);
|
||||
empty.setLayoutParams(MATCH_WRAP_1);
|
||||
empty.setGravity(CENTER);
|
||||
empty.setTextSize(18);
|
||||
empty.setText(R.string.no_forum_posts);
|
||||
empty.setVisibility(GONE);
|
||||
layout.addView(empty);
|
||||
|
||||
adapter = new ForumAdapter(this);
|
||||
list = new ListView(this);
|
||||
list.setLayoutParams(MATCH_WRAP_1);
|
||||
list.setAdapter(adapter);
|
||||
list.setOnItemClickListener(this);
|
||||
list.setVisibility(GONE);
|
||||
layout.addView(list);
|
||||
|
||||
// Show a progress bar while the list is loading
|
||||
loading = new ListLoadingProgressBar(this);
|
||||
layout.addView(loading);
|
||||
|
||||
setContentView(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
eventBus.addListener(this);
|
||||
notificationManager.blockNotification(groupId);
|
||||
notificationManager.clearForumPostNotification(groupId);
|
||||
loadForum();
|
||||
loadHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu items for use in the action bar
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.forum_actions, menu);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
ActivityOptionsCompat options = ActivityOptionsCompat
|
||||
.makeCustomAnimation(this, android.R.anim.slide_in_left,
|
||||
android.R.anim.slide_out_right);
|
||||
// Handle presses on the action bar items
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_forum_compose_post:
|
||||
Intent i = new Intent(this, WriteForumPostActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
i.putExtra(FORUM_NAME, forum.getName());
|
||||
i.putExtra(MIN_TIMESTAMP, getMinTimestampForNewPost());
|
||||
startActivity(i);
|
||||
return true;
|
||||
case R.id.action_forum_share:
|
||||
Intent i2 = new Intent(this, ShareForumActivity.class);
|
||||
i2.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
|
||||
i2.putExtra(GROUP_ID, groupId.getBytes());
|
||||
ActivityCompat
|
||||
.startActivityForResult(this, i2, REQUEST_FORUM_SHARED,
|
||||
options.toBundle());
|
||||
return true;
|
||||
case R.id.action_forum_sharing_status:
|
||||
Intent i3 = new Intent(this, ForumSharingStatusActivity.class);
|
||||
i3.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
|
||||
i3.putExtra(GROUP_ID, groupId.getBytes());
|
||||
ActivityCompat.startActivity(this, i3, options.toBundle());
|
||||
return true;
|
||||
case R.id.action_forum_delete:
|
||||
showUnsubscribeDialog();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadForum() {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
forum = forumManager.getForum(groupId);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading forum " + duration + " ms");
|
||||
displayForumName();
|
||||
} catch (NoSuchGroupException e) {
|
||||
finishOnUiThread();
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayForumName() {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
setTitle(forum.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadHeaders() {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
Collection<ForumPostHeader> headers =
|
||||
forumManager.getPostHeaders(groupId);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Load took " + duration + " ms");
|
||||
displayHeaders(headers);
|
||||
} catch (NoSuchGroupException e) {
|
||||
finishOnUiThread();
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayHeaders(final Collection<ForumPostHeader> headers) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
loading.setVisibility(GONE);
|
||||
adapter.clear();
|
||||
if (headers.isEmpty()) {
|
||||
empty.setVisibility(VISIBLE);
|
||||
list.setVisibility(GONE);
|
||||
} else {
|
||||
empty.setVisibility(GONE);
|
||||
list.setVisibility(VISIBLE);
|
||||
for (ForumPostHeader h : headers) {
|
||||
ForumItem item = new ForumItem(h);
|
||||
byte[] body = bodyCache.get(h.getId());
|
||||
if (body == null) loadPostBody(h);
|
||||
else item.setBody(body);
|
||||
adapter.add(item);
|
||||
}
|
||||
adapter.sort(ForumItemComparator.INSTANCE);
|
||||
// Scroll to the bottom
|
||||
list.setSelection(adapter.getCount() - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadPostBody(final ForumPostHeader h) {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
byte[] body = forumManager.getPostBody(h.getId());
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading message took " + duration + " ms");
|
||||
displayPost(h.getId(), body);
|
||||
} catch (NoSuchMessageException e) {
|
||||
// The item will be removed when we get the event
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayPost(final MessageId m, final byte[] body) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
bodyCache.put(m, body);
|
||||
int count = adapter.getCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
ForumItem item = adapter.getItem(i);
|
||||
if (item.getHeader().getId().equals(m)) {
|
||||
item.setBody(body);
|
||||
adapter.notifyDataSetChanged();
|
||||
// Scroll to the bottom
|
||||
list.setSelection(count - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int request, int result, Intent data) {
|
||||
super.onActivityResult(request, result, data);
|
||||
if (request == REQUEST_READ && result == RESULT_PREV_NEXT) {
|
||||
int position = data.getIntExtra("briar.POSITION", -1);
|
||||
if (position >= 0 && position < adapter.getCount())
|
||||
displayPost(position);
|
||||
}
|
||||
else if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) {
|
||||
Snackbar s = Snackbar.make(list, R.string.forum_shared_snackbar,
|
||||
LENGTH_LONG);
|
||||
s.getView().setBackgroundResource(R.color.briar_primary);
|
||||
s.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
eventBus.removeListener(this);
|
||||
notificationManager.unblockNotification(groupId);
|
||||
if (isFinishing()) markPostsRead();
|
||||
}
|
||||
|
||||
private void markPostsRead() {
|
||||
List<MessageId> unread = new ArrayList<>();
|
||||
int count = adapter.getCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
ForumPostHeader h = adapter.getItem(i).getHeader();
|
||||
if (!h.isRead()) unread.add(h.getId());
|
||||
}
|
||||
if (unread.isEmpty()) return;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Marking " + unread.size() + " posts read");
|
||||
markPostsRead(Collections.unmodifiableList(unread));
|
||||
}
|
||||
|
||||
private void markPostsRead(final Collection<MessageId> unread) {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
for (MessageId m : unread)
|
||||
forumManager.setReadFlag(m, true);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Marking read took " + duration + " ms");
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof MessageValidatedEvent) {
|
||||
MessageValidatedEvent m = (MessageValidatedEvent) e;
|
||||
if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) {
|
||||
LOG.info("Message added, reloading");
|
||||
loadHeaders();
|
||||
}
|
||||
} else if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent s = (GroupRemovedEvent) e;
|
||||
if (s.getGroup().getId().equals(groupId)) {
|
||||
LOG.info("Forum removed");
|
||||
finishOnUiThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long getMinTimestampForNewPost() {
|
||||
// Don't use an earlier timestamp than the newest post
|
||||
long timestamp = 0;
|
||||
int count = adapter.getCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
long t = adapter.getItem(i).getHeader().getTimestamp();
|
||||
if (t > timestamp) timestamp = t;
|
||||
}
|
||||
return timestamp + 1;
|
||||
}
|
||||
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
displayPost(position);
|
||||
}
|
||||
|
||||
private void displayPost(int position) {
|
||||
ForumPostHeader header = adapter.getItem(position).getHeader();
|
||||
Intent i = new Intent(this, ReadForumPostActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
i.putExtra(FORUM_NAME, forum.getName());
|
||||
i.putExtra("briar.MESSAGE_ID", header.getId().getBytes());
|
||||
Author author = header.getAuthor();
|
||||
if (author != null) {
|
||||
i.putExtra("briar.AUTHOR_NAME", author.getName());
|
||||
i.putExtra("briar.AUTHOR_ID", author.getId().getBytes());
|
||||
}
|
||||
i.putExtra("briar.AUTHOR_STATUS", header.getAuthorStatus().name());
|
||||
i.putExtra("briar.CONTENT_TYPE", header.getContentType());
|
||||
i.putExtra("briar.TIMESTAMP", header.getTimestamp());
|
||||
i.putExtra(MIN_TIMESTAMP, getMinTimestampForNewPost());
|
||||
i.putExtra("briar.POSITION", position);
|
||||
startActivityForResult(i, REQUEST_READ);
|
||||
}
|
||||
|
||||
private void showUnsubscribeDialog() {
|
||||
DialogInterface.OnClickListener okListener =
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
unsubscribe(forum);
|
||||
Toast.makeText(ForumActivity.this,
|
||||
R.string.forum_left_toast, LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
};
|
||||
AlertDialog.Builder builder =
|
||||
new AlertDialog.Builder(ForumActivity.this,
|
||||
R.style.BriarDialogTheme);
|
||||
builder.setTitle(getString(R.string.dialog_title_leave_forum));
|
||||
builder.setMessage(getString(R.string.dialog_message_leave_forum));
|
||||
builder.setPositiveButton(R.string.dialog_button_leave, okListener);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void unsubscribe(final Forum f) {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
forumManager.removeForum(f);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Removing forum took " + duration + " ms");
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.util.AuthorView;
|
||||
import org.briarproject.android.util.LayoutUtils;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static android.view.Gravity.CENTER_HORIZONTAL;
|
||||
import static android.view.Gravity.CENTER_VERTICAL;
|
||||
import static android.widget.LinearLayout.HORIZONTAL;
|
||||
import static android.widget.LinearLayout.VERTICAL;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
|
||||
|
||||
class ForumAdapter extends ArrayAdapter<ForumItem> {
|
||||
|
||||
private final int pad;
|
||||
|
||||
ForumAdapter(Context ctx) {
|
||||
super(ctx, android.R.layout.simple_expandable_list_item_1,
|
||||
new ArrayList<ForumItem>());
|
||||
pad = LayoutUtils.getPadding(ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
ForumItem item = getItem(position);
|
||||
ForumPostHeader header = item.getHeader();
|
||||
Context ctx = getContext();
|
||||
Resources res = ctx.getResources();
|
||||
|
||||
LinearLayout layout = new LinearLayout(ctx);
|
||||
layout.setOrientation(VERTICAL);
|
||||
layout.setGravity(CENTER_HORIZONTAL);
|
||||
if (!header.isRead())
|
||||
layout.setBackgroundColor(res.getColor(R.color.unread_background));
|
||||
|
||||
LinearLayout headerLayout = new LinearLayout(ctx);
|
||||
headerLayout.setOrientation(HORIZONTAL);
|
||||
headerLayout.setGravity(CENTER_VERTICAL);
|
||||
|
||||
AuthorView authorView = new AuthorView(ctx);
|
||||
authorView.setLayoutParams(WRAP_WRAP_1);
|
||||
authorView.setPadding(0, pad, pad, pad);
|
||||
Author author = header.getAuthor();
|
||||
if (author == null) {
|
||||
authorView.init(null, null, header.getAuthorStatus());
|
||||
} else {
|
||||
authorView.init(author.getName(), author.getId(),
|
||||
header.getAuthorStatus());
|
||||
}
|
||||
headerLayout.addView(authorView);
|
||||
|
||||
TextView date = new TextView(ctx);
|
||||
date.setPadding(pad, pad, pad, pad);
|
||||
long timestamp = header.getTimestamp();
|
||||
date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
|
||||
headerLayout.addView(date);
|
||||
layout.addView(headerLayout);
|
||||
|
||||
if (item.getBody() == null) {
|
||||
TextView ellipsis = new TextView(ctx);
|
||||
ellipsis.setPadding(pad, 0, pad, pad);
|
||||
ellipsis.setText("\u2026");
|
||||
layout.addView(ellipsis);
|
||||
} else if (header.getContentType().equals("text/plain")) {
|
||||
TextView text = new TextView(ctx);
|
||||
text.setPadding(pad, 0, pad, pad);
|
||||
text.setText(StringUtils.fromUtf8(item.getBody()));
|
||||
layout.addView(text);
|
||||
} else {
|
||||
ImageButton attachment = new ImageButton(ctx);
|
||||
attachment.setPadding(pad, 0, pad, pad);
|
||||
attachment.setImageResource(R.drawable.content_attachment);
|
||||
layout.addView(attachment);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
class ForumContacts {
|
||||
|
||||
private final Forum forum;
|
||||
private final Collection<Contact> contacts;
|
||||
|
||||
ForumContacts(Forum forum, Collection<Contact> contacts) {
|
||||
this.forum = forum;
|
||||
this.contacts = contacts;
|
||||
}
|
||||
|
||||
Forum getForum() {
|
||||
return forum;
|
||||
}
|
||||
|
||||
Collection<Contact> getContacts() {
|
||||
return contacts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
|
||||
// This class is not thread-safe
|
||||
class ForumItem {
|
||||
|
||||
private final ForumPostHeader header;
|
||||
private byte[] body;
|
||||
|
||||
ForumItem(ForumPostHeader header) {
|
||||
this.header = header;
|
||||
body = null;
|
||||
}
|
||||
|
||||
ForumPostHeader getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
byte[] getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
void setBody(byte[] body) {
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
class ForumItemComparator implements Comparator<ForumItem> {
|
||||
|
||||
static final ForumItemComparator INSTANCE = new ForumItemComparator();
|
||||
|
||||
public int compare(ForumItem a, ForumItem b) {
|
||||
// The oldest message comes first
|
||||
long aTime = a.getHeader().getTimestamp();
|
||||
long bTime = b.getHeader().getTimestamp();
|
||||
if (aTime < bTime) return -1;
|
||||
if (aTime > bTime) return 1;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.util.SortedList;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.util.TextAvatarView;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.briarproject.android.BriarActivity.GROUP_ID;
|
||||
import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
|
||||
|
||||
public class ForumListAdapter extends
|
||||
RecyclerView.Adapter<ForumListAdapter.ForumViewHolder> {
|
||||
|
||||
private SortedList<ForumListItem> forums = new SortedList<>(
|
||||
ForumListItem.class, new SortedList.Callback<ForumListItem>() {
|
||||
|
||||
@Override
|
||||
public int compare(ForumListItem a, ForumListItem b) {
|
||||
if (a == b) return 0;
|
||||
// The forum with the newest message comes first
|
||||
long aTime = a.getTimestamp(), bTime = b.getTimestamp();
|
||||
if (aTime > bTime) return -1;
|
||||
if (aTime < bTime) return 1;
|
||||
// Break ties by forum name
|
||||
String aName = a.getForum().getName();
|
||||
String bName = b.getForum().getName();
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(aName, bName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInserted(int position, int count) {
|
||||
notifyItemRangeInserted(position, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(int position, int count) {
|
||||
notifyItemRangeRemoved(position, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMoved(int fromPosition, int toPosition) {
|
||||
notifyItemMoved(fromPosition, toPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(int position, int count) {
|
||||
notifyItemRangeChanged(position, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(ForumListItem a, ForumListItem b) {
|
||||
return a.getForum().equals(b.getForum()) &&
|
||||
a.getTimestamp() == b.getTimestamp() &&
|
||||
a.getUnreadCount() == b.getUnreadCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(ForumListItem a, ForumListItem b) {
|
||||
return a.getForum().equals(b.getForum());
|
||||
}
|
||||
});
|
||||
|
||||
private final Context ctx;
|
||||
|
||||
public ForumListAdapter(Context ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(ctx).inflate(
|
||||
R.layout.list_item_forum, parent, false);
|
||||
return new ForumViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ForumViewHolder ui, int position) {
|
||||
final ForumListItem item = getItem(position);
|
||||
|
||||
// Avatar
|
||||
ui.avatar.setText(item.getForum().getName().substring(0, 1));
|
||||
ui.avatar.setBackgroundBytes(item.getForum().getId().getBytes());
|
||||
|
||||
// Forum Name
|
||||
ui.name.setText(item.getForum().getName());
|
||||
|
||||
// Unread Count
|
||||
int unread = item.getUnreadCount();
|
||||
if (unread > 0) {
|
||||
ui.unread.setText(ctx.getResources()
|
||||
.getQuantityString(R.plurals.unread_posts, unread, unread));
|
||||
ui.unread.setTextColor(
|
||||
ContextCompat.getColor(ctx, R.color.briar_button_positive));
|
||||
} else {
|
||||
ui.unread.setText(ctx.getString(R.string.no_unread_posts));
|
||||
ui.unread.setTextColor(
|
||||
ContextCompat.getColor(ctx, R.color.briar_text_secondary));
|
||||
}
|
||||
|
||||
// Date or "No Posts"
|
||||
if (item.isEmpty()) {
|
||||
ui.date.setVisibility(View.GONE);
|
||||
} else {
|
||||
long timestamp = item.getTimestamp();
|
||||
ui.date.setText(
|
||||
DateUtils.getRelativeTimeSpanString(ctx, timestamp));
|
||||
ui.date.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// Open Forum on Click
|
||||
ui.layout.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent i = new Intent(ctx, ForumActivity.class);
|
||||
Forum f = item.getForum();
|
||||
i.putExtra(GROUP_ID, f.getId().getBytes());
|
||||
i.putExtra(FORUM_NAME, f.getName());
|
||||
ctx.startActivity(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return forums.size();
|
||||
}
|
||||
|
||||
public ForumListItem getItem(int position) {
|
||||
return forums.get(position);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ForumListItem getItem(GroupId g) {
|
||||
for (int i = 0; i < forums.size(); i++) {
|
||||
ForumListItem item = forums.get(i);
|
||||
if (item.getForum().getGroup().getId().equals(g)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addAll(Collection<ForumListItem> items) {
|
||||
forums.addAll(items);
|
||||
}
|
||||
|
||||
public void updateItem(ForumListItem item) {
|
||||
ForumListItem oldItem = getItem(item.getForum().getGroup().getId());
|
||||
int position = forums.indexOf(oldItem);
|
||||
forums.updateItemAt(position, item);
|
||||
}
|
||||
|
||||
public void remove(ForumListItem item) {
|
||||
forums.remove(item);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
forums.clear();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return forums.size() == 0;
|
||||
}
|
||||
|
||||
protected static class ForumViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ViewGroup layout;
|
||||
private final TextAvatarView avatar;
|
||||
private final TextView name;
|
||||
private final TextView unread;
|
||||
private final TextView date;
|
||||
|
||||
public ForumViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
layout = (ViewGroup) v;
|
||||
avatar = (TextAvatarView) v.findViewById(R.id.avatarView);
|
||||
name = (TextView) v.findViewById(R.id.forumNameView);
|
||||
unread = (TextView) v.findViewById(R.id.unreadView);
|
||||
date = (TextView) v.findViewById(R.id.dateView);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.AndroidComponent;
|
||||
import org.briarproject.android.fragment.BaseEventFragment;
|
||||
import org.briarproject.android.util.BriarRecyclerView;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.NoSuchGroupException;
|
||||
import org.briarproject.api.event.ContactRemovedEvent;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.ForumInvitationReceivedEvent;
|
||||
import org.briarproject.api.event.GroupAddedEvent;
|
||||
import org.briarproject.api.event.GroupRemovedEvent;
|
||||
import org.briarproject.api.event.MessageValidatedEvent;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.sync.ClientId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.support.design.widget.Snackbar.LENGTH_INDEFINITE;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
public class ForumListFragment extends BaseEventFragment implements
|
||||
View.OnClickListener {
|
||||
|
||||
public final static String TAG = "ForumListFragment";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ForumListFragment.class.getName());
|
||||
|
||||
|
||||
private BriarRecyclerView list;
|
||||
private ForumListAdapter adapter;
|
||||
private Snackbar snackbar;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject protected volatile ForumManager forumManager;
|
||||
@Inject protected volatile ForumSharingManager forumSharingManager;
|
||||
|
||||
@Inject
|
||||
public ForumListFragment() {
|
||||
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
View contentView =
|
||||
inflater.inflate(R.layout.fragment_forum_list, container,
|
||||
false);
|
||||
|
||||
adapter = new ForumListAdapter(getActivity());
|
||||
|
||||
list = (BriarRecyclerView) contentView.findViewById(R.id.forumList);
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
list.setAdapter(adapter);
|
||||
list.setEmptyText(getString(R.string.no_forums));
|
||||
|
||||
snackbar = Snackbar.make(list, "", LENGTH_INDEFINITE);
|
||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
||||
snackbar.setAction(R.string.show_forums, this);
|
||||
snackbar.setActionTextColor(ContextCompat
|
||||
.getColor(getContext(), R.color.briar_button_positive));
|
||||
|
||||
return contentView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
loadForumHeaders();
|
||||
loadAvailableForums();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
|
||||
adapter.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.forum_list_actions, menu);
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
// Handle presses on the action bar items
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_create_forum:
|
||||
Intent intent =
|
||||
new Intent(getContext(), CreateForumActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadForumHeaders() {
|
||||
listener.runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// load forums
|
||||
long now = System.currentTimeMillis();
|
||||
Collection<ForumListItem> forums = new ArrayList<>();
|
||||
for (Forum f : forumManager.getForums()) {
|
||||
try {
|
||||
Collection<ForumPostHeader> headers =
|
||||
forumManager.getPostHeaders(f.getId());
|
||||
forums.add(new ForumListItem(f, headers));
|
||||
} catch (NoSuchGroupException e) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
displayForumHeaders(forums);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Full load took " + duration + " ms");
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayForumHeaders(final Collection<ForumListItem> forums) {
|
||||
listener.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (forums.size() > 0) adapter.addAll(forums);
|
||||
else list.showData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAvailableForums() {
|
||||
listener.runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
int available =
|
||||
forumSharingManager.getAvailableForums().size();
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading available took " + duration + " ms");
|
||||
displayAvailableForums(available);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayAvailableForums(final int availableCount) {
|
||||
listener.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (availableCount == 0) {
|
||||
snackbar.dismiss();
|
||||
} else {
|
||||
snackbar.show();
|
||||
snackbar.setText(getResources().getQuantityString(
|
||||
R.plurals.forums_shared, availableCount,
|
||||
availableCount));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof ContactRemovedEvent) {
|
||||
LOG.info("Contact removed, reloading available forums");
|
||||
loadAvailableForums();
|
||||
} else if (e instanceof GroupAddedEvent) {
|
||||
GroupAddedEvent g = (GroupAddedEvent) e;
|
||||
if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
|
||||
LOG.info("Forum added, reloading forums");
|
||||
loadForumHeaders();
|
||||
}
|
||||
} else if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent g = (GroupRemovedEvent) e;
|
||||
if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
|
||||
LOG.info("Forum removed, removing from list");
|
||||
removeForum(g.getGroup().getId());
|
||||
}
|
||||
} else if (e instanceof MessageValidatedEvent) {
|
||||
MessageValidatedEvent m = (MessageValidatedEvent) e;
|
||||
if (m.isValid()) {
|
||||
ClientId c = m.getClientId();
|
||||
if (c.equals(forumManager.getClientId())) {
|
||||
LOG.info("Forum post added, reloading");
|
||||
loadForumHeaders(m.getMessage().getGroupId());
|
||||
}
|
||||
}
|
||||
} else if (e instanceof ForumInvitationReceivedEvent) {
|
||||
loadAvailableForums();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadForumHeaders(final GroupId g) {
|
||||
listener.runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
Forum f = forumManager.getForum(g);
|
||||
Collection<ForumPostHeader> headers =
|
||||
forumManager.getPostHeaders(g);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Partial load took " + duration + " ms");
|
||||
updateForum(new ForumListItem(f, headers));
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateForum(final ForumListItem item) {
|
||||
listener.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapter.updateItem(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void removeForum(final GroupId g) {
|
||||
listener.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ForumListItem item = adapter.getItem(g);
|
||||
if (item != null) adapter.remove(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
// snackbar click
|
||||
startActivity(new Intent(getContext(), AvailableForumsActivity.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
class ForumListItem {
|
||||
|
||||
private final Forum forum;
|
||||
private final boolean empty;
|
||||
private final long timestamp;
|
||||
private final int unread;
|
||||
|
||||
ForumListItem(Forum forum, Collection<ForumPostHeader> headers) {
|
||||
this.forum = forum;
|
||||
empty = headers.isEmpty();
|
||||
if (empty) {
|
||||
timestamp = 0;
|
||||
unread = 0;
|
||||
} else {
|
||||
ForumPostHeader newest = null;
|
||||
long timestamp = -1;
|
||||
int unread = 0;
|
||||
for (ForumPostHeader h : headers) {
|
||||
if (h.getTimestamp() > timestamp) {
|
||||
timestamp = h.getTimestamp();
|
||||
newest = h;
|
||||
}
|
||||
if (!h.isRead()) unread++;
|
||||
}
|
||||
this.timestamp = newest.getTimestamp();
|
||||
this.unread = unread;
|
||||
}
|
||||
}
|
||||
|
||||
Forum getForum() {
|
||||
return forum;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return empty;
|
||||
}
|
||||
|
||||
long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
int getUnreadCount() {
|
||||
return unread;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.contact.ContactListItem;
|
||||
import org.briarproject.android.util.BriarRecyclerView;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
public class ForumSharingStatusActivity extends BriarActivity {
|
||||
|
||||
private GroupId groupId;
|
||||
private BriarRecyclerView sharedByList, sharedWithList;
|
||||
private ForumSharingStatusAdapter sharedByAdapter, sharedWithAdapter;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
protected volatile ForumSharingManager forumSharingManager;
|
||||
@Inject
|
||||
protected volatile IdentityManager identityManager;
|
||||
|
||||
public final static String TAG = "ForumSharingStatusActivity";
|
||||
private static final Logger LOG = Logger.getLogger(TAG);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_forum_sharing_status);
|
||||
|
||||
Intent i = getIntent();
|
||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException("No GroupId");
|
||||
groupId = new GroupId(b);
|
||||
|
||||
sharedByList = (BriarRecyclerView) findViewById(R.id.sharedByView);
|
||||
sharedByAdapter = new ForumSharingStatusAdapter(this);
|
||||
sharedByList.setLayoutManager(new LinearLayoutManager(this));
|
||||
sharedByList.setAdapter(sharedByAdapter);
|
||||
sharedByList.setEmptyText(getString(R.string.nobody));
|
||||
|
||||
sharedWithList = (BriarRecyclerView) findViewById(R.id.sharedWithView);
|
||||
sharedWithAdapter = new ForumSharingStatusAdapter(this);
|
||||
sharedWithList.setLayoutManager(new LinearLayoutManager(this));
|
||||
sharedWithList.setAdapter(sharedWithAdapter);
|
||||
sharedWithList.setEmptyText(getString(R.string.nobody));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
loadSharedBy();
|
||||
loadSharedWith();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
// Handle presses on the action bar items
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
private void loadSharedBy() {
|
||||
dbController.runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
List<ContactListItem> contactItems = new ArrayList<>();
|
||||
try {
|
||||
Collection<Contact> contacts =
|
||||
forumSharingManager.getSharedBy(groupId);
|
||||
for (Contact c : contacts) {
|
||||
LocalAuthor localAuthor = identityManager
|
||||
.getLocalAuthor(c.getLocalAuthorId());
|
||||
ContactListItem item =
|
||||
new ContactListItem(c, localAuthor, false, null,
|
||||
null);
|
||||
contactItems.add(item);
|
||||
}
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
displaySharedBy(contactItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displaySharedBy(final List<ContactListItem> contacts) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (contacts.isEmpty()) {
|
||||
sharedByList.showData();
|
||||
} else {
|
||||
sharedByAdapter.addAll(contacts);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadSharedWith() {
|
||||
dbController.runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
List<ContactListItem> contactItems = new ArrayList<>();
|
||||
try {
|
||||
Collection<Contact> contacts =
|
||||
forumSharingManager.getSharedWith(groupId);
|
||||
for (Contact c : contacts) {
|
||||
LocalAuthor localAuthor = identityManager
|
||||
.getLocalAuthor(c.getLocalAuthorId());
|
||||
ContactListItem item =
|
||||
new ContactListItem(c, localAuthor, false, null,
|
||||
null);
|
||||
contactItems.add(item);
|
||||
}
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
displaySharedWith(contactItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displaySharedWith(final List<ContactListItem> contacts) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (contacts.isEmpty()) {
|
||||
sharedWithList.showData();
|
||||
} else {
|
||||
sharedWithAdapter.addAll(contacts);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.contact.BaseContactListAdapter;
|
||||
import org.briarproject.android.contact.ContactListItem;
|
||||
|
||||
public class ForumSharingStatusAdapter
|
||||
extends BaseContactListAdapter<BaseContactListAdapter.BaseContactHolder> {
|
||||
|
||||
public ForumSharingStatusAdapter(Context context) {
|
||||
super(context, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseContactHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
|
||||
View v = LayoutInflater.from(viewGroup.getContext()).inflate(
|
||||
R.layout.list_item_contact_small, viewGroup, false);
|
||||
|
||||
return new BaseContactHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareContactListItems(ContactListItem c1, ContactListItem c2) {
|
||||
return compareByName(c1, c2);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.AndroidComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.util.AuthorView;
|
||||
import org.briarproject.android.util.ElasticHorizontalSpace;
|
||||
import org.briarproject.android.util.HorizontalBorder;
|
||||
import org.briarproject.android.util.LayoutUtils;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.NoSuchMessageException;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.view.Gravity.CENTER;
|
||||
import static android.view.Gravity.CENTER_VERTICAL;
|
||||
import static android.widget.LinearLayout.HORIZONTAL;
|
||||
import static android.widget.LinearLayout.VERTICAL;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
|
||||
import static org.briarproject.android.forum.ForumActivity.MIN_TIMESTAMP;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP_1;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP_1;
|
||||
|
||||
public class ReadForumPostActivity extends BriarActivity
|
||||
implements OnClickListener {
|
||||
|
||||
static final int RESULT_REPLY = RESULT_FIRST_USER;
|
||||
static final int RESULT_PREV_NEXT = RESULT_FIRST_USER + 1;
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ReadForumPostActivity.class.getName());
|
||||
|
||||
private GroupId groupId = null;
|
||||
private String forumName = null;
|
||||
private long minTimestamp = -1;
|
||||
private ImageButton prevButton = null, nextButton = null;
|
||||
private ImageButton replyButton = null;
|
||||
private TextView content = null;
|
||||
private int position = -1;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject protected volatile ForumManager forumManager;
|
||||
private volatile MessageId messageId = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
Intent i = getIntent();
|
||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException();
|
||||
groupId = new GroupId(b);
|
||||
forumName = i.getStringExtra(FORUM_NAME);
|
||||
if (forumName == null) throw new IllegalStateException();
|
||||
setTitle(forumName);
|
||||
b = i.getByteArrayExtra("briar.MESSAGE_ID");
|
||||
if (b == null) throw new IllegalStateException();
|
||||
messageId = new MessageId(b);
|
||||
String contentType = i.getStringExtra("briar.CONTENT_TYPE");
|
||||
if (contentType == null) throw new IllegalStateException();
|
||||
long timestamp = i.getLongExtra("briar.TIMESTAMP", -1);
|
||||
if (timestamp == -1) throw new IllegalStateException();
|
||||
minTimestamp = i.getLongExtra(MIN_TIMESTAMP, -1);
|
||||
if (minTimestamp == -1) throw new IllegalStateException();
|
||||
position = i.getIntExtra("briar.POSITION", -1);
|
||||
if (position == -1) throw new IllegalStateException();
|
||||
String authorName = i.getStringExtra("briar.AUTHOR_NAME");
|
||||
AuthorId authorId = null;
|
||||
b = i.getByteArrayExtra("briar.AUTHOR_ID");
|
||||
if (b != null) authorId = new AuthorId(b);
|
||||
String s = i.getStringExtra("briar.AUTHOR_STATUS");
|
||||
if (s == null) throw new IllegalStateException();
|
||||
Author.Status authorStatus = Author.Status.valueOf(s);
|
||||
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
layout.setLayoutParams(MATCH_MATCH);
|
||||
layout.setOrientation(VERTICAL);
|
||||
|
||||
ScrollView scrollView = new ScrollView(this);
|
||||
scrollView.setLayoutParams(MATCH_WRAP_1);
|
||||
|
||||
LinearLayout message = new LinearLayout(this);
|
||||
message.setOrientation(VERTICAL);
|
||||
|
||||
LinearLayout header = new LinearLayout(this);
|
||||
header.setLayoutParams(MATCH_WRAP);
|
||||
header.setOrientation(HORIZONTAL);
|
||||
header.setGravity(CENTER_VERTICAL);
|
||||
|
||||
int pad = LayoutUtils.getPadding(this);
|
||||
|
||||
AuthorView authorView = new AuthorView(this);
|
||||
authorView.setPadding(0, pad, pad, pad);
|
||||
authorView.setLayoutParams(WRAP_WRAP_1);
|
||||
authorView.init(authorName, authorId, authorStatus);
|
||||
header.addView(authorView);
|
||||
|
||||
TextView date = new TextView(this);
|
||||
date.setPadding(pad, pad, pad, pad);
|
||||
date.setText(DateUtils.getRelativeTimeSpanString(this, timestamp));
|
||||
header.addView(date);
|
||||
message.addView(header);
|
||||
|
||||
if (contentType.equals("text/plain")) {
|
||||
// Load and display the message body
|
||||
content = new TextView(this);
|
||||
content.setPadding(pad, 0, pad, pad);
|
||||
message.addView(content);
|
||||
loadPostBody();
|
||||
}
|
||||
scrollView.addView(message);
|
||||
layout.addView(scrollView);
|
||||
|
||||
layout.addView(new HorizontalBorder(this));
|
||||
|
||||
LinearLayout footer = new LinearLayout(this);
|
||||
footer.setLayoutParams(MATCH_WRAP);
|
||||
footer.setOrientation(HORIZONTAL);
|
||||
footer.setGravity(CENTER);
|
||||
Resources res = getResources();
|
||||
footer.setBackgroundColor(res.getColor(R.color.button_bar_background));
|
||||
|
||||
prevButton = new ImageButton(this);
|
||||
prevButton.setBackgroundResource(0);
|
||||
prevButton.setImageResource(R.drawable.navigation_previous_item);
|
||||
prevButton.setOnClickListener(this);
|
||||
footer.addView(prevButton);
|
||||
footer.addView(new ElasticHorizontalSpace(this));
|
||||
|
||||
nextButton = new ImageButton(this);
|
||||
nextButton.setBackgroundResource(0);
|
||||
nextButton.setImageResource(R.drawable.navigation_next_item);
|
||||
nextButton.setOnClickListener(this);
|
||||
footer.addView(nextButton);
|
||||
footer.addView(new ElasticHorizontalSpace(this));
|
||||
|
||||
replyButton = new ImageButton(this);
|
||||
replyButton.setBackgroundResource(0);
|
||||
replyButton.setImageResource(R.drawable.social_reply_all);
|
||||
replyButton.setOnClickListener(this);
|
||||
footer.addView(replyButton);
|
||||
layout.addView(footer);
|
||||
|
||||
setContentView(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (isFinishing()) markPostRead();
|
||||
}
|
||||
|
||||
private void markPostRead() {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
forumManager.setReadFlag(messageId, true);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Marking read took " + duration + " ms");
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadPostBody() {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
byte[] body = forumManager.getPostBody(messageId);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading post took " + duration + " ms");
|
||||
displayPostBody(StringUtils.fromUtf8(body));
|
||||
} catch (NoSuchMessageException e) {
|
||||
finishOnUiThread();
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayPostBody(final String body) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
content.setText(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void onClick(View view) {
|
||||
if (view == prevButton) {
|
||||
Intent i = new Intent();
|
||||
i.putExtra("briar.POSITION", position - 1);
|
||||
setResult(RESULT_PREV_NEXT, i);
|
||||
finish();
|
||||
} else if (view == nextButton) {
|
||||
Intent i = new Intent();
|
||||
i.putExtra("briar.POSITION", position + 1);
|
||||
setResult(RESULT_PREV_NEXT, i);
|
||||
finish();
|
||||
} else if (view == replyButton) {
|
||||
Intent i = new Intent(this, WriteForumPostActivity.class);
|
||||
i.putExtra(GROUP_ID, groupId.getBytes());
|
||||
i.putExtra(FORUM_NAME, forumName);
|
||||
i.putExtra("briar.PARENT_ID", messageId.getBytes());
|
||||
i.putExtra(MIN_TIMESTAMP, minTimestamp);
|
||||
startActivity(i);
|
||||
setResult(RESULT_REPLY);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.android.contact.ContactListItem;
|
||||
import org.briarproject.android.contact.ConversationItem;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
// This class is not thread-safe
|
||||
public class SelectableContactListItem extends ContactListItem {
|
||||
|
||||
private boolean selected, disabled;
|
||||
|
||||
public SelectableContactListItem(Contact contact, LocalAuthor localAuthor,
|
||||
GroupId groupId, boolean selected, boolean disabled) {
|
||||
|
||||
super(contact, localAuthor, false, groupId,
|
||||
Collections.<ConversationItem>emptyList());
|
||||
|
||||
this.selected = selected;
|
||||
this.disabled = disabled;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void toggleSelected() {
|
||||
selected = !selected;
|
||||
}
|
||||
|
||||
public boolean isDisabled() {
|
||||
return disabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class ShareForumActivity extends BriarActivity implements
|
||||
BaseFragment.BaseFragmentListener {
|
||||
|
||||
public final static String CONTACTS = "contacts";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_share_forum);
|
||||
|
||||
Intent i = getIntent();
|
||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException("No GroupId");
|
||||
GroupId groupId = new GroupId(b);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
ContactSelectorFragment contactSelectorFragment =
|
||||
activityComponent.newContactSelectorFragment();
|
||||
contactSelectorFragment.initBundle(groupId);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.shareForumContainer, contactSelectorFragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
public void showMessageScreen(GroupId groupId,
|
||||
Collection<ContactId> contacts) {
|
||||
|
||||
ShareForumMessageFragment messageFragment =
|
||||
activityComponent.newShareForumMessageFragment();
|
||||
messageFragment.initBundle(groupId, contacts);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in,
|
||||
android.R.anim.fade_out,
|
||||
android.R.anim.slide_in_left,
|
||||
android.R.anim.slide_out_right)
|
||||
.replace(R.id.shareForumContainer, messageFragment,
|
||||
ContactSelectorFragment.TAG)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public static ArrayList<Integer> getContactsFromIds(
|
||||
Collection<ContactId> contacts) {
|
||||
|
||||
// transform ContactIds to Integers so they can be added to a bundle
|
||||
ArrayList<Integer> intContacts = new ArrayList<>(contacts.size());
|
||||
for (ContactId contactId : contacts) {
|
||||
intContacts.add(contactId.getInt());
|
||||
}
|
||||
return intContacts;
|
||||
}
|
||||
|
||||
public void sharingSuccessful(View v) {
|
||||
setResult(RESULT_OK);
|
||||
hideSoftKeyboard(v);
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
protected static Collection<ContactId> getContactsFromIntegers(
|
||||
ArrayList<Integer> intContacts) {
|
||||
|
||||
// turn contact integers from a bundle back to ContactIds
|
||||
List<ContactId> contacts = new ArrayList<>(intContacts.size());
|
||||
for(Integer c : intContacts) {
|
||||
contacts.add(new ContactId(c));
|
||||
}
|
||||
return contacts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLoadingScreen(boolean isBlocking, int stringId) {
|
||||
// this is handled by the recycler view in ContactSelectorFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoadingScreen() {
|
||||
// this is handled by the recycler view in ContactSelectorFragment
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.forum.ShareForumActivity.CONTACTS;
|
||||
import static org.briarproject.android.forum.ShareForumActivity.getContactsFromIds;
|
||||
import static org.briarproject.api.forum.ForumConstants.GROUP_ID;
|
||||
|
||||
public class ShareForumMessageFragment extends BaseFragment {
|
||||
|
||||
public final static String TAG = "IntroductionMessageFragment";
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ShareForumMessageFragment.class.getName());
|
||||
|
||||
private ShareForumActivity shareForumActivity;
|
||||
private ViewHolder ui;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
protected volatile ForumSharingManager forumSharingManager;
|
||||
private volatile GroupId groupId;
|
||||
private volatile Collection<ContactId> contacts;
|
||||
|
||||
public void initBundle(GroupId groupId, Collection<ContactId> contacts) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putByteArray(GROUP_ID, groupId.getBytes());
|
||||
bundle.putIntegerArrayList(CONTACTS, getContactsFromIds(contacts));
|
||||
setArguments(bundle);
|
||||
}
|
||||
|
||||
@Inject
|
||||
public ShareForumMessageFragment() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
shareForumActivity = (ShareForumActivity) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new InstantiationError(
|
||||
"This fragment is only meant to be attached to the ShareForumActivity");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
// change toolbar text
|
||||
ActionBar actionBar = shareForumActivity.getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(R.string.forum_share_button);
|
||||
}
|
||||
|
||||
// allow for home button to act as back button
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
// inflate view
|
||||
View v = inflater.inflate(R.layout.share_forum_message, container,
|
||||
false);
|
||||
ui = new ViewHolder(v);
|
||||
ui.button.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onButtonClick();
|
||||
}
|
||||
});
|
||||
|
||||
// get groupID and contactIDs from fragment arguments
|
||||
groupId = new GroupId(getArguments().getByteArray(GROUP_ID));
|
||||
ArrayList<Integer> intContacts =
|
||||
getArguments().getIntegerArrayList(CONTACTS);
|
||||
if (intContacts == null) throw new IllegalArgumentException();
|
||||
contacts = ShareForumActivity.getContactsFromIntegers(intContacts);
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
shareForumActivity.onBackPressed();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUniqueTag() {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
public void onButtonClick() {
|
||||
// disable button to prevent accidental double invitations
|
||||
ui.button.setEnabled(false);
|
||||
|
||||
String msg = ui.message.getText().toString();
|
||||
shareForum(msg);
|
||||
|
||||
// don't wait for the introduction to be made before finishing activity
|
||||
shareForumActivity.sharingSuccessful(ui.message);
|
||||
}
|
||||
|
||||
private void shareForum(final String msg) {
|
||||
shareForumActivity.runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
for (ContactId c : contacts) {
|
||||
forumSharingManager.sendForumInvitation(groupId, c,
|
||||
msg);
|
||||
}
|
||||
} catch (DbException e) {
|
||||
sharingError();
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sharingError() {
|
||||
shareForumActivity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(shareForumActivity,
|
||||
R.string.introduction_error, LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class ViewHolder {
|
||||
|
||||
private final EditText message;
|
||||
private final Button button;
|
||||
|
||||
ViewHolder(View v) {
|
||||
message = (EditText) v.findViewById(R.id.invitationMessageView);
|
||||
button = (Button) v.findViewById(R.id.shareForumButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemSelectedListener;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.AndroidComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.identity.CreateIdentityActivity;
|
||||
import org.briarproject.android.identity.LocalAuthorItem;
|
||||
import org.briarproject.android.identity.LocalAuthorItemComparator;
|
||||
import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
|
||||
import org.briarproject.android.util.CommonLayoutParams;
|
||||
import org.briarproject.android.util.LayoutUtils;
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.api.crypto.KeyParser;
|
||||
import org.briarproject.api.crypto.PrivateKey;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumPost;
|
||||
import org.briarproject.api.forum.ForumPostFactory;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
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 org.briarproject.util.StringUtils;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.text.InputType.TYPE_CLASS_TEXT;
|
||||
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
|
||||
import static android.widget.LinearLayout.VERTICAL;
|
||||
import static android.widget.RelativeLayout.ALIGN_PARENT_LEFT;
|
||||
import static android.widget.RelativeLayout.ALIGN_PARENT_RIGHT;
|
||||
import static android.widget.RelativeLayout.CENTER_VERTICAL;
|
||||
import static android.widget.RelativeLayout.LEFT_OF;
|
||||
import static android.widget.RelativeLayout.RIGHT_OF;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.forum.ForumActivity.FORUM_NAME;
|
||||
import static org.briarproject.android.forum.ForumActivity.MIN_TIMESTAMP;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_WRAP;
|
||||
|
||||
public class WriteForumPostActivity extends BriarActivity
|
||||
implements OnItemSelectedListener, OnClickListener {
|
||||
|
||||
private static final int REQUEST_CREATE_IDENTITY = 2;
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(WriteForumPostActivity.class.getName());
|
||||
|
||||
@Inject @CryptoExecutor protected Executor cryptoExecutor;
|
||||
private LocalAuthorSpinnerAdapter adapter = null;
|
||||
private Spinner spinner = null;
|
||||
private ImageButton sendButton = null;
|
||||
private EditText content = null;
|
||||
private AuthorId localAuthorId = null;
|
||||
private GroupId groupId = null;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject protected volatile IdentityManager identityManager;
|
||||
@Inject protected volatile ForumManager forumManager;
|
||||
@Inject protected volatile ForumPostFactory forumPostFactory;
|
||||
@Inject protected volatile CryptoComponent crypto;
|
||||
private volatile MessageId parentId = null;
|
||||
private volatile long minTimestamp = -1;
|
||||
private volatile LocalAuthor localAuthor = null;
|
||||
private volatile Forum forum = null;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
Intent i = getIntent();
|
||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException();
|
||||
groupId = new GroupId(b);
|
||||
String forumName = i.getStringExtra(FORUM_NAME);
|
||||
if (forumName == null) throw new IllegalStateException();
|
||||
setTitle(forumName);
|
||||
minTimestamp = i.getLongExtra(MIN_TIMESTAMP, -1);
|
||||
if (minTimestamp == -1) throw new IllegalStateException();
|
||||
b = i.getByteArrayExtra("briar.PARENT_ID");
|
||||
if (b != null) parentId = new MessageId(b);
|
||||
|
||||
if (state != null) {
|
||||
b = state.getByteArray("briar.LOCAL_AUTHOR_ID");
|
||||
if (b != null) localAuthorId = new AuthorId(b);
|
||||
}
|
||||
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
layout.setLayoutParams(MATCH_WRAP);
|
||||
layout.setOrientation(VERTICAL);
|
||||
int pad = LayoutUtils.getPadding(this);
|
||||
layout.setPadding(pad, 0, pad, pad);
|
||||
|
||||
RelativeLayout header = new RelativeLayout(this);
|
||||
|
||||
TextView from = new TextView(this);
|
||||
from.setId(1);
|
||||
from.setTextSize(18);
|
||||
from.setText(R.string.from);
|
||||
RelativeLayout.LayoutParams left = CommonLayoutParams.relative();
|
||||
left.addRule(ALIGN_PARENT_LEFT);
|
||||
left.addRule(CENTER_VERTICAL);
|
||||
header.addView(from, left);
|
||||
|
||||
adapter = new LocalAuthorSpinnerAdapter(this, true);
|
||||
spinner = new Spinner(this);
|
||||
spinner.setId(2);
|
||||
spinner.setAdapter(adapter);
|
||||
spinner.setOnItemSelectedListener(this);
|
||||
RelativeLayout.LayoutParams between = CommonLayoutParams.relative();
|
||||
between.addRule(CENTER_VERTICAL);
|
||||
between.addRule(RIGHT_OF, 1);
|
||||
between.addRule(LEFT_OF, 3);
|
||||
header.addView(spinner, between);
|
||||
|
||||
sendButton = new ImageButton(this);
|
||||
sendButton.setId(3);
|
||||
sendButton.setBackgroundResource(0);
|
||||
sendButton.setImageResource(R.drawable.social_send_now);
|
||||
sendButton.setEnabled(false); // Enabled after loading the forum
|
||||
sendButton.setOnClickListener(this);
|
||||
RelativeLayout.LayoutParams right = CommonLayoutParams.relative();
|
||||
right.addRule(ALIGN_PARENT_RIGHT);
|
||||
right.addRule(CENTER_VERTICAL);
|
||||
header.addView(sendButton, right);
|
||||
layout.addView(header);
|
||||
|
||||
content = new EditText(this);
|
||||
content.setId(4);
|
||||
int inputType = TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
| TYPE_TEXT_FLAG_CAP_SENTENCES;
|
||||
content.setInputType(inputType);
|
||||
content.setHint(R.string.forum_post_hint);
|
||||
layout.addView(content);
|
||||
|
||||
setContentView(layout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadAuthorsAndForum();
|
||||
}
|
||||
|
||||
private void loadAuthorsAndForum() {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
Collection<LocalAuthor> localAuthors =
|
||||
identityManager.getLocalAuthors();
|
||||
forum = forumManager.getForum(groupId);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Load took " + duration + " ms");
|
||||
displayAuthorsAndForum(localAuthors);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayAuthorsAndForum(
|
||||
final Collection<LocalAuthor> localAuthors) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
if (localAuthors.isEmpty()) throw new IllegalStateException();
|
||||
adapter.clear();
|
||||
for (LocalAuthor a : localAuthors)
|
||||
adapter.add(new LocalAuthorItem(a));
|
||||
adapter.sort(LocalAuthorItemComparator.INSTANCE);
|
||||
int count = adapter.getCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
LocalAuthorItem item = adapter.getItem(i);
|
||||
if (item == LocalAuthorItem.ANONYMOUS) continue;
|
||||
if (item == LocalAuthorItem.NEW) continue;
|
||||
if (item.getLocalAuthor().getId().equals(localAuthorId)) {
|
||||
localAuthor = item.getLocalAuthor();
|
||||
spinner.setSelection(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setTitle(forum.getName());
|
||||
sendButton.setEnabled(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle state) {
|
||||
super.onSaveInstanceState(state);
|
||||
if (localAuthorId != null) {
|
||||
byte[] b = localAuthorId.getBytes();
|
||||
state.putByteArray("briar.LOCAL_AUTHOR_ID", b);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int request, int result, Intent data) {
|
||||
super.onActivityResult(request, result, data);
|
||||
if (request == REQUEST_CREATE_IDENTITY && result == RESULT_OK) {
|
||||
byte[] b = data.getByteArrayExtra("briar.LOCAL_AUTHOR_ID");
|
||||
if (b == null) throw new IllegalStateException();
|
||||
localAuthorId = new AuthorId(b);
|
||||
loadAuthorsAndForum();
|
||||
}
|
||||
}
|
||||
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
LocalAuthorItem item = adapter.getItem(position);
|
||||
if (item == LocalAuthorItem.ANONYMOUS) {
|
||||
localAuthor = null;
|
||||
localAuthorId = null;
|
||||
} else if (item == LocalAuthorItem.NEW) {
|
||||
localAuthor = null;
|
||||
localAuthorId = null;
|
||||
Intent i = new Intent(this, CreateIdentityActivity.class);
|
||||
startActivityForResult(i, REQUEST_CREATE_IDENTITY);
|
||||
} else {
|
||||
localAuthor = item.getLocalAuthor();
|
||||
localAuthorId = localAuthor.getId();
|
||||
}
|
||||
}
|
||||
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
localAuthor = null;
|
||||
localAuthorId = null;
|
||||
}
|
||||
|
||||
public void onClick(View view) {
|
||||
if (forum == null) throw new IllegalStateException();
|
||||
String body = content.getText().toString();
|
||||
if (body.equals("")) return;
|
||||
createPost(StringUtils.toUtf8(body));
|
||||
Toast.makeText(this, R.string.post_sent_toast, LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
private void createPost(final byte[] body) {
|
||||
cryptoExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
// Don't use an earlier timestamp than the newest post
|
||||
long timestamp = System.currentTimeMillis();
|
||||
timestamp = Math.max(timestamp, minTimestamp);
|
||||
ForumPost p;
|
||||
try {
|
||||
if (localAuthor == null) {
|
||||
p = forumPostFactory.createAnonymousPost(groupId,
|
||||
timestamp, parentId, "text/plain", body);
|
||||
} else {
|
||||
KeyParser keyParser = crypto.getSignatureKeyParser();
|
||||
byte[] b = localAuthor.getPrivateKey();
|
||||
PrivateKey authorKey = keyParser.parsePrivateKey(b);
|
||||
p = forumPostFactory.createPseudonymousPost(groupId,
|
||||
timestamp, parentId, localAuthor, "text/plain",
|
||||
body, authorKey);
|
||||
}
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
storePost(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storePost(final ForumPost p) {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
forumManager.addLocalPost(p);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Storing message took " + duration + " ms");
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
private ListPreference enableBluetooth;
|
||||
private ListPreference torOverMobile;
|
||||
private CheckBoxPreference notifyPrivateMessages;
|
||||
private CheckBoxPreference notifyForumPosts;
|
||||
private CheckBoxPreference notifyVibration;
|
||||
private Preference notifySound;
|
||||
|
||||
@@ -93,6 +94,8 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
(ListPreference) findPreference("pref_key_tor_mobile");
|
||||
notifyPrivateMessages = (CheckBoxPreference) findPreference(
|
||||
"pref_key_notify_private_messages");
|
||||
notifyForumPosts = (CheckBoxPreference) findPreference(
|
||||
"pref_key_notify_forum_posts");
|
||||
notifyVibration = (CheckBoxPreference) findPreference(
|
||||
"pref_key_notify_vibration");
|
||||
notifySound = findPreference("pref_key_notify_sound");
|
||||
@@ -100,6 +103,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
enableBluetooth.setOnPreferenceChangeListener(this);
|
||||
torOverMobile.setOnPreferenceChangeListener(this);
|
||||
notifyPrivateMessages.setOnPreferenceChangeListener(this);
|
||||
notifyForumPosts.setOnPreferenceChangeListener(this);
|
||||
notifyVibration.setOnPreferenceChangeListener(this);
|
||||
|
||||
notifySound.setOnPreferenceClickListener(
|
||||
@@ -193,6 +197,9 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
notifyPrivateMessages.setChecked(settings.getBoolean(
|
||||
"notifyPrivateMessages", true));
|
||||
|
||||
notifyForumPosts.setChecked(settings.getBoolean(
|
||||
"notifyForumPosts", true));
|
||||
|
||||
notifyVibration.setChecked(settings.getBoolean(
|
||||
"notifyVibration", true));
|
||||
|
||||
@@ -234,6 +241,10 @@ public class SettingsFragment extends PreferenceFragmentCompat
|
||||
Settings s = new Settings();
|
||||
s.putBoolean("notifyPrivateMessages", (Boolean) o);
|
||||
storeSettings(s);
|
||||
} else if (preference == notifyForumPosts) {
|
||||
Settings s = new Settings();
|
||||
s.putBoolean("notifyForumPosts", (Boolean) o);
|
||||
storeSettings(s);
|
||||
} else if (preference == notifyVibration) {
|
||||
Settings s = new Settings();
|
||||
s.putBoolean("notifyVibration", (Boolean) o);
|
||||
|
||||
@@ -2,12 +2,14 @@ package org.briarproject.android.identity;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TextView.OnEditorActionListener;
|
||||
@@ -16,7 +18,7 @@ import android.widget.Toast;
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
import org.briarproject.android.BriarActivity;
|
||||
import org.briarproject.android.util.LayoutUtils;
|
||||
import org.briarproject.android.util.AndroidUtils;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.api.crypto.KeyPair;
|
||||
@@ -31,18 +33,11 @@ import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.text.InputType.TYPE_CLASS_TEXT;
|
||||
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_WORDS;
|
||||
import static android.view.Gravity.CENTER;
|
||||
import static android.view.Gravity.CENTER_HORIZONTAL;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.widget.LinearLayout.VERTICAL;
|
||||
import static android.widget.Toast.LENGTH_LONG;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.MATCH_MATCH;
|
||||
import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
|
||||
public class CreateIdentityActivity extends BriarActivity
|
||||
@@ -55,10 +50,10 @@ public class CreateIdentityActivity extends BriarActivity
|
||||
@CryptoExecutor
|
||||
protected Executor cryptoExecutor;
|
||||
|
||||
private TextInputLayout nicknameInput;
|
||||
private EditText nicknameEntry;
|
||||
private Button createIdentityButton;
|
||||
private ProgressBar progress;
|
||||
private TextView feedback;
|
||||
|
||||
// Fields that are accessed from background threads must be volatile
|
||||
@Inject
|
||||
@@ -71,52 +66,32 @@ public class CreateIdentityActivity extends BriarActivity
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
LinearLayout layout = new LinearLayout(this);
|
||||
layout.setLayoutParams(MATCH_MATCH);
|
||||
layout.setOrientation(VERTICAL);
|
||||
layout.setGravity(CENTER_HORIZONTAL);
|
||||
int pad = LayoutUtils.getPadding(this);
|
||||
layout.setPadding(pad, pad, pad, pad);
|
||||
|
||||
TextView chooseNickname = new TextView(this);
|
||||
chooseNickname.setGravity(CENTER);
|
||||
chooseNickname.setTextSize(18);
|
||||
chooseNickname.setText(R.string.choose_nickname);
|
||||
layout.addView(chooseNickname);
|
||||
setContentView(R.layout.activity_create_identity);
|
||||
|
||||
nicknameEntry = new EditText(this) {
|
||||
nicknameInput = (TextInputLayout) findViewById(R.id.nicknameInputLayout);
|
||||
nicknameEntry = (EditText) findViewById(R.id.nicknameEntry);
|
||||
nicknameEntry.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
protected void onTextChanged(CharSequence text, int start,
|
||||
int lengthBefore, int lengthAfter) {
|
||||
public void beforeTextChanged(CharSequence s, int start, int count,
|
||||
int after) {}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before,
|
||||
int count) {
|
||||
enableOrDisableCreateButton();
|
||||
}
|
||||
};
|
||||
nicknameEntry.setId(1);
|
||||
nicknameEntry.setMaxLines(1);
|
||||
int inputType = TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_WORDS;
|
||||
nicknameEntry.setInputType(inputType);
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
});
|
||||
nicknameEntry.setOnEditorActionListener(this);
|
||||
layout.addView(nicknameEntry);
|
||||
|
||||
feedback = new TextView(this);
|
||||
feedback.setGravity(CENTER);
|
||||
feedback.setPadding(0, pad, 0, pad);
|
||||
layout.addView(feedback);
|
||||
createIdentityButton = (Button) findViewById(R.id.createIdentityButton);
|
||||
if (createIdentityButton != null)
|
||||
createIdentityButton.setOnClickListener(this);
|
||||
|
||||
createIdentityButton = new Button(this);
|
||||
createIdentityButton.setLayoutParams(WRAP_WRAP);
|
||||
createIdentityButton.setText(R.string.create_identity_button);
|
||||
createIdentityButton.setEnabled(false);
|
||||
createIdentityButton.setOnClickListener(this);
|
||||
layout.addView(createIdentityButton);
|
||||
|
||||
progress = new ProgressBar(this);
|
||||
progress.setLayoutParams(WRAP_WRAP);
|
||||
progress.setIndeterminate(true);
|
||||
progress.setVisibility(GONE);
|
||||
layout.addView(progress);
|
||||
|
||||
setContentView(layout);
|
||||
progress = (ProgressBar) findViewById(R.id.progressBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,10 +114,11 @@ public class CreateIdentityActivity extends BriarActivity
|
||||
String nickname = nicknameEntry.getText().toString();
|
||||
int length = StringUtils.toUtf8(nickname).length;
|
||||
if (length > MAX_AUTHOR_NAME_LENGTH) {
|
||||
feedback.setText(R.string.name_too_long);
|
||||
String str = getString(R.string.name_too_long);
|
||||
AndroidUtils.setError(nicknameInput, str, true);
|
||||
return false;
|
||||
}
|
||||
feedback.setText("");
|
||||
AndroidUtils.setError(nicknameInput, null, false);
|
||||
return length > 0;
|
||||
}
|
||||
|
||||
@@ -169,7 +145,7 @@ public class CreateIdentityActivity extends BriarActivity
|
||||
}
|
||||
|
||||
private void storeLocalAuthor(final LocalAuthor a) {
|
||||
runOnDbThread(new Runnable() {
|
||||
dbController.runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
|
||||
@@ -19,6 +19,8 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.ResultPoint;
|
||||
import com.google.zxing.ResultPointCallback;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.AndroidComponent;
|
||||
@@ -27,6 +29,7 @@ import org.briarproject.android.fragment.BaseEventFragment;
|
||||
import org.briarproject.android.util.CameraView;
|
||||
import org.briarproject.android.util.QrCodeDecoder;
|
||||
import org.briarproject.android.util.QrCodeUtils;
|
||||
import org.briarproject.android.util.ViewfinderView;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.KeyAgreementAbortedEvent;
|
||||
import org.briarproject.api.event.KeyAgreementFailedEvent;
|
||||
@@ -55,7 +58,7 @@ import static java.util.logging.Level.WARNING;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class ShowQrCodeFragment extends BaseEventFragment
|
||||
implements QrCodeDecoder.ResultCallback {
|
||||
implements QrCodeDecoder.ResultCallback, ResultPointCallback {
|
||||
|
||||
public static final String TAG = "ShowQrCodeFragment";
|
||||
|
||||
@@ -75,6 +78,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
protected Executor ioExecutor;
|
||||
|
||||
private CameraView cameraView;
|
||||
private ViewfinderView viewfinderView;
|
||||
private View statusView;
|
||||
private TextView status;
|
||||
private ImageView qrCode;
|
||||
@@ -109,9 +113,13 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
cameraView = (CameraView) view.findViewById(R.id.camera_view);
|
||||
viewfinderView =
|
||||
(ViewfinderView) view.findViewById(R.id.viewfinder_view);
|
||||
statusView = view.findViewById(R.id.status_container);
|
||||
status = (TextView) view.findViewById(R.id.connect_status);
|
||||
qrCode = (ImageView) view.findViewById(R.id.qr_code);
|
||||
|
||||
viewfinderView.setFrameProvider(cameraView);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -120,7 +128,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
|
||||
getActivity().setRequestedOrientation(SCREEN_ORIENTATION_NOSENSOR);
|
||||
|
||||
decoder = new QrCodeDecoder(this);
|
||||
decoder = new QrCodeDecoder(this, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -219,6 +227,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
getActivity().finish();
|
||||
} else {
|
||||
cameraView.start(camera, decoder, 0);
|
||||
viewfinderView.drawViewfinder();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -355,6 +364,11 @@ public class ShowQrCodeFragment extends BaseEventFragment
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void foundPossibleResultPoint(ResultPoint point) {
|
||||
viewfinderView.addPossibleResultPoint(point);
|
||||
}
|
||||
|
||||
private class BluetoothStateReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -120,10 +120,11 @@ public class BriarRecyclerView extends FrameLayout {
|
||||
emptyView.setVisibility(VISIBLE);
|
||||
recyclerView.setVisibility(INVISIBLE);
|
||||
} else {
|
||||
emptyView.setVisibility(INVISIBLE);
|
||||
// use GONE here so empty view doesn't use space on small lists
|
||||
emptyView.setVisibility(GONE);
|
||||
recyclerView.setVisibility(VISIBLE);
|
||||
}
|
||||
progressBar.setVisibility(INVISIBLE);
|
||||
progressBar.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,4 +132,9 @@ public class BriarRecyclerView extends FrameLayout {
|
||||
if (recyclerView == null) initViews();
|
||||
recyclerView.scrollToPosition(position);
|
||||
}
|
||||
|
||||
public RecyclerView getRecyclerView() {
|
||||
return this.recyclerView;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.briarproject.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.Camera;
|
||||
import android.hardware.Camera.AutoFocusCallback;
|
||||
import android.hardware.Camera.CameraInfo;
|
||||
@@ -13,6 +15,7 @@ import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
@@ -31,17 +34,25 @@ import static java.util.logging.Level.WARNING;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
AutoFocusCallback {
|
||||
AutoFocusCallback, ViewfinderView.FrameProvider {
|
||||
|
||||
private static final int AUTO_FOCUS_RETRY_DELAY = 5000; // Milliseconds
|
||||
private static final int MIN_FRAME_SIZE = 240;
|
||||
private static final int MAX_FRAME_SIZE = 675; // = 5/8 * 1080
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(CameraView.class.getName());
|
||||
|
||||
private Camera camera = null;
|
||||
private Rect framingRect;
|
||||
private Rect framingRectInPreview;
|
||||
private Rect framingRectInSensor;
|
||||
private PreviewConsumer previewConsumer = null;
|
||||
private int displayOrientation = 0, surfaceWidth = 0, surfaceHeight = 0;
|
||||
private boolean autoFocus = false, surfaceExists = false;
|
||||
|
||||
private Point cameraResolution;
|
||||
private final Object cameraResolutionLock = new Object();
|
||||
|
||||
public CameraView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -184,6 +195,24 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
LOG.info("No suitable focus mode");
|
||||
}
|
||||
params.setZoom(0);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
List<Camera.Area> areas = new ArrayList<>();
|
||||
areas.add(new Camera.Area(getFramingRectInSensor(), 1000));
|
||||
if (params.getMaxNumFocusAreas() > 0) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Focus areas supported: " +
|
||||
params.getMaxNumFocusAreas());
|
||||
}
|
||||
params.setFocusAreas(areas);
|
||||
}
|
||||
if (params.getMaxNumMeteringAreas() > 0) {
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Metering areas supported: " +
|
||||
params.getMaxNumMeteringAreas());
|
||||
}
|
||||
params.setMeteringAreas(areas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPreviewSize(Parameters params) {
|
||||
@@ -222,6 +251,13 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Best size " + bestSize.width + "x" + bestSize.height);
|
||||
params.setPreviewSize(bestSize.width, bestSize.height);
|
||||
synchronized (cameraResolutionLock) {
|
||||
cameraResolution = new Point(bestSize.width, bestSize.height);
|
||||
}
|
||||
} else {
|
||||
synchronized (cameraResolutionLock) {
|
||||
cameraResolution = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,4 +312,152 @@ public class CameraView extends SurfaceView implements SurfaceHolder.Callback,
|
||||
LOG.log(WARNING, "Error retrying auto focus", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the framing rect which the UI should draw to show the user where to place the
|
||||
* barcode. This target helps with alignment as well as forces the user to hold the device
|
||||
* far enough away to ensure the image will be in focus.
|
||||
*
|
||||
* @return The rectangle to draw on screen in window coordinates.
|
||||
*/
|
||||
@Override
|
||||
public Rect getFramingRect() {
|
||||
if (framingRect == null) {
|
||||
framingRect = calculateFramingRect(true);
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Calculated framing rect: " + framingRect);
|
||||
}
|
||||
return framingRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the framing rect which the UI should draw to show the user where to place the
|
||||
* barcode. This target helps with alignment as well as forces the user to hold the device
|
||||
* far enough away to ensure the image will be in focus.
|
||||
* <p/>
|
||||
* Adapted from the Zxing Barcode Scanner.
|
||||
*
|
||||
* @return The rectangle to draw on screen in window coordinates.
|
||||
*/
|
||||
private Rect calculateFramingRect(boolean withOrientation) {
|
||||
if (camera == null) {
|
||||
return null;
|
||||
}
|
||||
if (surfaceWidth == 0 || surfaceHeight == 0) {
|
||||
// Called early, before the surface is ready
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean portrait =
|
||||
withOrientation && displayOrientation % 180 == 90;
|
||||
int size = findDesiredDimensionInRange(
|
||||
portrait ? surfaceWidth : surfaceHeight,
|
||||
portrait ? surfaceHeight / 2 : surfaceWidth / 2,
|
||||
MIN_FRAME_SIZE, MAX_FRAME_SIZE);
|
||||
|
||||
int leftOffset = portrait ?
|
||||
(surfaceWidth - size) / 2 :
|
||||
((surfaceWidth / 2) - size) / 2;
|
||||
int topOffset = portrait ?
|
||||
((surfaceHeight / 2) - size) / 2 :
|
||||
(surfaceHeight - size) / 2;
|
||||
return new Rect(leftOffset, topOffset, leftOffset + size,
|
||||
topOffset + size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the square that fits best inside the given region.
|
||||
*/
|
||||
private static int findDesiredDimensionInRange(int side1, int side2,
|
||||
int hardMin, int hardMax) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Finding framing dimension, side1 = " + side1 +
|
||||
", side2 = " + side2);
|
||||
int minSide = Math.min(side1, side2);
|
||||
int dim = 5 * minSide / 8; // Target 5/8 of smallest side
|
||||
if (dim < hardMin) {
|
||||
if (hardMin > minSide) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Returning minimum side length: " + minSide);
|
||||
return minSide;
|
||||
} else {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Returning hard minimum: " + hardMin);
|
||||
return hardMin;
|
||||
}
|
||||
}
|
||||
if (dim > hardMax) {
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Returning hard maximum: " + hardMax);
|
||||
return hardMax;
|
||||
}
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Returning desired dimension: " + dim);
|
||||
return dim;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #getFramingRect} but coordinates are in terms of the preview
|
||||
* frame, not UI / screen.
|
||||
* <p/>
|
||||
* Adapted from the Zxing Barcode Scanner.
|
||||
*
|
||||
* @return {@link Rect} expressing QR code scan area in terms of the preview size
|
||||
*/
|
||||
@Override
|
||||
public Rect getFramingRectInPreview() {
|
||||
if (framingRectInPreview == null) {
|
||||
Rect framingRect = getFramingRect();
|
||||
if (framingRect == null) {
|
||||
return null;
|
||||
}
|
||||
Rect rect = new Rect(framingRect);
|
||||
Point cameraResolution = getCameraResolution();
|
||||
if (cameraResolution == null || surfaceWidth == 0 ||
|
||||
surfaceHeight == 0) {
|
||||
// Called early, before the surface is ready
|
||||
return null;
|
||||
}
|
||||
rect.left = rect.left * cameraResolution.x / surfaceWidth;
|
||||
rect.right = rect.right * cameraResolution.x / surfaceWidth;
|
||||
rect.top = rect.top * cameraResolution.y / surfaceHeight;
|
||||
rect.bottom = rect.bottom * cameraResolution.y / surfaceHeight;
|
||||
framingRectInPreview = rect;
|
||||
}
|
||||
return framingRectInPreview;
|
||||
}
|
||||
|
||||
private Point getCameraResolution() {
|
||||
Point ret;
|
||||
synchronized (cameraResolutionLock) {
|
||||
ret = new Point(cameraResolution);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #getFramingRect} but coordinates are in terms of the sensor,
|
||||
* not UI / screen (ie. it is independent of orientation)
|
||||
*
|
||||
* @return {@link Rect} expressing QR code scan area in terms of the sensor
|
||||
*/
|
||||
private Rect getFramingRectInSensor() {
|
||||
if (framingRectInSensor == null) {
|
||||
Rect framingRect = calculateFramingRect(false);
|
||||
if (framingRect == null) {
|
||||
return null;
|
||||
}
|
||||
Rect rect = new Rect(framingRect);
|
||||
if (surfaceWidth == 0 || surfaceHeight == 0) {
|
||||
// Called early, before the surface is ready
|
||||
return null;
|
||||
}
|
||||
rect.left = (rect.left * 2000 / surfaceWidth) - 1000;
|
||||
rect.right = (rect.right * 2000 / surfaceWidth) - 1000;
|
||||
rect.top = (rect.top * 2000 / surfaceHeight) - 1000;
|
||||
rect.bottom = (rect.bottom * 2000 / surfaceHeight) - 1000;
|
||||
framingRectInSensor = rect;
|
||||
}
|
||||
return framingRectInSensor;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,18 @@ import android.hardware.Camera.Size;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.google.zxing.BinaryBitmap;
|
||||
import com.google.zxing.DecodeHintType;
|
||||
import com.google.zxing.LuminanceSource;
|
||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
||||
import com.google.zxing.Reader;
|
||||
import com.google.zxing.ReaderException;
|
||||
import com.google.zxing.Result;
|
||||
import com.google.zxing.ResultPointCallback;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
@@ -26,11 +30,14 @@ public class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
||||
|
||||
private final Reader reader = new QRCodeReader();
|
||||
private final ResultCallback callback;
|
||||
private final ResultPointCallback pointCallback;
|
||||
|
||||
private boolean stopped = false;
|
||||
|
||||
public QrCodeDecoder(ResultCallback callback) {
|
||||
public QrCodeDecoder(ResultCallback callback,
|
||||
ResultPointCallback pointCallback) {
|
||||
this.callback = callback;
|
||||
this.pointCallback = pointCallback;
|
||||
}
|
||||
|
||||
public void start(Camera camera) {
|
||||
@@ -72,9 +79,11 @@ public class QrCodeDecoder implements PreviewConsumer, PreviewCallback {
|
||||
LuminanceSource src = new PlanarYUVLuminanceSource(data, width,
|
||||
height, 0, 0, width, height, false);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(src));
|
||||
Map<DecodeHintType, Object> hints = new HashMap<>();
|
||||
hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, pointCallback);
|
||||
Result result = null;
|
||||
try {
|
||||
result = reader.decode(bitmap);
|
||||
result = reader.decode(bitmap, hints);
|
||||
} catch (ReaderException e) {
|
||||
return null; // No barcode found
|
||||
} catch (RuntimeException e) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.briarproject.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import org.briarproject.R;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
public class TextAvatarView extends FrameLayout {
|
||||
|
||||
final private AppCompatTextView textView;
|
||||
final private CircleImageView backgroundView;
|
||||
|
||||
public TextAvatarView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater
|
||||
.inflate(R.layout.text_avatar_view, this, true);
|
||||
textView = (AppCompatTextView) findViewById(R.id.textAvatarView);
|
||||
backgroundView = (CircleImageView) findViewById(R.id.avatarBackground);
|
||||
}
|
||||
|
||||
public TextAvatarView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
textView.setText(text);
|
||||
}
|
||||
|
||||
public void setBackgroundBytes(byte[] bytes) {
|
||||
int r = getByte(bytes, 0) * 3 / 4 + 96;
|
||||
int g = getByte(bytes, 1) * 3 / 4 + 96;
|
||||
int b = getByte(bytes, 2) * 3 / 4 + 96;
|
||||
int color = Color.rgb(r, g, b);
|
||||
|
||||
backgroundView.setFillColor(color);
|
||||
}
|
||||
|
||||
private byte getByte(byte[] bytes, int index) {
|
||||
if (bytes == null) {
|
||||
return -128;
|
||||
} else {
|
||||
return bytes[index % bytes.length];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (C) 2008 ZXing authors
|
||||
* Copyright (C) 2016 Sublime Software Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.briarproject.android.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.zxing.ResultPoint;
|
||||
|
||||
import org.briarproject.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This view is overlaid on top of the camera preview. It adds the viewfinder
|
||||
* rectangle and partial transparency outside it, as well as the laser scanner
|
||||
* animation and result points.
|
||||
*
|
||||
* @author dswitkin@google.com (Daniel Switkin)
|
||||
*/
|
||||
public final class ViewfinderView extends View {
|
||||
|
||||
private static final int[] SCANNER_ALPHA =
|
||||
{0, 64, 128, 192, 255, 192, 128, 64};
|
||||
private static final long ANIMATION_DELAY = 80L;
|
||||
private static final int CURRENT_POINT_OPACITY = 0xA0;
|
||||
private static final int MAX_RESULT_POINTS = 20;
|
||||
private static final int POINT_SIZE = 6;
|
||||
|
||||
private FrameProvider frameProvider;
|
||||
private final Paint paint;
|
||||
private Bitmap resultBitmap;
|
||||
private final int maskColor;
|
||||
private final int resultColor;
|
||||
private final int laserColor;
|
||||
private final int resultPointColor;
|
||||
private int scannerAlpha;
|
||||
private List<ResultPoint> possibleResultPoints;
|
||||
private List<ResultPoint> lastPossibleResultPoints;
|
||||
|
||||
// This constructor is used when the class is built from an XML resource.
|
||||
public ViewfinderView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
if (isInEditMode()) {
|
||||
paint = null;
|
||||
maskColor = 0;
|
||||
resultColor = 0;
|
||||
laserColor = 0;
|
||||
resultPointColor = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize these once for performance rather than calling them every
|
||||
// time in onDraw().
|
||||
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
Resources resources = getResources();
|
||||
maskColor = resources.getColor(R.color.viewfinder_mask);
|
||||
resultColor = resources.getColor(R.color.result_view);
|
||||
laserColor = resources.getColor(R.color.viewfinder_laser);
|
||||
resultPointColor = resources.getColor(R.color.possible_result_points);
|
||||
scannerAlpha = 0;
|
||||
possibleResultPoints = new ArrayList<>(5);
|
||||
lastPossibleResultPoints = null;
|
||||
}
|
||||
|
||||
public void setFrameProvider(FrameProvider frameProvider) {
|
||||
this.frameProvider = frameProvider;
|
||||
}
|
||||
|
||||
@SuppressLint("DrawAllocation")
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
if (frameProvider == null) {
|
||||
return; // not ready yet, early draw before done configuring
|
||||
}
|
||||
Rect frame = this.frameProvider.getFramingRect();
|
||||
Rect previewFrame = this.frameProvider.getFramingRectInPreview();
|
||||
if (frame == null || previewFrame == null) {
|
||||
return;
|
||||
}
|
||||
int width = canvas.getWidth();
|
||||
int height = canvas.getHeight();
|
||||
|
||||
// Draw the exterior (i.e. outside the framing rect) darkened
|
||||
paint.setColor(resultBitmap != null ? resultColor : maskColor);
|
||||
canvas.drawRect(0, 0, width, frame.top, paint);
|
||||
canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
|
||||
canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1,
|
||||
paint);
|
||||
canvas.drawRect(0, frame.bottom + 1, width, height, paint);
|
||||
|
||||
if (resultBitmap != null) {
|
||||
// Draw the opaque result bitmap over the scanning rectangle
|
||||
paint.setAlpha(CURRENT_POINT_OPACITY);
|
||||
canvas.drawBitmap(resultBitmap, null, frame, paint);
|
||||
} else {
|
||||
|
||||
// Draw a red "laser scanner" line through the middle to show
|
||||
// decoding is active
|
||||
paint.setColor(laserColor);
|
||||
paint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
|
||||
scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
|
||||
int middle = frame.height() / 2 + frame.top;
|
||||
canvas.drawRect(frame.left + 2, middle - 1, frame.right - 1,
|
||||
middle + 2, paint);
|
||||
|
||||
float scaleX = frame.width() / (float) previewFrame.width();
|
||||
float scaleY = frame.height() / (float) previewFrame.height();
|
||||
|
||||
List<ResultPoint> currentPossible = possibleResultPoints;
|
||||
List<ResultPoint> currentLast = lastPossibleResultPoints;
|
||||
int frameLeft = frame.left;
|
||||
int frameTop = frame.top;
|
||||
if (currentPossible.isEmpty()) {
|
||||
lastPossibleResultPoints = null;
|
||||
} else {
|
||||
possibleResultPoints = new ArrayList<>(5);
|
||||
lastPossibleResultPoints = currentPossible;
|
||||
paint.setAlpha(CURRENT_POINT_OPACITY);
|
||||
paint.setColor(resultPointColor);
|
||||
synchronized (currentPossible) {
|
||||
for (ResultPoint point : currentPossible) {
|
||||
canvas.drawCircle(
|
||||
frameLeft + (int) (point.getX() * scaleX),
|
||||
frameTop + (int) (point.getY() * scaleY),
|
||||
POINT_SIZE, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentLast != null) {
|
||||
paint.setAlpha(CURRENT_POINT_OPACITY / 2);
|
||||
paint.setColor(resultPointColor);
|
||||
synchronized (currentLast) {
|
||||
float radius = POINT_SIZE / 2.0f;
|
||||
for (ResultPoint point : currentLast) {
|
||||
canvas.drawCircle(
|
||||
frameLeft + (int) (point.getX() * scaleX),
|
||||
frameTop + (int) (point.getY() * scaleY),
|
||||
radius, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request another update at the animation interval, but only
|
||||
// repaint the laser line, not the entire viewfinder mask.
|
||||
postInvalidateDelayed(ANIMATION_DELAY,
|
||||
frame.left - POINT_SIZE,
|
||||
frame.top - POINT_SIZE,
|
||||
frame.right + POINT_SIZE,
|
||||
frame.bottom + POINT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
public void drawViewfinder() {
|
||||
Bitmap resultBitmap = this.resultBitmap;
|
||||
this.resultBitmap = null;
|
||||
if (resultBitmap != null) {
|
||||
resultBitmap.recycle();
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a bitmap with the result points highlighted instead of the live
|
||||
* scanning display.
|
||||
*
|
||||
* @param barcode An image of the decoded barcode.
|
||||
*/
|
||||
public void drawResultBitmap(Bitmap barcode) {
|
||||
resultBitmap = barcode;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void addPossibleResultPoint(ResultPoint point) {
|
||||
List<ResultPoint> points = possibleResultPoints;
|
||||
synchronized (points) {
|
||||
points.add(point);
|
||||
int size = points.size();
|
||||
if (size > MAX_RESULT_POINTS) {
|
||||
// trim it
|
||||
points.subList(0, size - MAX_RESULT_POINTS / 2).clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface FrameProvider {
|
||||
|
||||
Rect getFramingRect();
|
||||
Rect getFramingRectInPreview();
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,6 @@ targetCompatibility = 1.6
|
||||
|
||||
apply plugin: 'witness'
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "com.google.dagger:dagger:2.0.2"
|
||||
compile 'com.google.dagger:dagger-compiler:2.0.2'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.briarproject.api.clients;
|
||||
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.sync.BaseMessageContext;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class BdfMessageContext extends BaseMessageContext {
|
||||
|
||||
private final BdfDictionary dictionary;
|
||||
|
||||
public BdfMessageContext(BdfDictionary dictionary,
|
||||
Collection<MessageId> dependencies) {
|
||||
|
||||
super(dependencies);
|
||||
this.dictionary = dictionary;
|
||||
}
|
||||
|
||||
public BdfMessageContext(BdfDictionary dictionary) {
|
||||
this(dictionary, null);
|
||||
}
|
||||
|
||||
public BdfDictionary getDictionary() {
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import org.briarproject.api.db.Metadata;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.sync.ClientId;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.InvalidMessageException;
|
||||
import org.briarproject.api.sync.MessageContext;
|
||||
|
||||
public interface MessageQueueManager {
|
||||
|
||||
@@ -34,10 +36,11 @@ public interface MessageQueueManager {
|
||||
interface QueueMessageValidator {
|
||||
|
||||
/**
|
||||
* Validates the given message and returns its metadata if the message
|
||||
* is valid, or null if the message is invalid.
|
||||
* Validates the given message and returns its metadata and
|
||||
* dependencies.
|
||||
*/
|
||||
Metadata validateMessage(QueueMessage q, Group g);
|
||||
MessageContext validateMessage(QueueMessage q, Group g)
|
||||
throws InvalidMessageException;
|
||||
}
|
||||
|
||||
interface IncomingQueueMessageHook {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.briarproject.api.event;
|
||||
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.introduction.IntroductionRequest;
|
||||
|
||||
public class ForumInvitationReceivedEvent extends Event {
|
||||
|
||||
private final Forum forum;
|
||||
private final ContactId contactId;
|
||||
|
||||
public ForumInvitationReceivedEvent(Forum forum, ContactId contactId) {
|
||||
this.forum = forum;
|
||||
this.contactId = contactId;
|
||||
}
|
||||
|
||||
public Forum getForum() {
|
||||
return forum;
|
||||
}
|
||||
|
||||
public ContactId getContactId() {
|
||||
return contactId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.briarproject.api.event;
|
||||
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
|
||||
public class ForumInvitationResponseReceivedEvent extends Event {
|
||||
|
||||
private final String forumName;
|
||||
private final ContactId contactId;
|
||||
|
||||
public ForumInvitationResponseReceivedEvent(String forumName,
|
||||
ContactId contactId) {
|
||||
|
||||
this.forumName = forumName;
|
||||
this.contactId = contactId;
|
||||
}
|
||||
|
||||
public String getForumName() {
|
||||
return forumName;
|
||||
}
|
||||
|
||||
public ContactId getContactId() {
|
||||
return contactId;
|
||||
}
|
||||
}
|
||||
43
briar-api/src/org/briarproject/api/forum/Forum.java
Normal file
43
briar-api/src/org/briarproject/api/forum/Forum.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package org.briarproject.api.forum;
|
||||
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
public class Forum {
|
||||
|
||||
private final Group group;
|
||||
private final String name;
|
||||
private final byte[] salt;
|
||||
|
||||
public Forum(Group group, String name, byte[] salt) {
|
||||
this.group = group;
|
||||
this.name = name;
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
public GroupId getId() {
|
||||
return group.getId();
|
||||
}
|
||||
|
||||
public Group getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return group.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof Forum && group.equals(((Forum) o).group);
|
||||
}
|
||||
}
|
||||
52
briar-api/src/org/briarproject/api/forum/ForumConstants.java
Normal file
52
briar-api/src/org/briarproject/api/forum/ForumConstants.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package org.briarproject.api.forum;
|
||||
|
||||
import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
|
||||
|
||||
public interface ForumConstants {
|
||||
|
||||
/** The maximum length of a forum's name in UTF-8 bytes. */
|
||||
int MAX_FORUM_NAME_LENGTH = 100;
|
||||
|
||||
/** The length of a forum's random salt in bytes. */
|
||||
int FORUM_SALT_LENGTH = 32;
|
||||
|
||||
/** The maximum length of a forum post's content type in UTF-8 bytes. */
|
||||
int MAX_CONTENT_TYPE_LENGTH = 50;
|
||||
|
||||
/** The maximum length of a forum post's body in bytes. */
|
||||
int MAX_FORUM_POST_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
|
||||
|
||||
/* Forum Sharing Constants */
|
||||
String CONTACT_ID = "contactId";
|
||||
String GROUP_ID = "groupId";
|
||||
String TO_BE_SHARED_BY_US = "toBeSharedByUs";
|
||||
String SHARED_BY_US = "sharedByUs";
|
||||
String SHARED_WITH_US = "sharedWithUs";
|
||||
String TYPE = "type";
|
||||
String SESSION_ID = "sessionId";
|
||||
String STORAGE_ID = "storageId";
|
||||
String STATE = "state";
|
||||
String LOCAL = "local";
|
||||
String TIME = "time";
|
||||
String READ = "read";
|
||||
String IS_SHARER = "isSharer";
|
||||
String FORUM_ID = "forumId";
|
||||
String FORUM_NAME = "forumName";
|
||||
String FORUM_SALT = "forumSalt";
|
||||
String INVITATION_MSG = "invitationMsg";
|
||||
int SHARE_MSG_TYPE_INVITATION = 1;
|
||||
int SHARE_MSG_TYPE_ACCEPT = 2;
|
||||
int SHARE_MSG_TYPE_DECLINE = 3;
|
||||
int SHARE_MSG_TYPE_LEAVE = 4;
|
||||
int SHARE_MSG_TYPE_ABORT = 5;
|
||||
String TASK = "task";
|
||||
int TASK_ADD_FORUM_TO_LIST_SHARED_WITH_US = 0;
|
||||
int TASK_REMOVE_FORUM_FROM_LIST_SHARED_WITH_US = 1;
|
||||
int TASK_ADD_SHARED_FORUM = 2;
|
||||
int TASK_ADD_FORUM_TO_LIST_TO_BE_SHARED_BY_US = 3;
|
||||
int TASK_REMOVE_FORUM_FROM_LIST_TO_BE_SHARED_BY_US = 4;
|
||||
int TASK_SHARE_FORUM = 5;
|
||||
int TASK_UNSHARE_FORUM_SHARED_BY_US = 6;
|
||||
int TASK_UNSHARE_FORUM_SHARED_WITH_US = 7;
|
||||
|
||||
}
|
||||
11
briar-api/src/org/briarproject/api/forum/ForumFactory.java
Normal file
11
briar-api/src/org/briarproject/api/forum/ForumFactory.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package org.briarproject.api.forum;
|
||||
|
||||
public interface ForumFactory {
|
||||
|
||||
/** Creates a forum with the given name. */
|
||||
Forum createForum(String name);
|
||||
|
||||
/** Creates a forum with the given name and salt. */
|
||||
Forum createForum(String name, byte[] salt);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.briarproject.api.forum;
|
||||
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.messaging.BaseMessage;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
public class ForumInvitationMessage extends BaseMessage {
|
||||
|
||||
private final SessionId sessionId;
|
||||
private final ContactId contactId;
|
||||
private final String forumName, message;
|
||||
private final boolean available;
|
||||
|
||||
public ForumInvitationMessage(MessageId id, SessionId sessionId,
|
||||
ContactId contactId, String forumName, String message,
|
||||
boolean available, long time, boolean local, boolean sent,
|
||||
boolean seen, boolean read) {
|
||||
|
||||
super(id, time, local, read, sent, seen);
|
||||
this.sessionId = sessionId;
|
||||
this.contactId = contactId;
|
||||
this.forumName = forumName;
|
||||
this.message = message;
|
||||
this.available = available;
|
||||
}
|
||||
|
||||
public SessionId getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public ContactId getContactId() {
|
||||
return contactId;
|
||||
}
|
||||
|
||||
public String getForumName() {
|
||||
return forumName;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return available;
|
||||
}
|
||||
|
||||
}
|
||||
49
briar-api/src/org/briarproject/api/forum/ForumManager.java
Normal file
49
briar-api/src/org/briarproject/api/forum/ForumManager.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package org.briarproject.api.forum;
|
||||
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.sync.ClientId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface ForumManager {
|
||||
|
||||
/** Returns the unique ID of the forum client. */
|
||||
ClientId getClientId();
|
||||
|
||||
/** Subscribes to a forum. */
|
||||
Forum addForum(String name) throws DbException;
|
||||
|
||||
/** Unsubscribes from a forum. */
|
||||
void removeForum(Forum f) throws DbException;
|
||||
|
||||
/** Stores a local forum post. */
|
||||
void addLocalPost(ForumPost p) throws DbException;
|
||||
|
||||
/** Returns the forum with the given ID. */
|
||||
Forum getForum(GroupId g) throws DbException;
|
||||
|
||||
/** Returns the forum with the given ID. */
|
||||
Forum getForum(Transaction txn, GroupId g) throws DbException;
|
||||
|
||||
/** Returns all forums to which the user subscribes. */
|
||||
Collection<Forum> getForums() throws DbException;
|
||||
|
||||
/** Returns the body of the forum post with the given ID. */
|
||||
byte[] getPostBody(MessageId m) throws DbException;
|
||||
|
||||
/** Returns the headers of all posts in the given forum. */
|
||||
Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException;
|
||||
|
||||
/** Marks a forum post as read or unread. */
|
||||
void setReadFlag(MessageId m, boolean read) throws DbException;
|
||||
|
||||
/** Registers a hook to be called whenever a forum is removed. */
|
||||
void registerRemoveForumHook(RemoveForumHook hook);
|
||||
|
||||
interface RemoveForumHook {
|
||||
void removingForum(Transaction txn, Forum f) throws DbException;
|
||||
}
|
||||
}
|
||||
37
briar-api/src/org/briarproject/api/forum/ForumPost.java
Normal file
37
briar-api/src/org/briarproject/api/forum/ForumPost.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package org.briarproject.api.forum;
|
||||
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
public class ForumPost {
|
||||
|
||||
private final Message message;
|
||||
private final MessageId parent;
|
||||
private final Author author;
|
||||
private final String contentType;
|
||||
|
||||
public ForumPost(Message message, MessageId parent, Author author,
|
||||
String contentType) {
|
||||
this.message = message;
|
||||
this.parent = parent;
|
||||
this.author = author;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public Message getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public MessageId getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public Author getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.briarproject.api.forum;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.crypto.PrivateKey;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
public interface ForumPostFactory {
|
||||
|
||||
ForumPost createAnonymousPost(GroupId groupId, long timestamp,
|
||||
MessageId parent, String contentType, byte[] body)
|
||||
throws FormatException;
|
||||
|
||||
ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
|
||||
MessageId parent, Author author, String contentType, byte[] body,
|
||||
PrivateKey privateKey) throws FormatException,
|
||||
GeneralSecurityException;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.briarproject.api.forum;
|
||||
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
public class ForumPostHeader {
|
||||
|
||||
private final MessageId id;
|
||||
private final long timestamp;
|
||||
private final Author author;
|
||||
private final Author.Status authorStatus;
|
||||
private final String contentType;
|
||||
private final boolean read;
|
||||
|
||||
public ForumPostHeader(MessageId id, long timestamp, Author author,
|
||||
Author.Status authorStatus, String contentType, boolean read) {
|
||||
this.id = id;
|
||||
this.timestamp = timestamp;
|
||||
this.author = author;
|
||||
this.authorStatus = authorStatus;
|
||||
this.contentType = contentType;
|
||||
this.read = read;
|
||||
}
|
||||
|
||||
public MessageId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Author getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public Author.Status getAuthorStatus() {
|
||||
return authorStatus;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public boolean isRead() {
|
||||
return read;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user