diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 025e452b2a2243482c0816a33547ce75269767eb..43231803db60450db720e7af84c60e3d4c6a4d2d 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -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" diff --git a/briar-android/res/drawable/chevron48dp_down.xml b/briar-android/res/drawable/chevron48dp_down.xml new file mode 100644 index 0000000000000000000000000000000000000000..16dd3cd9d7093f9118d7439825363074e6705895 --- /dev/null +++ b/briar-android/res/drawable/chevron48dp_down.xml @@ -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> diff --git a/briar-android/res/drawable/chevron48dp_up.xml b/briar-android/res/drawable/chevron48dp_up.xml new file mode 100644 index 0000000000000000000000000000000000000000..7d59523a92f4030ca1747b1c3941f60e152816a2 --- /dev/null +++ b/briar-android/res/drawable/chevron48dp_up.xml @@ -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> diff --git a/briar-android/res/drawable/level_indicator_circle.xml b/briar-android/res/drawable/level_indicator_circle.xml new file mode 100644 index 0000000000000000000000000000000000000000..22fc407d3a46ed4d87b0f83431e84f6782e7ef1f --- /dev/null +++ b/briar-android/res/drawable/level_indicator_circle.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/drawable/selector_chevron.xml b/briar-android/res/drawable/selector_chevron.xml new file mode 100644 index 0000000000000000000000000000000000000000..bc08694ba67a833441046c7841bb7b18db5f7f16 --- /dev/null +++ b/briar-android/res/drawable/selector_chevron.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/activity_forum.xml b/briar-android/res/layout/activity_forum.xml new file mode 100644 index 0000000000000000000000000000000000000000..946ce34e8a3ac99193729a6f6354ab540935b1ca --- /dev/null +++ b/briar-android/res/layout/activity_forum.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/forum_discussion_cell.xml b/briar-android/res/layout/forum_discussion_cell.xml new file mode 100644 index 0000000000000000000000000000000000000000..57383a720623ffc952528c8ab2eadabe2d6b5ae1 --- /dev/null +++ b/briar-android/res/layout/forum_discussion_cell.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/text_input_field.xml b/briar-android/res/layout/text_input_field.xml new file mode 100644 index 0000000000000000000000000000000000000000..c59e93c7ebcd3d7e15f8d1fda849b2319f13f4ce --- /dev/null +++ b/briar-android/res/layout/text_input_field.xml @@ -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> diff --git a/briar-android/res/values/attrs.xml b/briar-android/res/values/attrs.xml new file mode 100644 index 0000000000000000000000000000000000000000..380379e9e6b9a8435453b2db254182ebd06c1fbc --- /dev/null +++ b/briar-android/res/values/attrs.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <declare-styleable name="BriarRecyclerView"> + <attr name="scrollToEnd" format="boolean" /> + </declare-styleable> + +</resources> \ No newline at end of file diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml index 3fb50397850e50e9d086b0b5f65a72353f1a131d..14a0938bb09091ae58a05b39bca66308383198b4 100644 --- a/briar-android/res/values/color.xml +++ b/briar-android/res/values/color.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml index 5ca081c5687a1cf70a1568ac36609a73ddebbf0e..942a2d5bfa535971982113e90be7fe38970cb91e 100644 --- a/briar-android/res/values/dimens.xml +++ b/briar-android/res/values/dimens.xml @@ -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> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index 0e01dde61aa790bafc32222181600420ae1f9018..1e1e8e28c510d4217a6d5bf90606c1df58974223 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -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> diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml index abb3e2457e35dc5cc6bee7f633b07a5a24b27235..38f609e83c61ef764ac06e59a6d488fbe5f05850 100644 --- a/briar-android/res/values/styles.xml +++ b/briar-android/res/values/styles.xml @@ -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> \ No newline at end of file diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index b777e38a43bebfc04627891e0b31d2cf9aec44ce..f28335cd8aa86d83163479e534f834067e20554d 100644 --- a/briar-android/src/org/briarproject/android/ActivityComponent.java +++ b/briar-android/src/org/briarproject/android/ActivityComponent.java @@ -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); diff --git a/briar-android/src/org/briarproject/android/ActivityModule.java b/briar-android/src/org/briarproject/android/ActivityModule.java index 0e13fbd0d7aa59311bf86b0b664a8afd6bfc5f46..6766f38022bfaf9e489cbadc9e17022683cc0545 100644 --- a/briar-android/src/org/briarproject/android/ActivityModule.java +++ b/briar-android/src/org/briarproject/android/ActivityModule.java @@ -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( diff --git a/briar-android/src/org/briarproject/android/AppModule.java b/briar-android/src/org/briarproject/android/AppModule.java index 68d37071af3aa1476aab1596b89943180e2aa8ca..ed2ef27244176ced083913bb702237a6b1b07e2c 100644 --- a/briar-android/src/org/briarproject/android/AppModule.java +++ b/briar-android/src/org/briarproject/android/AppModule.java @@ -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; + } } diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index cfa26e8c8adf0b7e0fd68a6ec1ed4cbb4a185eba..b99cb9f4733ae8c8a6e8c3799ab58b001d4fe45a 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -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 { + + private static final Logger LOG = + Logger.getLogger(ForumActivity.class.getName()); 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; + @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,263 +200,406 @@ 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); - } - } - }); + @Override + public void onResume() { + super.onResume(); + notificationManager.blockNotification(groupId); + notificationManager.clearForumPostNotification(groupId); } - private void displayForumName() { - runOnUiThread(new Runnable() { - public void run() { - setTitle(forum.getName()); - } - }); + @Override + public void onPause() { + super.onPause(); + notificationManager.unblockNotification(groupId); } - 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); - } - } - }); + 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()); + } + hideSoftKeyboard(textInput); + inputContainer.setVisibility(GONE); + forumAdapter.setReplyEntry(null); } - 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); + private void showUnsubscribeDialog() { + DialogInterface.OnClickListener okListener = + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + 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(); + } + } + }); } - adapter.sort(ForumItemComparator.INSTANCE); - // Scroll to the bottom - list.setSelection(adapter.getCount() - 1); - } - } - }); + }; + AlertDialog.Builder builder = + new AlertDialog.Builder(ForumActivity.this, + R.style.BriarDialogTheme); + builder.setTitle(getString(R.string.dialog_title_leave_forum)); + builder.setMessage(getString(R.string.dialog_message_leave_forum)); + builder.setPositiveButton(R.string.dialog_button_leave, okListener); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); } - private void 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); - } + @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); + } } - 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; + 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; + } - @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); + 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; } - 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(); + + 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; } - } - @Override - public void onPause() { - super.onPause(); - eventBus.removeListener(this); - notificationManager.unblockNotification(groupId); - if (isFinishing()) markPostsRead(); - } + 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; + } - 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 setReplyEntry(ForumEntry entry) { + if (replyEntry != null) { + notifyItemChanged(getVisiblePos(replyEntry)); + } + replyEntry = entry; + if (replyEntry != null) { + notifyItemChanged(getVisiblePos(replyEntry)); + } } - 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); + 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 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(); + 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()); + } } - } else if (e instanceof GroupRemovedEvent) { - GroupRemovedEvent s = (GroupRemovedEvent) e; - if (s.getGroup().getId().equals(groupId)) { - LOG.info("Forum removed"); - finishOnUiThread(); + } + + 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); } - } - 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; + 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; } - return timestamp + 1; - } - public void onItemClick(AdapterView<?> parent, View view, int position, - long id) { - displayPost(position); - } + @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; + } - 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()); + @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); } - i.putExtra("briar.AUTHOR_STATUS", header.getAuthorStatus().name()); - i.putExtra("briar.CONTENT_TYPE", header.getContentType()); - i.putExtra("briar.TIMESTAMP", header.getTimestamp()); - i.putExtra(MIN_TIMESTAMP, getMinTimestampForNewPost()); - i.putExtra("briar.POSITION", position); - startActivityForResult(i, REQUEST_READ); - } - private void showUnsubscribeDialog() { - DialogInterface.OnClickListener okListener = - new DialogInterface.OnClickListener() { + @Override + public void 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(DialogInterface dialog, int which) { - unsubscribe(forum); - Toast.makeText(ForumActivity.this, - R.string.forum_left_toast, LENGTH_SHORT) - .show(); + public void onClick(View v) { + ui.chevron.setSelected(!ui.chevron.isSelected()); + if (ui.chevron.isSelected()) { + hideDescendants(data); + } else { + showDescendants(data); + } } - }; - AlertDialog.Builder builder = - new AlertDialog.Builder(ForumActivity.this, - R.style.BriarDialogTheme); - builder.setTitle(getString(R.string.dialog_title_leave_forum)); - builder.setMessage(getString(R.string.dialog_message_leave_forum)); - builder.setPositiveButton(R.string.dialog_button_leave, okListener); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } + }); + } 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); + } + }); + } - 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 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; + } } + } diff --git a/briar-android/src/org/briarproject/android/forum/ForumController.java b/briar-android/src/org/briarproject/android/forum/ForumController.java new file mode 100644 index 0000000000000000000000000000000000000000..2fa4f0dab0c58cbcd7017f717a44468f97bf7317 --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/ForumController.java @@ -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); + } + +} diff --git a/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..676f2d51d0c977d41bcd2cc09ab5fbcb7bfeacfd --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/ForumControllerImpl.java @@ -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); + } + } + }); + } + +} diff --git a/briar-android/src/org/briarproject/android/forum/ForumEntry.java b/briar-android/src/org/briarproject/android/forum/ForumEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..c3c668f7a052d46c0cadefd3e05e1d524c960fd2 --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/ForumEntry.java @@ -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; + } +} diff --git a/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java b/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java new file mode 100644 index 0000000000000000000000000000000000000000..630c371aeadc7bd75034c550f4aaaeddf5d520bd --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/ForumPersistentData.java @@ -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; + } +} diff --git a/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..ec1470ed85ceeaa6a34436bd6b5b0cc1a1c3de5d --- /dev/null +++ b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java @@ -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() { + + } +} diff --git a/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java b/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java deleted file mode 100644 index cfc80ace2a4a4363327a4c88b3115bdc6cecbf64..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/forum/ReadForumPostActivity.java +++ /dev/null @@ -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(); - } - } -} diff --git a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java b/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java deleted file mode 100644 index d2185aa4630df3d1dcd024fd322a6c4bcdd154dd..0000000000000000000000000000000000000000 --- a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java +++ /dev/null @@ -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); - } - } - }); - } -} diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java index 93eeb669c177492cbb203c61c3c0448e7fbf0907..e399ecda03ff693693a86c8cfa30b2c6e11bf3dc 100644 --- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java +++ b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java @@ -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 diff --git a/briar-android/src/org/briarproject/android/util/CustomAnimations.java b/briar-android/src/org/briarproject/android/util/CustomAnimations.java index 07dec13243138a1cec3ea150b6d84e4e85f66868..6705194a4f4ee42233f1d4097df0dbcb64e79fbd 100644 --- a/briar-android/src/org/briarproject/android/util/CustomAnimations.java +++ b/briar-android/src/org/briarproject/android/util/CustomAnimations.java @@ -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 diff --git a/briar-android/test/java/briarproject/activity/ForumActivityTest.java b/briar-android/test/java/briarproject/activity/ForumActivityTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ee044b112a25337de5cf1d9416264a688facdc55 --- /dev/null +++ b/briar-android/test/java/briarproject/activity/ForumActivityTest.java @@ -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())); + } +} diff --git a/briar-android/test/java/briarproject/activity/TestForumActivity.java b/briar-android/test/java/briarproject/activity/TestForumActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..f73d83e18f66d12e8b549b9ba5584210c90a76b2 --- /dev/null +++ b/briar-android/test/java/briarproject/activity/TestForumActivity.java @@ -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); + } + }; + } +} diff --git a/briar-core/src/org/briarproject/clients/MessageTreeImpl.java b/briar-core/src/org/briarproject/clients/MessageTreeImpl.java index 55f69cf54008adbcb57e57788a24afe25f0284d7..4759f39fdb9ec291ef08f9912b6048caba7067c6 100644 --- a/briar-core/src/org/briarproject/clients/MessageTreeImpl.java +++ b/briar-core/src/org/briarproject/clients/MessageTreeImpl.java @@ -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(); diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java index 04d934a21ccccaabdae511448eee6d6fa55d5d4d..e7494850e2f95c88aa851ec6abc60d8d4ad770f5 100644 --- a/briar-core/src/org/briarproject/forum/ForumModule.java +++ b/briar-core/src/org/briarproject/forum/ForumModule.java @@ -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; - } }