mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-14 03:39:05 +01:00
Forum, nested discussions front end UI/UX, rev 2
This commit is contained in:
@@ -129,16 +129,6 @@
|
||||
/>
|
||||
</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"
|
||||
@@ -159,17 +149,6 @@
|
||||
/>
|
||||
</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"
|
||||
|
||||
4
briar-android/res/drawable/chevron48dp_down.xml
Normal file
4
briar-android/res/drawable/chevron48dp_down.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<vector android:height="24dp" android:viewportHeight="48.0"
|
||||
android:viewportWidth="48.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9.1,19.3l14.9,11.8l14.9,-11.8l-1.9,-2.4l-13,10.4l-13,-10.4z"/>
|
||||
</vector>
|
||||
4
briar-android/res/drawable/chevron48dp_up.xml
Normal file
4
briar-android/res/drawable/chevron48dp_up.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<vector android:height="24dp" android:viewportHeight="48.0"
|
||||
android:viewportWidth="48.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M38.9,28.7l-14.9,-11.8l-14.9,11.8l1.9,2.4l13,-10.4l13,10.4z"/>
|
||||
</vector>
|
||||
9
briar-android/res/drawable/level_indicator_circle.xml
Normal file
9
briar-android/res/drawable/level_indicator_circle.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||
|
||||
<solid android:color="@color/window_background"/>
|
||||
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@color/forum_discussion_nested_line"/>
|
||||
</shape>
|
||||
5
briar-android/res/drawable/selector_chevron.xml
Normal file
5
briar-android/res/drawable/selector_chevron.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true" android:drawable="@drawable/chevron48dp_down"/>
|
||||
<item android:drawable="@drawable/chevron48dp_up"/>
|
||||
</selector>
|
||||
21
briar-android/res/layout/activity_forum.xml
Normal file
21
briar-android/res/layout/activity_forum.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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">
|
||||
|
||||
<org.briarproject.android.util.BriarRecyclerView
|
||||
android:id="@+id/forum_discussion_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
app:scrollToEnd="false"/>
|
||||
|
||||
<include
|
||||
layout="@layout/text_input_field"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
161
briar-android/res/layout/forum_discussion_cell.xml
Normal file
161
briar-android/res/layout/forum_discussion_cell.xml
Normal file
@@ -0,0 +1,161 @@
|
||||
<?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:id="@+id/forum_cell"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<View
|
||||
android:id="@+id/nested_line_1"
|
||||
style="@style/DiscussionLevelIndicator"
|
||||
android:layout_width="@dimen/forum_nested_line_width"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="showingDescendants"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/nested_line_2"
|
||||
style="@style/DiscussionLevelIndicator"
|
||||
android:layout_width="@dimen/forum_nested_line_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toRightOf="@id/nested_line_1"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/nested_line_3"
|
||||
style="@style/DiscussionLevelIndicator"
|
||||
android:layout_width="@dimen/forum_nested_line_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toRightOf="@id/nested_line_2"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/nested_line_4"
|
||||
style="@style/DiscussionLevelIndicator"
|
||||
android:layout_width="@dimen/forum_nested_line_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toRightOf="@id/nested_line_3"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/nested_line_5"
|
||||
style="@style/DiscussionLevelIndicator"
|
||||
android:layout_width="@dimen/forum_nested_line_width"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toRightOf="@id/nested_line_4"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nested_line_text"
|
||||
android:layout_width="@dimen/forum_nested_indicator"
|
||||
android:layout_height="@dimen/forum_nested_indicator"
|
||||
android:layout_centerInParent="true"
|
||||
android:background="@drawable/level_indicator_circle"
|
||||
android:gravity="center"
|
||||
android:textSize="@dimen/text_size_small"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/margin_medium"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/margin_small"
|
||||
android:layout_marginLeft="@dimen/margin_medium"
|
||||
android:layout_marginRight="@dimen/margin_medium"
|
||||
android:layout_marginTop="@dimen/margin_medium"
|
||||
android:textIsSelectable="true"
|
||||
android:textSize="@dimen/text_size_medium"
|
||||
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."/>
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="@dimen/forum_avatar_size"
|
||||
android:layout_height="@dimen/forum_avatar_size"
|
||||
android:layout_alignLeft="@id/text"
|
||||
android:layout_below="@id/text"
|
||||
android:layout_marginRight="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:src="@drawable/ic_launcher"
|
||||
app:civ_border_color="@color/briar_primary"
|
||||
app:civ_border_width="@dimen/avatar_border_width"
|
||||
tools:src="@drawable/ic_launcher"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/chevron"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_below="@id/text"
|
||||
android:layout_marginRight="@dimen/margin_medium"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:clickable="true"
|
||||
android:src="@drawable/selector_chevron"
|
||||
android:tint="@color/briar_button_positive"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/btn_reply"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text"
|
||||
android:layout_marginRight="@dimen/margin_medium"
|
||||
android:layout_toLeftOf="@id/chevron"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:text="@string/btn_reply"
|
||||
android:textColor="@color/briar_button_positive"
|
||||
android:textSize="@dimen/text_size_tiny"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/replies"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@id/btn_reply"
|
||||
android:layout_toLeftOf="@id/btn_reply"
|
||||
android:padding="@dimen/margin_medium"
|
||||
android:textSize="@dimen/text_size_tiny"
|
||||
tools:text="2 replies"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBaseline="@id/replies"
|
||||
android:layout_toLeftOf="@id/replies"
|
||||
android:layout_toRightOf="@id/avatar"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="@dimen/text_size_tiny"
|
||||
tools:text="09:09 John Smith"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/bottom_divider"
|
||||
style="@style/Divider.ForumList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/margin_separator"
|
||||
android:layout_alignLeft="@id/text"
|
||||
android:layout_below="@id/btn_reply"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
35
briar-android/res/layout/text_input_field.xml
Normal file
35
briar-android/res/layout/text_input_field.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
android:id="@+id/text_input_container"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/button_bar_background"
|
||||
android:elevation="@dimen/margin_tiny"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/margin_large"
|
||||
android:paddingStart="@dimen/margin_large">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/input_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:layout_weight="1"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:maxLines="5"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/btn_send"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="38dp"
|
||||
android:layout_margin="@dimen/margin_medium"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/send"
|
||||
android:onClick="sendMessage"
|
||||
android:src="@drawable/social_send_now_white"
|
||||
android:tint="@color/briar_primary"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
7
briar-android/res/values/attrs.xml
Normal file
7
briar-android/res/values/attrs.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="BriarRecyclerView">
|
||||
<attr name="scrollToEnd" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
@@ -43,4 +43,6 @@
|
||||
|
||||
<color name="spinner_border">#61000000</color> <!-- 38% Black -->
|
||||
<color name="spinner_arrow">@color/briar_blue_dark</color>
|
||||
<color name="forum_discussion_nested_line">#cfd2d4</color>
|
||||
<color name="forum_cell_highlight">#ffffff</color>
|
||||
</resources>
|
||||
@@ -41,5 +41,8 @@
|
||||
<dimen name="message_bubble_margin_tail">14dp</dimen>
|
||||
<dimen name="message_bubble_margin_non_tail">51dp</dimen>
|
||||
<dimen name="message_bubble_timestamp_margin">15dp</dimen>
|
||||
<dimen name="forum_nested_line_width">2dp</dimen>
|
||||
<dimen name="forum_nested_indicator">24dp</dimen>
|
||||
<dimen name="forum_avatar_size">20dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -198,6 +198,17 @@
|
||||
<string name="introduction_success_title">Introduced contact was added</string>
|
||||
<string name="introduction_success_text">You have been introduced to %1$s.</string>
|
||||
|
||||
<!-- Forum -->
|
||||
<string name="btn_reply">REPLY</string>
|
||||
<plurals name="message_replies">
|
||||
<item quantity="one">%1$d reply</item>
|
||||
<item quantity="other">%1$d replies</item>
|
||||
</plurals>
|
||||
<string name="forum_new_entry_posted">Forum entry posted</string>
|
||||
<string name="forum_new_entry_received">New forum entry</string>
|
||||
<string name="forum_new_message_hint">New Entry</string>
|
||||
<string name="forum_message_reply_hint">New Reply</string>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<string name="dialog_title_lost_password">Lost Password</string>
|
||||
<string name="dialog_message_lost_password">Password recovery is not possible. Do you want to delete your account?\n\nCaution: This will permanently delete your identities, contacts and messages</string>
|
||||
@@ -225,4 +236,6 @@
|
||||
<!-- Progress titles -->
|
||||
<string name="progress_title_logout">Signing out of Briar..</string>
|
||||
<string name="progress_title_please_wait">Please wait..</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -140,4 +140,9 @@
|
||||
<item name="android:layout_marginBottom">16dp</item>
|
||||
</style>
|
||||
|
||||
<style name="DiscussionLevelIndicator">
|
||||
<item name="android:layout_marginLeft">4dp</item>
|
||||
<item name="android:background">?android:attr/listDivider</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -8,10 +8,8 @@ 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;
|
||||
@@ -54,16 +52,12 @@ public interface ActivityComponent {
|
||||
|
||||
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);
|
||||
|
||||
@@ -12,6 +12,8 @@ import org.briarproject.android.controller.ConfigController;
|
||||
import org.briarproject.android.controller.ConfigControllerImpl;
|
||||
import org.briarproject.android.controller.DbController;
|
||||
import org.briarproject.android.controller.DbControllerImpl;
|
||||
import org.briarproject.android.forum.ForumController;
|
||||
import org.briarproject.android.forum.ForumControllerImpl;
|
||||
import org.briarproject.android.controller.NavDrawerController;
|
||||
import org.briarproject.android.controller.NavDrawerControllerImpl;
|
||||
import org.briarproject.android.controller.PasswordController;
|
||||
@@ -21,6 +23,7 @@ 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.ForumTestControllerImpl;
|
||||
import org.briarproject.android.forum.ShareForumMessageFragment;
|
||||
import org.briarproject.android.fragment.BaseFragment;
|
||||
import org.briarproject.android.introduction.ContactChooserFragment;
|
||||
@@ -98,6 +101,22 @@ public class ActivityModule {
|
||||
return dbController;
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
protected ForumController provideForumController(
|
||||
ForumControllerImpl forumController) {
|
||||
activity.addLifecycleController(forumController);
|
||||
return forumController;
|
||||
}
|
||||
|
||||
@Named("ForumTestController")
|
||||
@ActivityScope
|
||||
@Provides
|
||||
protected ForumController provideForumTestController(
|
||||
ForumTestControllerImpl forumController) {
|
||||
return forumController;
|
||||
}
|
||||
|
||||
@ActivityScope
|
||||
@Provides
|
||||
protected NavDrawerController provideNavDrawerController(
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application;
|
||||
|
||||
import org.briarproject.android.api.AndroidNotificationManager;
|
||||
import org.briarproject.android.api.ReferenceManager;
|
||||
import org.briarproject.android.forum.ForumPersistentData;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.PublicKey;
|
||||
import org.briarproject.api.crypto.SecretKey;
|
||||
@@ -136,4 +137,10 @@ public class AppModule {
|
||||
eventBus.addListener(notificationManager);
|
||||
return notificationManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ForumPersistentData provideForumPersistence(ForumPersistentData fpd) {
|
||||
return fpd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,23 @@ package org.briarproject.android.forum;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
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.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -22,76 +27,61 @@ import org.briarproject.R;
|
||||
import org.briarproject.android.ActivityComponent;
|
||||
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.MessageStateChangedEvent;
|
||||
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.android.controller.handler.ResultHandler;
|
||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||
import org.briarproject.android.util.BriarRecyclerView;
|
||||
import org.briarproject.android.util.CustomAnimations;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
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 im.delight.android.identicons.IdenticonDrawable;
|
||||
|
||||
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.INVISIBLE;
|
||||
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;
|
||||
import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
|
||||
|
||||
public class ForumActivity extends BriarActivity implements EventListener,
|
||||
OnItemClickListener {
|
||||
public class ForumActivity extends BriarActivity implements
|
||||
ForumController.ForumPostListener {
|
||||
|
||||
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;
|
||||
public static final String FORUM_NAME = "briar.FORUM_NAME";
|
||||
public static final String MIN_TIMESTAMP = "briar.MIN_TIMESTAMP";
|
||||
private static final int REQUEST_FORUM_SHARED = 3;
|
||||
|
||||
@Inject
|
||||
protected AndroidNotificationManager notificationManager;
|
||||
|
||||
// uncomment the next line for a test component with dummy data
|
||||
// @Named("ForumTestController")
|
||||
@Inject
|
||||
protected ForumController forumController;
|
||||
|
||||
private BriarRecyclerView recyclerView;
|
||||
private EditText textInput;
|
||||
private ViewGroup inputContainer;
|
||||
private LinearLayoutManager linearLayoutManager;
|
||||
|
||||
// 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;
|
||||
|
||||
protected ForumAdapter forumAdapter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle state) {
|
||||
super.onCreate(state);
|
||||
|
||||
setContentView(R.layout.activity_forum);
|
||||
|
||||
Intent i = getIntent();
|
||||
byte[] b = i.getByteArrayExtra(GROUP_ID);
|
||||
if (b == null) throw new IllegalStateException();
|
||||
@@ -99,32 +89,30 @@ public class ForumActivity extends BriarActivity implements EventListener,
|
||||
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);
|
||||
inputContainer = (ViewGroup) findViewById(R.id.text_input_container);
|
||||
inputContainer.setVisibility(GONE);
|
||||
textInput = (EditText) findViewById(R.id.input_text);
|
||||
recyclerView =
|
||||
(BriarRecyclerView) findViewById(R.id.forum_discussion_list);
|
||||
linearLayoutManager = new LinearLayoutManager(this);
|
||||
recyclerView.setLayoutManager(linearLayoutManager);
|
||||
recyclerView.showProgressBar();
|
||||
forumController
|
||||
.loadForum(groupId, new UiResultHandler<Boolean>(this) {
|
||||
@Override
|
||||
public void onResultUi(Boolean result) {
|
||||
if (result) {
|
||||
setTitle(forumController.getForumName());
|
||||
forumAdapter = new ForumAdapter(
|
||||
forumController.getForumEntries());
|
||||
recyclerView.setAdapter(forumAdapter);
|
||||
recyclerView.showData();
|
||||
} else {
|
||||
// TODO Maybe an error dialog ?
|
||||
finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,14 +120,20 @@ public class ForumActivity extends BriarActivity implements EventListener,
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
private void displaySnackbar(int stringId) {
|
||||
Snackbar snackbar =
|
||||
Snackbar.make(recyclerView, stringId, Snackbar.LENGTH_SHORT);
|
||||
snackbar.getView().setBackgroundResource(R.color.briar_primary);
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
eventBus.addListener(this);
|
||||
notificationManager.blockNotification(groupId);
|
||||
notificationManager.clearForumPostNotification(groupId);
|
||||
loadForum();
|
||||
loadHeaders();
|
||||
protected void onActivityResult(int request, int result, Intent data) {
|
||||
super.onActivityResult(request, result, data);
|
||||
|
||||
if (request == REQUEST_FORUM_SHARED && result == RESULT_OK) {
|
||||
displaySnackbar(R.string.forum_shared_snackbar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,6 +145,27 @@ public class ForumActivity extends BriarActivity implements EventListener,
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (inputContainer.getVisibility() == VISIBLE) {
|
||||
inputContainer.setVisibility(GONE);
|
||||
forumAdapter.setReplyEntry(null);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
private void showTextInput(boolean isNewMessage) {
|
||||
// An animation here would be an overkill because of the keyboard
|
||||
// popping up.
|
||||
inputContainer.setVisibility(View.VISIBLE);
|
||||
textInput.setText("");
|
||||
textInput.requestFocus();
|
||||
textInput.setHint(isNewMessage ? R.string.forum_new_message_hint :
|
||||
R.string.forum_message_reply_hint);
|
||||
showSoftKeyboard(textInput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
ActivityOptionsCompat options = ActivityOptionsCompat
|
||||
@@ -159,11 +174,9 @@ public class ForumActivity extends BriarActivity implements EventListener,
|
||||
// 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);
|
||||
if (inputContainer.getVisibility() != VISIBLE) {
|
||||
showTextInput(true);
|
||||
}
|
||||
return true;
|
||||
case R.id.action_forum_share:
|
||||
Intent i2 = new Intent(this, ShareForumActivity.class);
|
||||
@@ -187,225 +200,34 @@ public class ForumActivity extends BriarActivity implements EventListener,
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
notificationManager.blockNotification(groupId);
|
||||
notificationManager.clearForumPostNotification(groupId);
|
||||
}
|
||||
|
||||
@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());
|
||||
public void sendMessage(View view) {
|
||||
String text = textInput.getText().toString();
|
||||
if (text.trim().length() == 0)
|
||||
return;
|
||||
ForumEntry replyEntry = forumAdapter.getReplyEntry();
|
||||
if (replyEntry == null) {
|
||||
// root post
|
||||
forumController.createPost(StringUtils.toUtf8(text));
|
||||
} else {
|
||||
forumController.createPost(StringUtils.toUtf8(text),
|
||||
replyEntry.getMessageId());
|
||||
}
|
||||
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 MessageStateChangedEvent) {
|
||||
MessageStateChangedEvent m = (MessageStateChangedEvent) e;
|
||||
if (m.getState() == DELIVERED &&
|
||||
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);
|
||||
hideSoftKeyboard(textInput);
|
||||
inputContainer.setVisibility(GONE);
|
||||
forumAdapter.setReplyEntry(null);
|
||||
}
|
||||
|
||||
private void showUnsubscribeDialog() {
|
||||
@@ -413,10 +235,19 @@ public class ForumActivity extends BriarActivity implements EventListener,
|
||||
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();
|
||||
forumController.unsubscribe(
|
||||
new UiResultHandler<Boolean>(
|
||||
ForumActivity.this) {
|
||||
@Override
|
||||
public void onResultUi(Boolean result) {
|
||||
if (result) {
|
||||
Toast.makeText(ForumActivity.this,
|
||||
R.string.forum_left_toast,
|
||||
LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
AlertDialog.Builder builder =
|
||||
@@ -429,21 +260,346 @@ public class ForumActivity extends BriarActivity implements EventListener,
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void addLocalEntry(int index, ForumEntry entry) {
|
||||
forumAdapter.addEntry(index, entry, true);
|
||||
displaySnackbar(R.string.forum_new_entry_posted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addForeignEntry(int index, ForumEntry entry) {
|
||||
forumAdapter.addEntry(index, entry, false);
|
||||
displaySnackbar(R.string.forum_new_entry_received);
|
||||
}
|
||||
|
||||
static class ForumViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public final TextView textView, lvlText, dateText, repliesText;
|
||||
public final View[] lvls;
|
||||
public final ImageView avatar;
|
||||
public final View chevron, replyButton;
|
||||
public final ViewGroup cell;
|
||||
public final View bottomDivider;
|
||||
|
||||
public ForumViewHolder(View v) {
|
||||
super(v);
|
||||
|
||||
textView = (TextView) v.findViewById(R.id.text);
|
||||
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
|
||||
dateText = (TextView) v.findViewById(R.id.date);
|
||||
repliesText = (TextView) v.findViewById(R.id.replies);
|
||||
int[] nestedLineIds = {
|
||||
R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3,
|
||||
R.id.nested_line_4, R.id.nested_line_5
|
||||
};
|
||||
lvls = new View[nestedLineIds.length];
|
||||
for (int i = 0; i < lvls.length; i++) {
|
||||
lvls[i] = v.findViewById(nestedLineIds[i]);
|
||||
}
|
||||
avatar = (ImageView) v.findViewById(R.id.avatar);
|
||||
chevron = v.findViewById(R.id.chevron);
|
||||
replyButton = v.findViewById(R.id.btn_reply);
|
||||
cell = (ViewGroup) v.findViewById(R.id.forum_cell);
|
||||
bottomDivider = v.findViewById(R.id.bottom_divider);
|
||||
}
|
||||
}
|
||||
|
||||
public class ForumAdapter extends RecyclerView.Adapter<ForumViewHolder> {
|
||||
|
||||
private final List<ForumEntry> forumEntries;
|
||||
// highlight not depandant on time
|
||||
private ForumEntry replyEntry;
|
||||
// temporary highlight
|
||||
private ForumEntry addedEntry;
|
||||
|
||||
public ForumAdapter(@NonNull List<ForumEntry> forumEntries) {
|
||||
this.forumEntries = forumEntries;
|
||||
}
|
||||
|
||||
private ForumEntry getReplyEntry() {
|
||||
return replyEntry;
|
||||
}
|
||||
|
||||
public void addEntry(int index, ForumEntry entry,
|
||||
boolean isScrolling) {
|
||||
forumEntries.add(index, entry);
|
||||
boolean isShowingDescendants = false;
|
||||
if (entry.getLevel() > 0) {
|
||||
// update parent and make sure descendants are visible
|
||||
// Note that the parent's visibility is guaranteed (otherwise
|
||||
// the reply button would not be visible)
|
||||
for (int i = index - 1; i >= 0; i--) {
|
||||
ForumEntry higherEntry = forumEntries.get(i);
|
||||
if (higherEntry.getLevel() < entry.getLevel()) {
|
||||
// parent found
|
||||
if (!higherEntry.isShowingDescendants()) {
|
||||
isShowingDescendants = true;
|
||||
showDescendants(higherEntry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isShowingDescendants) {
|
||||
int visiblePos = getVisiblePos(entry);
|
||||
notifyItemInserted(visiblePos);
|
||||
if (isScrolling)
|
||||
linearLayoutManager
|
||||
.scrollToPositionWithOffset(visiblePos, 0);
|
||||
}
|
||||
addedEntry = entry;
|
||||
}
|
||||
|
||||
private boolean hasDescendants(ForumEntry forumEntry) {
|
||||
int i = forumEntries.indexOf(forumEntry);
|
||||
if (i >= 0 && i < forumEntries.size() - 1) {
|
||||
if (forumEntries.get(i + 1).getLevel() >
|
||||
forumEntry.getLevel()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasVisibleDescendants(int visiblePos) {
|
||||
int levelLimit = forumEntries.get(visiblePos).getLevel();
|
||||
for (int i = visiblePos + 1; i < getItemCount(); i++) {
|
||||
ForumEntry entry = getVisibleEntry(i);
|
||||
if (entry.getLevel() <= levelLimit)
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int getReplyCount(ForumEntry entry) {
|
||||
int counter = 0;
|
||||
int pos = forumEntries.indexOf(entry);
|
||||
if (pos >= 0) {
|
||||
int ancestorLvl = forumEntries.get(pos).getLevel();
|
||||
for (int i = pos + 1; i < forumEntries.size(); i++) {
|
||||
int descendantLvl = forumEntries.get(i).getLevel();
|
||||
if (descendantLvl <= ancestorLvl)
|
||||
break;
|
||||
if (descendantLvl == ancestorLvl + 1)
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
return counter;
|
||||
}
|
||||
|
||||
public void setReplyEntry(ForumEntry entry) {
|
||||
if (replyEntry != null) {
|
||||
notifyItemChanged(getVisiblePos(replyEntry));
|
||||
}
|
||||
replyEntry = entry;
|
||||
if (replyEntry != null) {
|
||||
notifyItemChanged(getVisiblePos(replyEntry));
|
||||
}
|
||||
}
|
||||
|
||||
private List<Integer> getSubTreeIndexes(int pos, int levelLimit) {
|
||||
List<Integer> indexList = new ArrayList<>();
|
||||
|
||||
for (int i = pos + 1; i < getItemCount(); i++) {
|
||||
ForumEntry entry = getVisibleEntry(i);
|
||||
if (entry.getLevel() > levelLimit) {
|
||||
indexList.add(i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return indexList;
|
||||
}
|
||||
|
||||
public void showDescendants(ForumEntry forumEntry) {
|
||||
forumEntry.setShowingDescendants(true);
|
||||
int visiblePos = getVisiblePos(forumEntry);
|
||||
List<Integer> indexList =
|
||||
getSubTreeIndexes(visiblePos, forumEntry.getLevel());
|
||||
if (!indexList.isEmpty()) {
|
||||
if (indexList.size() == 1) {
|
||||
notifyItemInserted(indexList.get(0));
|
||||
} else {
|
||||
notifyItemRangeInserted(indexList.get(0),
|
||||
indexList.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void hideDescendants(ForumEntry forumEntry) {
|
||||
int visiblePos = getVisiblePos(forumEntry);
|
||||
List<Integer> indexList =
|
||||
getSubTreeIndexes(visiblePos, forumEntry.getLevel());
|
||||
if (!indexList.isEmpty()) {
|
||||
if (indexList.size() == 1) {
|
||||
notifyItemRemoved(indexList.get(0));
|
||||
} else {
|
||||
notifyItemRangeRemoved(indexList.get(0),
|
||||
indexList.size());
|
||||
}
|
||||
}
|
||||
forumEntry.setShowingDescendants(false);
|
||||
}
|
||||
|
||||
public int getVisiblePos(ForumEntry entry) {
|
||||
int visibleCounter = 0;
|
||||
int levelLimit = -1;
|
||||
for (int i = 0; i < forumEntries.size(); i++) {
|
||||
ForumEntry forumEntry = forumEntries.get(i);
|
||||
if (forumEntry.equals(entry)) {
|
||||
return visibleCounter;
|
||||
} else if (levelLimit >= 0 &&
|
||||
levelLimit < forumEntry.getLevel()) {
|
||||
// entry is in a hidden sub-tree
|
||||
continue;
|
||||
}
|
||||
levelLimit = -1;
|
||||
if (!forumEntry.isShowingDescendants()) {
|
||||
levelLimit = forumEntry.getLevel();
|
||||
}
|
||||
visibleCounter++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ForumEntry getVisibleEntry(int position) {
|
||||
int levelLimit = -1;
|
||||
for (ForumEntry forumEntry : forumEntries) {
|
||||
if (levelLimit >= 0) {
|
||||
if (forumEntry.getLevel() > levelLimit) {
|
||||
continue;
|
||||
}
|
||||
levelLimit = -1;
|
||||
}
|
||||
if (!forumEntry.isShowingDescendants()) {
|
||||
levelLimit = forumEntry.getLevel();
|
||||
}
|
||||
if (position-- == 0) {
|
||||
return forumEntry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ForumViewHolder onCreateViewHolder(
|
||||
ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.forum_discussion_cell, parent, false);
|
||||
return new ForumViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(
|
||||
final ForumViewHolder ui, final int position) {
|
||||
final ForumEntry data = getVisibleEntry(position);
|
||||
if (!data.isRead()) {
|
||||
data.setRead(true);
|
||||
forumController.entryRead(data);
|
||||
}
|
||||
ui.textView.setText(data.getText());
|
||||
|
||||
for (int i = 0; i < ui.lvls.length; i++) {
|
||||
ui.lvls[i].setVisibility(i < data.getLevel() ? VISIBLE : GONE);
|
||||
}
|
||||
if (data.getLevel() > 5) {
|
||||
ui.lvlText.setVisibility(VISIBLE);
|
||||
ui.lvlText.setText("" + data.getLevel());
|
||||
} else {
|
||||
ui.lvlText.setVisibility(GONE);
|
||||
}
|
||||
ui.dateText.setText(DateUtils
|
||||
.getRelativeTimeSpanString(ForumActivity.this,
|
||||
data.getTimestamp()) + " " + data.getAuthor());
|
||||
|
||||
int replies = getReplyCount(data);
|
||||
if (replies == 0) {
|
||||
ui.repliesText.setText("");
|
||||
} else {
|
||||
ui.repliesText.setText(getResources()
|
||||
.getQuantityString(R.plurals.message_replies, replies,
|
||||
replies));
|
||||
}
|
||||
ui.avatar.setImageDrawable(
|
||||
new IdenticonDrawable(data.getAuthorId().getBytes()));
|
||||
|
||||
if (hasDescendants(data)) {
|
||||
ui.chevron.setVisibility(VISIBLE);
|
||||
if (hasVisibleDescendants(position)) {
|
||||
ui.chevron.setSelected(false);
|
||||
} else {
|
||||
ui.chevron.setSelected(true);
|
||||
}
|
||||
ui.chevron.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ui.chevron.setSelected(!ui.chevron.isSelected());
|
||||
if (ui.chevron.isSelected()) {
|
||||
hideDescendants(data);
|
||||
} else {
|
||||
showDescendants(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ui.chevron.setVisibility(INVISIBLE);
|
||||
}
|
||||
if (data.equals(replyEntry)) {
|
||||
ui.cell.setBackgroundColor(ContextCompat
|
||||
.getColor(ForumActivity.this,
|
||||
R.color.forum_cell_highlight));
|
||||
} else if (data.equals(addedEntry)) {
|
||||
CustomAnimations.animateColorTransition(ui.cell, ContextCompat
|
||||
.getColor(ForumActivity.this,
|
||||
R.color.window_background), 3000,
|
||||
new ResultHandler<Void>() {
|
||||
@Override
|
||||
public void onResult(Void result) {
|
||||
ui.setIsRecyclable(true);
|
||||
}
|
||||
});
|
||||
// don't allow cell recycling until the animation finishes
|
||||
ui.setIsRecyclable(false);
|
||||
addedEntry = null;
|
||||
} else {
|
||||
ui.cell.setBackgroundColor(ContextCompat
|
||||
.getColor(ForumActivity.this,
|
||||
R.color.window_background));
|
||||
}
|
||||
ui.replyButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (inputContainer.getVisibility() != VISIBLE) {
|
||||
showTextInput(false);
|
||||
}
|
||||
setReplyEntry(data);
|
||||
linearLayoutManager
|
||||
.scrollToPositionWithOffset(position, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int visibleCounter = 0;
|
||||
int levelLimit = -1;
|
||||
for (ForumEntry forumEntry : forumEntries) {
|
||||
if (levelLimit >= 0) {
|
||||
if (forumEntry.getLevel() > levelLimit) {
|
||||
continue;
|
||||
}
|
||||
levelLimit = -1;
|
||||
}
|
||||
if (!forumEntry.isShowingDescendants()) {
|
||||
levelLimit = forumEntry.getLevel();
|
||||
}
|
||||
visibleCounter++;
|
||||
}
|
||||
return visibleCounter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.android.controller.ActivityLifecycleController;
|
||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public interface ForumController extends ActivityLifecycleController {
|
||||
|
||||
void loadForum(GroupId groupId, UiResultHandler<Boolean> resultHandler);
|
||||
String getForumName();
|
||||
List<ForumEntry> getForumEntries();
|
||||
void unsubscribe(UiResultHandler<Boolean> resultHandler);
|
||||
void entryRead(ForumEntry forumEntry);
|
||||
void entriesRead(Collection<ForumEntry> messageIds);
|
||||
void createPost(byte[] body);
|
||||
void createPost(byte[] body, MessageId parentId);
|
||||
|
||||
public interface ForumPostListener {
|
||||
void addLocalEntry(int index, ForumEntry entry);
|
||||
void addForeignEntry(int index, ForumEntry entry);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import org.briarproject.android.controller.DbControllerImpl;
|
||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||
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.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.MessageStateChangedEvent;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumPost;
|
||||
import org.briarproject.api.forum.ForumPostFactory;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
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.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
import java.util.concurrent.Executor;
|
||||
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.api.sync.ValidationManager.State.DELIVERED;
|
||||
|
||||
public class ForumControllerImpl extends DbControllerImpl
|
||||
implements ForumController, EventListener {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ForumControllerImpl.class.getName());
|
||||
|
||||
@Inject
|
||||
protected Activity activity;
|
||||
@Inject
|
||||
@CryptoExecutor
|
||||
protected Executor cryptoExecutor;
|
||||
@Inject
|
||||
protected volatile ForumPostFactory forumPostFactory;
|
||||
@Inject
|
||||
protected volatile CryptoComponent crypto;
|
||||
@Inject
|
||||
protected volatile ForumManager forumManager;
|
||||
@Inject
|
||||
protected volatile EventBus eventBus;
|
||||
@Inject
|
||||
protected volatile IdentityManager identityManager;
|
||||
@Inject
|
||||
protected ForumPersistentData data;
|
||||
|
||||
private ForumPostListener listener;
|
||||
private MessageId localAdd = null;
|
||||
|
||||
@Inject
|
||||
ForumControllerImpl() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreate() {
|
||||
if (activity instanceof ForumPostListener) {
|
||||
listener = (ForumPostListener) activity;
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
"An activity that injects the ForumController must " +
|
||||
"implement the ForumPostListener");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResume() {
|
||||
eventBus.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPause() {
|
||||
eventBus.removeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroy() {
|
||||
if (activity.isFinishing()) {
|
||||
data.clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void findSingleNewEntry() {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
List<ForumEntry> oldEntries = getForumEntries();
|
||||
data.clearHeaders();
|
||||
try {
|
||||
loadPosts();
|
||||
List<ForumEntry> allEntries = getForumEntries();
|
||||
int i = 0;
|
||||
for (ForumEntry entry : allEntries) {
|
||||
boolean isNew = true;
|
||||
for (ForumEntry oldEntry : oldEntries) {
|
||||
if (entry.getMessageId()
|
||||
.equals(oldEntry.getMessageId())) {
|
||||
isNew = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isNew) {
|
||||
if (localAdd != null &&
|
||||
entry.getMessageId().equals(localAdd)) {
|
||||
addLocalEntry(i, entry);
|
||||
} else {
|
||||
addForeignEntry(i, entry);
|
||||
}
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} catch (DbException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void eventOccurred(Event e) {
|
||||
if (e instanceof MessageStateChangedEvent) {
|
||||
MessageStateChangedEvent m = (MessageStateChangedEvent) e;
|
||||
if (m.getState() == DELIVERED &&
|
||||
m.getMessage().getGroupId().equals(data.getGroupId())) {
|
||||
LOG.info("Message added, reloading");
|
||||
findSingleNewEntry();
|
||||
}
|
||||
} else if (e instanceof GroupRemovedEvent) {
|
||||
GroupRemovedEvent s = (GroupRemovedEvent) e;
|
||||
if (s.getGroup().getId().equals(data.getGroupId())) {
|
||||
LOG.info("Forum removed");
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
activity.finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAuthor() throws DbException {
|
||||
Collection<LocalAuthor> localAuthors =
|
||||
identityManager.getLocalAuthors();
|
||||
|
||||
for (LocalAuthor author : localAuthors) {
|
||||
if (author == null)
|
||||
continue;
|
||||
data.setLocalAuthor(author);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void loadPosts() throws DbException {
|
||||
long now = System.currentTimeMillis();
|
||||
Collection<ForumPostHeader> headers =
|
||||
forumManager.getPostHeaders(data.getGroupId());
|
||||
data.addHeaders(headers);
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading headers took " + duration + " ms");
|
||||
now = System.currentTimeMillis();
|
||||
for (ForumPostHeader header : headers) {
|
||||
if (data.getBody(header.getId()) == null) {
|
||||
byte[] body = forumManager.getPostBody(header.getId());
|
||||
data.addBody(header.getId(), body);
|
||||
}
|
||||
}
|
||||
duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading bodies took " + duration + " ms");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadForum(final GroupId groupId,
|
||||
final UiResultHandler<Boolean> resultHandler) {
|
||||
LOG.info("Loading forum...");
|
||||
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (data.getGroupId() == null ||
|
||||
!data.getGroupId().equals(groupId)) {
|
||||
data.setGroupId(groupId);
|
||||
long now = System.currentTimeMillis();
|
||||
data.setForum(forumManager.getForum(groupId));
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading forum took " + duration +
|
||||
" ms");
|
||||
now = System.currentTimeMillis();
|
||||
loadAuthor();
|
||||
duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Loading author took " + duration +
|
||||
" ms");
|
||||
loadPosts();
|
||||
}
|
||||
resultHandler.onResult(true);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
resultHandler.onResult(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getForumName() {
|
||||
return data.getForum() == null ? null : data.getForum().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ForumEntry> getForumEntries() {
|
||||
Collection<ForumPostHeader> headers = data.getHeaders();
|
||||
List<ForumEntry> forumEntries = new ArrayList<>();
|
||||
Stack<MessageId> idStack = new Stack<>();
|
||||
|
||||
for (ForumPostHeader h : headers) {
|
||||
if (h.getParentId() == null) {
|
||||
idStack.clear();
|
||||
} else if (idStack.isEmpty() ||
|
||||
!idStack.contains(h.getParentId())) {
|
||||
idStack.push(h.getParentId());
|
||||
} else if (!h.getParentId().equals(idStack.peek())) {
|
||||
do {
|
||||
idStack.pop();
|
||||
} while (!h.getParentId().equals(idStack.peek()));
|
||||
}
|
||||
forumEntries.add(new ForumEntry(h,
|
||||
StringUtils.fromUtf8(data.getBody(h.getId())),
|
||||
idStack.size()));
|
||||
}
|
||||
return forumEntries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(final UiResultHandler<Boolean> resultHandler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
forumManager.removeForum(data.getForum());
|
||||
long duration = System.currentTimeMillis() - now;
|
||||
if (LOG.isLoggable(INFO))
|
||||
LOG.info("Removing forum took " + duration + " ms");
|
||||
resultHandler.onResult(true);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
resultHandler.onResult(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entryRead(ForumEntry forumEntry) {
|
||||
entriesRead(Collections.singletonList(forumEntry));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entriesRead(final Collection<ForumEntry> forumEntries) {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
for (ForumEntry fe : forumEntries) {
|
||||
forumManager.setReadFlag(fe.getMessageId(), 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPost(byte[] body) {
|
||||
createPost(body, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPost(final byte[] body, final MessageId parentId) {
|
||||
cryptoExecutor.execute(new Runnable() {
|
||||
public void run() {
|
||||
// Don't use an earlier timestamp than the newest post
|
||||
long timestamp = System.currentTimeMillis();
|
||||
ForumPost p;
|
||||
try {
|
||||
KeyParser keyParser = crypto.getSignatureKeyParser();
|
||||
byte[] b = data.getLocalAuthor().getPrivateKey();
|
||||
PrivateKey authorKey = keyParser.parsePrivateKey(b);
|
||||
p = forumPostFactory.createPseudonymousPost(
|
||||
data.getGroupId(), timestamp, parentId,
|
||||
data.getLocalAuthor(), "text/plain", body,
|
||||
authorKey);
|
||||
} catch (GeneralSecurityException | FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
storePost(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addLocalEntry(final int index, final ForumEntry entry) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.addLocalEntry(index, entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addForeignEntry(final int index, final ForumEntry entry) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
listener.addForeignEntry(index, entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storePost(final ForumPost p) {
|
||||
runOnDbThread(new Runnable() {
|
||||
public void run() {
|
||||
try {
|
||||
localAdd = p.getMessage().getId();
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
public class ForumEntry {
|
||||
|
||||
private final MessageId messageId;
|
||||
private final String text;
|
||||
private final int level;
|
||||
private final long timestamp;
|
||||
private final String author;
|
||||
private final AuthorId authorId;
|
||||
private boolean isShowingDescendants = true;
|
||||
private boolean isRead = true;
|
||||
|
||||
public ForumEntry(ForumPostHeader h, String text, int level) {
|
||||
this(h.getId(), text, level, h.getTimestamp(), h.getAuthor().getName(),
|
||||
h.getAuthor().getId());
|
||||
this.isRead = h.isRead();
|
||||
}
|
||||
|
||||
public ForumEntry(MessageId messageId, String text, int level,
|
||||
long timestamp, String author, AuthorId authorId) {
|
||||
this.messageId = messageId;
|
||||
this.text = text;
|
||||
this.level = level;
|
||||
this.timestamp = timestamp;
|
||||
this.author = author;
|
||||
this.authorId = authorId;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public AuthorId getAuthorId() {
|
||||
return authorId;
|
||||
}
|
||||
|
||||
public boolean isShowingDescendants() {
|
||||
return isShowingDescendants;
|
||||
}
|
||||
|
||||
public void setShowingDescendants(boolean showingDescendants) {
|
||||
this.isShowingDescendants = showingDescendants;
|
||||
}
|
||||
|
||||
public MessageId getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public boolean isRead() {
|
||||
return isRead;
|
||||
}
|
||||
|
||||
public void setRead(boolean read) {
|
||||
isRead = read;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.api.clients.MessageTree;
|
||||
import org.briarproject.api.forum.Forum;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.clients.MessageTreeImpl;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* This class is a singleton that defines the data that should persist, i.e.
|
||||
* still be present in memory after activity restarts. This class is not thread
|
||||
* safe.
|
||||
*/
|
||||
public class ForumPersistentData {
|
||||
|
||||
protected volatile MessageTree<ForumPostHeader> tree =
|
||||
new MessageTreeImpl<>();
|
||||
private volatile Map<MessageId, byte[]> bodyCache = new HashMap<>();
|
||||
private volatile LocalAuthor localAuthor;
|
||||
private volatile Forum forum;
|
||||
private volatile GroupId groupId;
|
||||
|
||||
@Inject
|
||||
public ForumPersistentData() {
|
||||
|
||||
}
|
||||
|
||||
public void clearAll() {
|
||||
tree.clear();
|
||||
bodyCache.clear();
|
||||
localAuthor = null;
|
||||
forum = null;
|
||||
groupId = null;
|
||||
}
|
||||
|
||||
public void clearHeaders() {
|
||||
tree.clear();
|
||||
}
|
||||
|
||||
public void addHeaders(Collection<ForumPostHeader> headers) {
|
||||
tree.add(headers);
|
||||
}
|
||||
|
||||
public Collection<ForumPostHeader> getHeaders() {
|
||||
return tree.depthFirstOrder();
|
||||
}
|
||||
|
||||
public void addBody(MessageId messageId, byte[] body) {
|
||||
bodyCache.put(messageId, body);
|
||||
}
|
||||
|
||||
public byte[] getBody(MessageId messageId) {
|
||||
return bodyCache.get(messageId);
|
||||
}
|
||||
|
||||
public LocalAuthor getLocalAuthor() {
|
||||
return localAuthor;
|
||||
}
|
||||
|
||||
public void setLocalAuthor(
|
||||
LocalAuthor localAuthor) {
|
||||
this.localAuthor = localAuthor;
|
||||
}
|
||||
|
||||
public Forum getForum() {
|
||||
return forum;
|
||||
}
|
||||
|
||||
public void setForum(Forum forum) {
|
||||
this.forum = forum;
|
||||
}
|
||||
|
||||
public GroupId getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(GroupId groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package org.briarproject.android.forum;
|
||||
|
||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||
import org.briarproject.api.UniqueId;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class ForumTestControllerImpl implements ForumController {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ForumControllerImpl.class.getName());
|
||||
|
||||
private final static String[] AUTHORS = {
|
||||
"Guðmundur",
|
||||
"Jónas",
|
||||
"Geir Þorsteinn Gísli Máni Halldórsson Guðjónsson Mogensen",
|
||||
"Baldur Friðrik",
|
||||
"Anna Katrín",
|
||||
"Þór",
|
||||
"Anna Þorbjörg",
|
||||
"Guðrún",
|
||||
"Helga",
|
||||
"Haraldur"
|
||||
};
|
||||
|
||||
private final static AuthorId[] AUTHOR_ID = new AuthorId[AUTHORS.length];
|
||||
|
||||
static {
|
||||
SecureRandom random = new SecureRandom();
|
||||
for (int i = 0; i < AUTHOR_ID.length; i++) {
|
||||
byte[] b = new byte[UniqueId.LENGTH];
|
||||
random.nextBytes(b);
|
||||
AUTHOR_ID[i] = new AuthorId(b);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private final static String SAGA =
|
||||
"Það er upphaf á sögu þessari að Hákon konungur " +
|
||||
"Aðalsteinsfóstri réð fyrir Noregi og var þetta á ofanverðum " +
|
||||
"hans dögum. Þorkell hét maður; hann var kallaður skerauki; " +
|
||||
"hann bjó í Súrnadal og var hersir að nafnbót. Hann átti sér " +
|
||||
"konu er Ísgerður hét og sonu þrjá barna; hét einn Ari, annar " +
|
||||
"Gísli, þriðji Þorbjörn, hann var þeirra yngstur, og uxu allir " +
|
||||
"upp heima þar. " +
|
||||
"Maður er nefndur Ísi; hann bjó í firði er Fibuli heitir á " +
|
||||
"Norðmæri; kona hans hét Ingigerður en Ingibjörg dóttir. Ari, " +
|
||||
"sonur Þorkels Sýrdæls, biður hennar og var hún honum gefin " +
|
||||
"með miklu fé. Kolur hét þræll er í brott fór með henni.";
|
||||
|
||||
private ForumEntry[] forumEntries;
|
||||
|
||||
@Inject
|
||||
public ForumTestControllerImpl() {
|
||||
|
||||
}
|
||||
|
||||
private void textRandomize(SecureRandom random, int[] i) {
|
||||
for (int e = 0; e < forumEntries.length; e++) {
|
||||
// select a random white-space for the cut-off
|
||||
do {
|
||||
i[e] = Math.abs(random.nextInt() % (SAGA.length()));
|
||||
} while (SAGA.charAt(i[e]) != ' ');
|
||||
}
|
||||
}
|
||||
|
||||
private int levelRandomize(SecureRandom random, int[] l) {
|
||||
int maxl = 0;
|
||||
int lastl = 0;
|
||||
l[0] = 0;
|
||||
for (int e = 1; e < forumEntries.length; e++) {
|
||||
// select random level 1-10
|
||||
do {
|
||||
l[e] = Math.abs(random.nextInt() % 10);
|
||||
} while (l[e] > lastl + 1);
|
||||
lastl = l[e];
|
||||
if (lastl > maxl)
|
||||
maxl = lastl;
|
||||
}
|
||||
return maxl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadForum(GroupId groupId,
|
||||
UiResultHandler<Boolean> resultHandler) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
forumEntries = new ForumEntry[100];
|
||||
// string cut off index
|
||||
int[] i = new int[forumEntries.length];
|
||||
// entry discussion level
|
||||
int[] l = new int[forumEntries.length];
|
||||
|
||||
textRandomize(random, i);
|
||||
int maxLevel;
|
||||
// make sure we get a deep discussion
|
||||
do {
|
||||
maxLevel = levelRandomize(random, l);
|
||||
} while (maxLevel < 6);
|
||||
for (int e = 0; e < forumEntries.length; e++) {
|
||||
int authorIndex = Math.abs(random.nextInt() % AUTHORS.length);
|
||||
long timestamp =
|
||||
System.currentTimeMillis() - Math.abs(random.nextInt());
|
||||
byte[] b = new byte[UniqueId.LENGTH];
|
||||
random.nextBytes(b);
|
||||
forumEntries[e] =
|
||||
new ForumEntry(new MessageId(b), SAGA.substring(0, i[e]),
|
||||
l[e], timestamp, AUTHORS[authorIndex],
|
||||
AUTHOR_ID[authorIndex]);
|
||||
}
|
||||
LOG.info("forum entries: " + forumEntries.length);
|
||||
resultHandler.onResult(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getForumName() {
|
||||
return "SAGA";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ForumEntry> getForumEntries() {
|
||||
return forumEntries == null ? null :
|
||||
new ArrayList<ForumEntry>(Arrays.asList(forumEntries));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(UiResultHandler<Boolean> resultHandler) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entryRead(ForumEntry forumEntry) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entriesRead(Collection<ForumEntry> messageIds) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPost(byte[] body) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPost(byte[] body, MessageId parentId) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreate() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResume() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPause() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroy() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.briarproject.android.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Build;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
@@ -18,6 +19,7 @@ public class BriarRecyclerView extends FrameLayout {
|
||||
private TextView emptyView;
|
||||
private ProgressBar progressBar;
|
||||
private RecyclerView.AdapterDataObserver emptyObserver;
|
||||
private boolean isScrollingToEnd = false;
|
||||
|
||||
public BriarRecyclerView(Context context) {
|
||||
super(context);
|
||||
@@ -25,6 +27,11 @@ public class BriarRecyclerView extends FrameLayout {
|
||||
|
||||
public BriarRecyclerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
TypedArray attributes = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.BriarRecyclerView);
|
||||
isScrollingToEnd = attributes
|
||||
.getBoolean(R.styleable.BriarRecyclerView_scrollToEnd, true);
|
||||
}
|
||||
|
||||
public BriarRecyclerView(Context context, AttributeSet attrs,
|
||||
@@ -44,7 +51,7 @@ public class BriarRecyclerView extends FrameLayout {
|
||||
showProgressBar();
|
||||
|
||||
// scroll down when opening keyboard
|
||||
if (Build.VERSION.SDK_INT >= 11) {
|
||||
if (isScrollingToEnd && Build.VERSION.SDK_INT >= 11) {
|
||||
recyclerView.addOnLayoutChangeListener(
|
||||
new View.OnLayoutChangeListener() {
|
||||
@Override
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package org.briarproject.android.util;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.briarproject.android.controller.handler.ResultHandler;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.MeasureSpec.UNSPECIFIED;
|
||||
import static android.view.View.VISIBLE;
|
||||
@@ -21,6 +26,49 @@ public class CustomAnimations {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public static void animateColorTransition(final View view, int color,
|
||||
int duration, final ResultHandler<Void> finishedCallback) {
|
||||
// No soup for Gingerbread
|
||||
if (Build.VERSION.SDK_INT < 11) {
|
||||
return;
|
||||
}
|
||||
ValueAnimator anim = new ValueAnimator();
|
||||
ColorDrawable viewColor = (ColorDrawable) view.getBackground();
|
||||
anim.setIntValues(viewColor.getColor(), color);
|
||||
anim.setEvaluator(new ArgbEvaluator());
|
||||
anim.addListener(new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (finishedCallback != null) finishedCallback.onResult(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {
|
||||
|
||||
}
|
||||
});
|
||||
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||
view.setBackgroundColor((Integer)valueAnimator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
anim.setDuration(duration);
|
||||
|
||||
anim.start();
|
||||
}
|
||||
|
||||
private static void animateHeightGingerbread(ViewGroup viewGroup,
|
||||
boolean isExtending) {
|
||||
// No animations for Gingerbread
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package briarproject.activity;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import junit.framework.Assert;
|
||||
|
||||
import org.briarproject.BuildConfig;
|
||||
import org.briarproject.TestUtils;
|
||||
import org.briarproject.android.controller.handler.UiResultHandler;
|
||||
import org.briarproject.android.forum.ForumActivity;
|
||||
import org.briarproject.android.forum.ForumController;
|
||||
import org.briarproject.android.forum.ForumEntry;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricGradleTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@RunWith(RobolectricGradleTestRunner.class)
|
||||
@Config(constants = BuildConfig.class, sdk = 21,
|
||||
application = TestBriarApplication.class)
|
||||
public class ForumActivityTest {
|
||||
|
||||
private final static String AUTHOR_1 = "Author 1";
|
||||
private final static String AUTHOR_2 = "Author 2";
|
||||
private final static String AUTHOR_3 = "Author 3";
|
||||
private final static String AUTHOR_4 = "Author 4";
|
||||
private final static String AUTHOR_5 = "Author 5";
|
||||
private final static String AUTHOR_6 = "Author 6";
|
||||
|
||||
private final static String[] AUTHORS = {
|
||||
AUTHOR_1, AUTHOR_2, AUTHOR_3, AUTHOR_4, AUTHOR_5, AUTHOR_6
|
||||
};
|
||||
|
||||
/*
|
||||
1
|
||||
-> 2
|
||||
-> 3
|
||||
-> 4
|
||||
5
|
||||
6
|
||||
*/
|
||||
private final static int[] LEVELS = {
|
||||
0, 1, 2, 3, 1, 0
|
||||
};
|
||||
|
||||
private TestForumActivity forumActivity;
|
||||
@Captor
|
||||
private ArgumentCaptor<UiResultHandler<Boolean>> rc;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra("briar.GROUP_ID", TestUtils.getRandomId());
|
||||
forumActivity = Robolectric.buildActivity(TestForumActivity.class)
|
||||
.withIntent(intent).create().resume().get();
|
||||
}
|
||||
|
||||
|
||||
private List<ForumEntry> getDummyData() {
|
||||
ForumEntry[] forumEntries = new ForumEntry[6];
|
||||
for (int i = 0; i < forumEntries.length; i++) {
|
||||
forumEntries[i] =
|
||||
new ForumEntry(new MessageId(TestUtils.getRandomId()),
|
||||
AUTHORS[i], LEVELS[i], System.currentTimeMillis(),
|
||||
AUTHORS[i], new AuthorId(TestUtils.getRandomId()));
|
||||
}
|
||||
return new ArrayList<ForumEntry>(Arrays.asList(forumEntries));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNestedEntries() {
|
||||
ForumController mc = forumActivity.getController();
|
||||
List<ForumEntry> dummyData = getDummyData();
|
||||
Mockito.when(mc.getForumEntries()).thenReturn(dummyData);
|
||||
// Verify that the forum load is called once
|
||||
verify(mc, times(1))
|
||||
.loadForum(Mockito.any(GroupId.class), rc.capture());
|
||||
rc.getValue().onResult(true);
|
||||
verify(mc, times(1)).getForumEntries();
|
||||
ForumActivity.ForumAdapter adapter = forumActivity.getAdapter();
|
||||
Assert.assertNotNull(adapter);
|
||||
// Cascade close
|
||||
assertEquals(6, adapter.getItemCount());
|
||||
adapter.hideDescendants(dummyData.get(2));
|
||||
assertEquals(5, adapter.getItemCount());
|
||||
adapter.hideDescendants(dummyData.get(1));
|
||||
assertEquals(4, adapter.getItemCount());
|
||||
adapter.hideDescendants(dummyData.get(0));
|
||||
assertEquals(2, adapter.getItemCount());
|
||||
assertTrue(dummyData.get(0).getText()
|
||||
.equals(adapter.getVisibleEntry(0).getText()));
|
||||
assertTrue(dummyData.get(5).getText()
|
||||
.equals(adapter.getVisibleEntry(1).getText()));
|
||||
// Cascade re-open
|
||||
adapter.showDescendants(dummyData.get(0));
|
||||
assertEquals(4, adapter.getItemCount());
|
||||
adapter.showDescendants(dummyData.get(1));
|
||||
assertEquals(5, adapter.getItemCount());
|
||||
adapter.showDescendants(dummyData.get(2));
|
||||
assertEquals(6, adapter.getItemCount());
|
||||
assertTrue(dummyData.get(2).getText()
|
||||
.equals(adapter.getVisibleEntry(2).getText()));
|
||||
assertTrue(dummyData.get(4).getText()
|
||||
.equals(adapter.getVisibleEntry(4).getText()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package briarproject.activity;
|
||||
|
||||
import org.briarproject.android.ActivityModule;
|
||||
import org.briarproject.android.controller.BriarController;
|
||||
import org.briarproject.android.controller.BriarControllerImpl;
|
||||
import org.briarproject.android.forum.ForumActivity;
|
||||
import org.briarproject.android.forum.ForumController;
|
||||
import org.briarproject.android.forum.ForumControllerImpl;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
/**
|
||||
* This class exposes the SetupController and offers the possibility to
|
||||
* override it.
|
||||
*/
|
||||
public class TestForumActivity extends ForumActivity {
|
||||
|
||||
public ForumController getController() {
|
||||
return forumController;
|
||||
}
|
||||
|
||||
public ForumAdapter getAdapter() {
|
||||
return forumAdapter;
|
||||
}
|
||||
|
||||
protected ActivityModule getActivityModule() {
|
||||
return new ActivityModule(this) {
|
||||
@Override
|
||||
protected BriarController provideBriarController(
|
||||
BriarControllerImpl briarControllerImpl) {
|
||||
BriarController c = Mockito.mock(BriarController.class);
|
||||
Mockito.when(c.hasEncryptionKey()).thenReturn(true);
|
||||
return c;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ForumController provideForumController(
|
||||
ForumControllerImpl forumController) {
|
||||
return Mockito.mock(ForumController.class);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
||||
implements MessageTree<T> {
|
||||
|
||||
@@ -26,11 +24,6 @@ public class MessageTreeImpl<T extends MessageTree.MessageNode>
|
||||
}
|
||||
};
|
||||
|
||||
@Inject
|
||||
public MessageTreeImpl() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
roots.clear();
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.briarproject.forum;
|
||||
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.MessageQueueManager;
|
||||
import org.briarproject.api.clients.MessageTree;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
@@ -10,14 +9,12 @@ import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.forum.ForumFactory;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumPostFactory;
|
||||
import org.briarproject.api.forum.ForumPostHeader;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.sync.GroupFactory;
|
||||
import org.briarproject.api.sync.ValidationManager;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.clients.MessageTreeImpl;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
@@ -104,10 +101,4 @@ public class ForumModule {
|
||||
return forumSharingManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MessageTree<ForumPostHeader> provideForumMessageTree(
|
||||
MessageTreeImpl<ForumPostHeader> messageTree) {
|
||||
return messageTree;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user