Merge branch '253-introduction-ui' into 'master'

Introduction UI

This is the user interface for the new introduction feature as specified by @Megalox in #253.

This will close #253 

See merge request !122
This commit is contained in:
akwizgran
2016-04-13 16:54:04 +00:00
77 changed files with 2536 additions and 333 deletions

View File

@@ -90,6 +90,7 @@
<activity <activity
android:name=".android.contact.ConversationActivity" android:name=".android.contact.ConversationActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/BriarThemeNoActionBar.Default"
android:parentActivityName=".android.NavDrawerActivity" android:parentActivityName=".android.NavDrawerActivity"
android:windowSoftInputMode="stateHidden"> android:windowSoftInputMode="stateHidden">
<meta-data <meta-data
@@ -174,7 +175,16 @@
android:parentActivityName=".android.NavDrawerActivity"> android:parentActivityName=".android.NavDrawerActivity">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".android.NavDrawerActivity" android:value=".android.NavDrawerActivity"/>
</activity>
<activity
android:name=".android.introduction.IntroductionActivity"
android:label="@string/introduction_activity_title"
android:parentActivityName=".android.contact.ConversationActivity"
android:windowSoftInputMode="stateHidden|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".android.contact.ConversationActivity"
/> />
</activity> </activity>
<activity <activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 957 B

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 B

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A FAB does not work, because even with fabSize="mini" it will be too big due to shadow drawing
on lower API levels
-->
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/briar_primary_dark">
<item>
<shape android:shape="oval">
<solid android:color="@color/briar_primary"/>
</shape>
</item>
</ripple>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#2D3E50"
android:pathData="M10.8972,19.9503 C6.5514,19.3493,3.43091,15.2154,4.0625,10.896
C4.55452,7.53099,7.09451,4.8236,10.394,4.14714
C14.2569,3.35517,18.1698,5.54347,19.5236,9.25295
C20.0698,10.7495,20.1616,12.4612,19.777,13.9758
C19.5457,14.8864,18.8106,16.3388,18.2072,17.0771
C16.4904,19.1779,13.581,20.3215,10.8973,19.9503 Z"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="1"/>
</vector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="#95D220"
android:pathData="M10.8972,19.9503 C6.5514,19.3493,3.43091,15.2154,4.0625,10.896
C4.55452,7.53099,7.09451,4.8236,10.394,4.14714
C14.2569,3.35517,18.1698,5.54347,19.5236,9.25295
C20.0698,10.7495,20.1616,12.4612,19.777,13.9758
C19.5457,14.8864,18.8106,16.3388,18.2072,17.0771
C16.4904,19.1779,13.581,20.3215,10.8973,19.9503 Z"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="1.5"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:alpha="0.56" android:height="48dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M9.01,14L2,14v2h7.01v3L13,15l-3.99,-4v3zM14.99,13v-3L22,10L22,8h-7.01L14.99,5L11,9l3.99,4z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M21,8V7l-3,2 -3,-2v1l3,2 3,-2zm1,-5H2C0.9,3 0,3.9 0,5v14c0,1.1 0.9,2 2,2h20c1.1,0 1.99,-0.9 1.99,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM8,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zm6,12H2v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1zm8,-6h-8V6h8v6z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M21,8V7l-3,2 -3,-2v1l3,2 3,-2zm1,-5H2C0.9,3 0,3.9 0,5v14c0,1.1 0.9,2 2,2h20c1.1,0 1.99,-0.9 1.99,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM8,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zm6,12H2v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1zm8,-6h-8V6h8v6z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="16dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zm4.24,-1.41L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="16dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="16dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha=".9" android:fillColor="#FFFFFF" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/>
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A FAB does not work, because even with fabSize="mini" it will be too big due to shadow drawing
on lower API levels
-->
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@color/briar_primary"/>
</shape>
</item>
</selector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>

View File

@@ -7,7 +7,6 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<org.briarproject.android.util.BriarRecyclerView <org.briarproject.android.util.BriarRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/contactList" android:id="@+id/contactList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>

View File

@@ -1,46 +1,79 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" 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:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
tools:context=".android.contact.ConversationActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
style="@style/BriarToolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<include layout="@layout/contact_avatar_status"/>
<TextView
android:id="@+id/contactName"
style="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_marginStart="@dimen/margin_medium"
android:gravity="center"
tools:text="Contact Name"/>
</LinearLayout>
</android.support.v7.widget.Toolbar>
<org.briarproject.android.util.BriarRecyclerView <org.briarproject.android.util.BriarRecyclerView
android:id="@+id/conversationView" android:id="@+id/conversationView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1"/> android:layout_weight="1"
android:background="@color/conversation_background"/>
<View style="@style/Divider.Horizontal"/> <View style="@style/Divider.Horizontal"/>
<LinearLayout <LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/button_bar_background" android:background="@color/button_bar_background"
android:orientation="horizontal"
android:paddingLeft="@dimen/margin_medium" android:paddingLeft="@dimen/margin_medium"
android:paddingStart="@dimen/margin_medium"> android:paddingStart="@dimen/margin_medium">
<EditText <EditText
android:id="@+id/contentView" android:id="@+id/contentView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:hint="@string/private_message_hint"
android:layout_weight="1" android:layout_weight="1"
android:hint="@string/private_message_hint"
android:inputType="text|textMultiLine|textCapSentences"/> android:inputType="text|textMultiLine|textCapSentences"/>
<ImageButton <ImageButton
android:id="@+id/sendButton" android:id="@+id/sendButton"
android:layout_width="38dp" android:layout_width="38dp"
android:layout_height="38dp" android:layout_height="38dp"
android:layout_gravity="bottom" android:layout_margin="@dimen/margin_small"
android:src="@drawable/social_send_now" android:background="@drawable/round_button"
android:background="?attr/selectableItemBackground" android:src="@drawable/social_send_now_white"
android:scaleType="fitEnd"
android:contentDescription="@string/send" android:contentDescription="@string/send"
android:paddingRight="@dimen/margin_medium" android:elevation="@dimen/margin_tiny"
android:paddingEnd="@dimen/margin_medium" />
android:paddingBottom="@dimen/margin_medium"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

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

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="32dp"
android:layout_height="32dp"
tools:showIn="@layout/activity_conversation">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/contactAvatar"
android:layout_width="30dp"
android:layout_height="30dp"
android:transitionName="avatar"
app:civ_border_color="@color/action_bar_text"
app:civ_border_width="@dimen/avatar_border_width"
tools:src="@drawable/ic_launcher"/>
<ImageView
android:id="@+id/contactStatus"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_gravity="bottom|right"
android:scaleType="fitCenter"
tools:src="@drawable/contact_online"
tools:ignore="ContentDescription"/>
</FrameLayout>

View File

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

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/margin_activity_horizontal"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/introductionHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/margin_medium">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarContact1"
android:layout_width="@dimen/listitem_picture_size"
android:layout_height="@dimen/listitem_picture_size"
android:layout_centerHorizontal="true"
android:layout_marginEnd="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:layout_toLeftOf="@+id/introductionIcon"
android:layout_toStartOf="@+id/introductionIcon"
app:civ_border_color="@color/briar_text_primary"
app:civ_border_width="@dimen/avatar_border_width"
tools:src="@drawable/ic_launcher"/>
<ImageView
android:id="@+id/introductionIcon"
android:layout_width="@dimen/listitem_picture_size"
android:layout_height="@dimen/listitem_picture_size"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_contact_introduction"
tools:ignore="ContentDescription"/>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarContact2"
android:layout_width="@dimen/listitem_picture_size"
android:layout_height="@dimen/listitem_picture_size"
android:layout_centerHorizontal="true"
android:layout_marginLeft="@dimen/listitem_horizontal_margin"
android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:layout_toEndOf="@+id/introductionIcon"
android:layout_toRightOf="@+id/introductionIcon"
android:transitionName="avatar"
app:civ_border_color="@color/briar_text_primary"
app:civ_border_width="@dimen/avatar_border_width"
tools:src="@drawable/ic_launcher"/>
</RelativeLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
tools:visibility="gone"/>
<TextView
android:id="@+id/introductionText"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/margin_medium"
android:layout_weight="1"
android:gravity="top"
android:textSize="@dimen/text_size_medium"
tools:text="@string/introduction_message_text"/>
<EditText
android:id="@+id/introductionMessageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_medium"
android:gravity="bottom"
android:hint="@string/introduction_message_hint"
android:inputType="text|textMultiLine|textCapSentences"/>
<Button
android:id="@+id/makeIntroductionButton"
style="@style/BriarButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/introduction_button"
/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>

View File

@@ -1,16 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/listitem_height_one_line_avatar" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"> android:paddingTop="@dimen/listitem_horizontal_margin"
android:paddingBottom="@dimen/listitem_horizontal_margin"
android:background="?attr/selectableItemBackground"
>
<de.hdodenhof.circleimageview.CircleImageView <de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/avatarView" android:id="@+id/avatarView"
@@ -21,56 +24,62 @@
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginLeft="@dimen/listitem_horizontal_margin" android:layout_marginLeft="@dimen/listitem_horizontal_margin"
android:layout_marginStart="@dimen/listitem_horizontal_margin" android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:transitionName="avatar"
app:civ_border_color="@color/briar_text_primary"
app:civ_border_width="@dimen/avatar_border_width" app:civ_border_width="@dimen/avatar_border_width"
app:civ_border_color="@color/briar_text_primary"/> tools:src="@drawable/ic_launcher"/>
<LinearLayout <LinearLayout
android:id="@+id/bulbHolder" android:id="@+id/textViews"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:orientation="vertical"
android:layout_alignParentRight="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/listitem_horizontal_margin" android:layout_marginLeft="@dimen/listitem_horizontal_margin"
android:layout_marginRight="@dimen/listitem_horizontal_margin" android:layout_marginStart="@dimen/listitem_horizontal_margin"
android:gravity="right" android:layout_toLeftOf="@+id/bulbView"
android:orientation="vertical"> android:layout_toRightOf="@+id/avatarView"
android:layout_toEndOf="@+id/avatarView">
<ImageView <TextView
android:id="@+id/bulbView" android:id="@+id/nameView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:src="@drawable/contact_disconnected"/> android:maxLines="2"
android:textColor="@android:color/primary_text_light"
android:textSize="@dimen/text_size_medium"
tools:text="This is a name of a contact"/>
<TextView <TextView
android:id="@+id/dateView" android:id="@+id/dateView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/no_private_messages" android:textColor="@android:color/secondary_text_light"
android:textSize="@dimen/text_size_small"
tools:text="Dec 24"/> tools:text="Dec 24"/>
<TextView
android:id="@+id/identityView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/tertiary_text_light"
android:textSize="@dimen/text_size_tiny"
tools:text="My Identity"/>
</LinearLayout> </LinearLayout>
<TextView <ImageView
android:id="@+id/nameView" android:id="@+id/bulbView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentLeft="true" android:layout_alignParentEnd="true"
android:layout_alignParentStart="true" android:layout_alignParentRight="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginEnd="@dimen/margin_small" android:layout_marginRight="@dimen/listitem_horizontal_margin"
android:layout_marginLeft="@dimen/listitem_text_left_margin" tools:src="@drawable/contact_connected"/>
android:layout_marginRight="@dimen/margin_small"
android:layout_marginStart="@dimen/listitem_text_left_margin"
android:layout_toLeftOf="@id/bulbHolder"
android:layout_toStartOf="@id/bulbHolder"
android:gravity="center_vertical"
android:maxLines="2"
android:textSize="@dimen/text_size_medium"
tools:text="This is a name of a contact. It can be quite long."/>
</RelativeLayout> </RelativeLayout>
<View style="@style/Divider.Horizontal"/> <View style="@style/Divider.ContactList"/>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/messageLayout"
layout="@layout/list_item_msg_in"/>
<RelativeLayout
android:id="@+id/introductionLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|start"
android:background="@drawable/notice_in"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
<TextView
android:id="@+id/introductionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="80dp"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
tools:text="@string/introduction_request_received"/>
<TextView
android:id="@+id/introductionTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_below="@+id/acceptButton"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
<Button
android:id="@+id/acceptButton"
style="@style/BriarButtonFlat.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="-15dp"
android:layout_alignEnd="@+id/introductionText"
android:layout_alignRight="@+id/introductionText"
android:layout_below="@+id/introductionText"
android:text="@string/dialog_button_accept"/>
<Button
android:id="@+id/declineButton"
style="@style/BriarButtonFlat.Negative"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/introductionText"
android:layout_toLeftOf="@+id/acceptButton"
android:layout_toStartOf="@+id/acceptButton"
android:text="@string/dialog_button_decline"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/messageLayout"
layout="@layout/list_item_msg_out"/>
<RelativeLayout
android:id="@+id/introductionLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
android:background="@drawable/notice_out"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_tail">
<TextView
android:id="@+id/introductionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
tools:text="@string/introduction_request_received"/>
<TextView
android:id="@+id/introductionTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/introductionText"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
<ImageView
android:id="@+id/introductionStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/introductionTime"
android:layout_toRightOf="@+id/introductionTime"
android:layout_alignBottom="@+id/introductionTime"
android:layout_marginLeft="@dimen/margin_medium"
tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -1,53 +1,51 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal">
android:paddingRight="@dimen/margin_medium"
android:paddingEnd="@dimen/margin_medium"
android:paddingTop="@dimen/margin_small"
android:paddingBottom="@dimen/margin_small">
<de.hdodenhof.circleimageview.CircleImageView <de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/msgAvatar" android:id="@+id/msgAvatar"
android:layout_width="@dimen/listitem_picture_size" android:layout_width="@dimen/listitem_picture_size"
android:layout_height="@dimen/listitem_picture_size" android:layout_height="@dimen/listitem_picture_size"
android:layout_marginLeft="@dimen/listitem_horizontal_margin" android:layout_marginLeft="@dimen/margin_medium"
android:layout_marginStart="@dimen/listitem_horizontal_margin" android:layout_marginStart="@dimen/margin_medium"
android:visibility="gone"
app:civ_border_color="@color/briar_text_primary"
app:civ_border_width="@dimen/avatar_border_width" app:civ_border_width="@dimen/avatar_border_width"
app:civ_border_color="@color/briar_text_primary"/> tools:src="@drawable/ic_launcher"/>
<RelativeLayout <LinearLayout
android:id="@+id/msgLayout" android:id="@+id/msgLayout"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="left|start"
android:background="@drawable/msg_in" android:background="@drawable/msg_in"
android:paddingLeft="17dp" android:orientation="vertical"
android:paddingTop="5dp" android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:paddingRight="7dp" android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
android:paddingBottom="5dp">
<TextView <TextView
android:id="@+id/msgBody" android:id="@+id/msgBody"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="80dp"
android:textIsSelectable="true" android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
tools:text="Short message"/> tools:text="Short message"/>
<TextView <TextView
android:id="@+id/msgTime" android:id="@+id/msgTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="10sp" android:layout_gravity="right|end"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:maxLines="1"
android:textColor="@color/private_message_date" android:textColor="@color/private_message_date"
android:layout_below="@+id/msgBody" android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/> tools:text="Dec 24, 13:37"/>
</RelativeLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -4,11 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical">
android:paddingLeft="@dimen/margin_medium"
android:paddingStart="@dimen/margin_medium"
android:paddingTop="@dimen/margin_small"
android:paddingBottom="@dimen/margin_small">
<RelativeLayout <RelativeLayout
android:id="@+id/msgLayout" android:id="@+id/msgLayout"
@@ -16,28 +12,29 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="right|end" android:layout_gravity="right|end"
android:background="@drawable/msg_out" android:background="@drawable/msg_out"
android:paddingLeft="7dp" android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:paddingTop="5dp" android:layout_marginRight="@dimen/message_bubble_margin_tail">
android:paddingRight="17dp"
android:paddingBottom="5dp">
<TextView <TextView
android:id="@+id/msgBody" android:id="@+id/msgBody"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="@color/briar_text_primary_inverse"
android:textIsSelectable="true" android:textIsSelectable="true"
android:minWidth="80dp" android:textSize="@dimen/text_size_medium"
tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/> tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
<TextView <TextView
android:id="@+id/msgTime" android:id="@+id/msgTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/msgBody" android:layout_below="@+id/msgBody"
android:layout_toLeftOf="@+id/msgStatus"
android:textSize="10sp"
android:textColor="@color/private_message_date"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/private_message_date_inverse"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/> tools:text="Dec 24, 13:37"/>
<ImageView <ImageView
@@ -45,10 +42,11 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignBottom="@+id/msgTime" android:layout_alignBottom="@+id/msgTime"
android:layout_alignRight="@+id/msgBody" android:layout_marginLeft="@dimen/margin_medium"
android:layout_alignEnd="@+id/msgBody" android:layout_toEndOf="@+id/msgTime"
android:layout_marginLeft="3dp" android:layout_toRightOf="@+id/msgTime"
tools:src="@drawable/message_delivered"/> tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered_white"/>
</RelativeLayout> </RelativeLayout>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/noticeLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/notice_in"
android:orientation="vertical"
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
android:layout_marginRight="@dimen/message_bubble_margin_non_tail">
<TextView
android:id="@+id/noticeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
tools:text="@string/introduction_response_accepted_received"/>
<TextView
android:id="@+id/noticeTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:maxLines="1"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
</LinearLayout>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/noticeLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|end"
android:background="@drawable/notice_out"
android:layout_marginLeft="@dimen/message_bubble_margin_non_tail"
android:layout_marginRight="@dimen/message_bubble_margin_tail">
<TextView
android:id="@+id/noticeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textSize="@dimen/text_size_medium"
android:textStyle="italic"
tools:text="@string/introduction_response_accepted_sent"/>
<TextView
android:id="@+id/noticeTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/noticeText"
android:textColor="@color/private_message_date"
android:textSize="@dimen/text_size_tiny"
tools:text="Dec 24, 13:37"/>
<ImageView
android:id="@+id/noticeStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/noticeTime"
android:layout_marginLeft="@dimen/margin_medium"
android:layout_toEndOf="@+id/noticeTime"
android:layout_toRightOf="@+id/noticeTime"
tools:ignore="ContentDescription"
tools:src="@drawable/message_delivered"/>
</RelativeLayout>
</LinearLayout>

View File

@@ -3,10 +3,16 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_introduction"
android:icon="@drawable/introduction_white"
android:title="@string/introduction_button"
app:showAsAction="never"/>
<item <item
android:id="@+id/action_social_remove_person" android:id="@+id/action_social_remove_person"
android:icon="@drawable/social_remove_person" android:icon="@drawable/social_remove_person"
app:showAsAction="always" android:title="@string/delete_contact"
android:title="@string/delete_contact"/> app:showAsAction="never"/>
</menu> </menu>

View File

@@ -8,11 +8,13 @@
<color name="briar_red">#C1392B</color> <color name="briar_red">#C1392B</color>
<color name="window_background">#EEEEEE</color> <color name="window_background">#EEEEEE</color>
<color name="conversation_background">#efebe9</color>
<color name="action_bar_text">#FFFFFF</color> <color name="action_bar_text">#FFFFFF</color>
<color name="action_bar_background">@color/briar_blue</color> <color name="action_bar_background">@color/briar_blue</color>
<color name="button_bar_background">#FFFFFF</color> <color name="button_bar_background">#FFFFFF</color>
<color name="dashboard_background">#FFFFFF</color> <color name="dashboard_background">#FFFFFF</color>
<color name="private_message_date">#AAAAAA</color> <color name="private_message_date">#AAAAAA</color>
<color name="private_message_date_inverse">#e0e0e0</color>
<color name="unread_background">#FFFFFF</color> <color name="unread_background">#FFFFFF</color>
<color name="horizontal_border">#CCCCCC</color> <color name="horizontal_border">#CCCCCC</color>
<color name="forums_available_background">@color/briar_gold</color> <color name="forums_available_background">@color/briar_gold</color>
@@ -28,6 +30,8 @@
<color name="briar_text_link">@color/briar_green_dark</color> <color name="briar_text_link">@color/briar_green_dark</color>
<color name="briar_text_primary">@color/briar_primary</color> <color name="briar_text_primary">@color/briar_primary</color>
<color name="briar_text_primary_inverse">#ffffff</color> <color name="briar_text_primary_inverse">#ffffff</color>
<color name="briar_text_secondary">#333333</color>
<color name="briar_text_tertiary">#333333</color>
<!-- this is needed as preference_category_material layout uses this color as the text color --> <!-- this is needed as preference_category_material layout uses this color as the text color -->
<color name="preference_fallback_accent_color">@color/briar_accent</color> <color name="preference_fallback_accent_color">@color/briar_accent</color>

View File

@@ -23,8 +23,12 @@
<dimen name="listitem_horizontal_margin">16dp</dimen> <dimen name="listitem_horizontal_margin">16dp</dimen>
<dimen name="listitem_text_left_margin">72dp</dimen> <dimen name="listitem_text_left_margin">72dp</dimen>
<dimen name="listitem_height_one_line_avatar">56dp</dimen> <dimen name="listitem_height_one_line_avatar">56dp</dimen>
<dimen name="listitem_picture_size">40dp</dimen> <dimen name="listitem_picture_size">48dp</dimen>
<dimen name="dropdown_picture_size">32dp</dimen> <dimen name="dropdown_picture_size">32dp</dimen>
<dimen name="avatar_border_width">1dp</dimen> <dimen name="avatar_border_width">1dp</dimen>
<dimen name="message_bubble_margin_tail">14dp</dimen>
<dimen name="message_bubble_margin_non_tail">51dp</dimen>
<dimen name="message_bubble_timestamp_margin">15dp</dimen>
</resources> </resources>

View File

@@ -142,6 +142,27 @@
<string name="no_data">No data</string> <string name="no_data">No data</string>
<string name="unknown_app">an unknown app</string> <string name="unknown_app">an unknown app</string>
<!-- Introduction Client -->
<string name="introduction_activity_title">Select Contact</string>
<string name="introduction_message_title">Introduce Contacts</string>
<string name="introduction_message_text">You can compose a message that will be sent to %1$s and %2$s along with your introduction:</string>
<string name="introduction_message_hint">Type message (optional)</string>
<string name="introduction_button">Make Introduction</string>
<string name="introduction_error">There was an error making the introduction.</string>
<string name="introduction_response_error">Error when responding to introduction</string>
<string name="introduction_warn_different_identities_title">Warning: Different Identities</string>
<string name="introduction_warn_different_identities_text">You are trying to introduce two contacts that you have added with different identities. This might reveal that both identities are yours.</string>
<string name="introduction_request_sent">You have asked to introduce %1$s to %2$s.</string>
<string name="introduction_request_received">%1$s has asked to introduce you to %2$s. Do you want to add %2$s to your contact list?</string>
<string name="introduction_request_exists_received">%1$s has asked to introduce you to %2$s, but %2$s is already in your contact list. Since %1$s might not know that, you can still respond:</string>
<string name="introduction_request_answered_received">%1$s has asked to introduce you to %2$s.</string>
<string name="introduction_response_accepted_sent">You accepted the introduction to %1$s.</string>
<string name="introduction_response_declined_sent">You declined the introduction to %1$s.</string>
<string name="introduction_response_accepted_received">%1$s accepted to be introduced to %2$s.</string>
<string name="introduction_response_declined_received">%1$s declined to be introduced to %2$s.</string>
<string name="introduction_success_title">Introduced contact was added</string>
<string name="introduction_success_text">You have been introduced to %1$s.</string>
<!-- Dialogs --> <!-- Dialogs -->
<string name="dialog_title_lost_password">Lost Password</string> <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> <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>
@@ -152,6 +173,9 @@
<string name="dialog_title_welcome">Welcome to Briar</string> <string name="dialog_title_welcome">Welcome to Briar</string>
<string name="dialog_welcome_message">Add a contact to start communicating securely or press the icon in the upper left corner of the screen for more options.</string> <string name="dialog_welcome_message">Add a contact to start communicating securely or press the icon in the upper left corner of the screen for more options.</string>
<string name="dialog_button_ok">OK</string> <string name="dialog_button_ok">OK</string>
<string name="dialog_button_introduce">Introduce</string>
<string name="dialog_button_accept">Accept</string>
<string name="dialog_button_decline">Decline</string>
<!-- Toolbar headers --> <!-- Toolbar headers -->
<string name="dashboard_toolbar_header">Briar</string> <string name="dashboard_toolbar_header">Briar</string>
<string name="settings_toolbar_header">Settings</string> <string name="settings_toolbar_header">Settings</string>

View File

@@ -51,13 +51,31 @@
<item name="elevation">1dp</item> <item name="elevation">1dp</item>
</style> </style>
<style name="BriarButton"> <style name="BriarDialogTheme" parent="Theme.AppCompat.Light.Dialog">
<item name="colorPrimary">@color/briar_primary</item>
<item name="colorPrimaryDark">@color/briar_primary_dark</item>
<item name="colorAccent">@color/briar_accent</item>
</style>
<style name="BriarButton" parent="Widget.AppCompat.Button.Colored">
<item name="android:textSize">@dimen/text_size_medium</item> <item name="android:textSize">@dimen/text_size_medium</item>
<item name="android:padding">@dimen/margin_large</item> <item name="android:padding">@dimen/margin_large</item>
</style> </style>
<style name="BriarButton.Default"/> <style name="BriarButton.Default"/>
<style name="BriarButtonFlat.Negative" parent="Widget.AppCompat.Button.Borderless">
<item name="android:textColor">#ff0000</item>
<item name="android:textSize">@dimen/text_size_medium</item>
<item name="android:padding">@dimen/margin_large</item>
</style>
<style name="BriarButtonFlat.Positive" parent="Widget.AppCompat.Button.Borderless">
<item name="android:textColor">#06b9ff</item>
<item name="android:textSize">@dimen/text_size_medium</item>
<item name="android:padding">@dimen/margin_large</item>
</style>
<style name="BriarTextTitle"> <style name="BriarTextTitle">
<item name="android:textSize">@dimen/text_size_medium</item> <item name="android:textSize">@dimen/text_size_medium</item>
<item name="android:textColor">@android:color/primary_text_light</item> <item name="android:textColor">@android:color/primary_text_light</item>
@@ -76,11 +94,17 @@
<item name="android:background">?android:attr/listDivider</item> <item name="android:background">?android:attr/listDivider</item>
</style> </style>
<style name="Divider.Horizontal"> <style name="Divider.Horizontal" parent="Divider">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1px</item> <item name="android:layout_height">1px</item>
</style> </style>
<style name="Divider.ContactList" parent="Divider">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">2dp</item>
<item name="android:layout_marginLeft">@dimen/margin_large</item>
</style>
<style name="NavMenuButton" parent="Widget.AppCompat.Button.Borderless.Colored"> <style name="NavMenuButton" parent="Widget.AppCompat.Button.Borderless.Colored">
<item name="android:textSize">@dimen/text_size_medium</item> <item name="android:textSize">@dimen/text_size_medium</item>
<item name="android:textColor">@android:color/tertiary_text_light</item> <item name="android:textColor">@android:color/tertiary_text_light</item>

View File

@@ -12,6 +12,9 @@ import org.briarproject.android.forum.ReadForumPostActivity;
import org.briarproject.android.forum.ShareForumActivity; import org.briarproject.android.forum.ShareForumActivity;
import org.briarproject.android.forum.WriteForumPostActivity; import org.briarproject.android.forum.WriteForumPostActivity;
import org.briarproject.android.identity.CreateIdentityActivity; import org.briarproject.android.identity.CreateIdentityActivity;
import org.briarproject.android.introduction.ContactChooserFragment;
import org.briarproject.android.introduction.IntroductionActivity;
import org.briarproject.android.introduction.IntroductionMessageFragment;
import org.briarproject.android.invitation.AddContactActivity; import org.briarproject.android.invitation.AddContactActivity;
import org.briarproject.android.keyagreement.ChooseIdentityFragment; import org.briarproject.android.keyagreement.ChooseIdentityFragment;
import org.briarproject.android.keyagreement.KeyAgreementActivity; import org.briarproject.android.keyagreement.KeyAgreementActivity;
@@ -80,6 +83,12 @@ public interface AndroidComponent extends CoreEagerSingletons {
void inject(ShowQrCodeFragment fragment); void inject(ShowQrCodeFragment fragment);
void inject(IntroductionActivity activity);
void inject(ContactChooserFragment fragment);
void inject(IntroductionMessageFragment fragment);
// Eager singleton load // Eager singleton load
void inject(AppModule.EagerSingletons init); void inject(AppModule.EagerSingletons init);
} }

View File

@@ -14,10 +14,15 @@ import org.briarproject.android.api.AndroidExecutor;
import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.contact.ConversationActivity; import org.briarproject.android.contact.ConversationActivity;
import org.briarproject.android.forum.ForumActivity; import org.briarproject.android.forum.ForumActivity;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.db.DatabaseExecutor;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.event.Event; import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventListener; import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.IntroductionRequestReceivedEvent;
import org.briarproject.api.event.IntroductionResponseReceivedEvent;
import org.briarproject.api.event.IntroductionSucceededEvent;
import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.SettingsUpdatedEvent; import org.briarproject.api.event.SettingsUpdatedEvent;
import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumManager;
@@ -57,6 +62,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3; private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3;
private static final int FORUM_POST_NOTIFICATION_ID = 4; private static final int FORUM_POST_NOTIFICATION_ID = 4;
private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 5;
private static final String CONTACT_URI = private static final String CONTACT_URI =
"content://org.briarproject/contact"; "content://org.briarproject/contact";
private static final String FORUM_URI = private static final String FORUM_URI =
@@ -111,6 +117,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
public Void call() { public Void call() {
clearPrivateMessageNotification(); clearPrivateMessageNotification();
clearForumPostNotification(); clearForumPostNotification();
clearIntroductionSuccessNotification();
return null; return null;
} }
}); });
@@ -135,6 +142,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
nm.cancel(FORUM_POST_NOTIFICATION_ID); nm.cancel(FORUM_POST_NOTIFICATION_ID);
} }
private void clearIntroductionSuccessNotification() {
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.cancel(INTRODUCTION_SUCCESS_NOTIFICATION_ID);
}
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof SettingsUpdatedEvent) { if (e instanceof SettingsUpdatedEvent) {
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e; SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
@@ -148,6 +161,15 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
else if (c.equals(forumManager.getClientId())) else if (c.equals(forumManager.getClientId()))
showForumPostNotification(m.getMessage().getGroupId()); showForumPostNotification(m.getMessage().getGroupId());
} }
} else if (e instanceof IntroductionRequestReceivedEvent) {
ContactId c = ((IntroductionRequestReceivedEvent) e).getContactId();
showIntroductionNotifications(c);
} else if (e instanceof IntroductionResponseReceivedEvent) {
ContactId c = ((IntroductionResponseReceivedEvent) e).getContactId();
showIntroductionNotifications(c);
} else if (e instanceof IntroductionSucceededEvent) {
Contact c = ((IntroductionSucceededEvent) e).getContact();
showIntroductionSucceededNotification(c);
} }
} }
@@ -335,4 +357,49 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager,
} }
}); });
} }
private void showIntroductionNotifications(final ContactId c) {
androidExecutor.execute(new Runnable() {
public void run() {
try {
GroupId group = messagingManager.getConversationId(c);
showPrivateMessageNotification(group);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void showIntroductionSucceededNotification(final Contact c) {
androidExecutor.execute(new Runnable() {
public void run() {
NotificationCompat.Builder b =
new NotificationCompat.Builder(appContext);
b.setSmallIcon(R.drawable.introduction_notification);
b.setContentTitle(appContext
.getString(R.string.introduction_success_title));
b.setContentText(appContext
.getString(R.string.introduction_success_text,
c.getAuthor().getName()));
b.setDefaults(getDefaults());
b.setAutoCancel(true);
Intent i = new Intent(appContext, NavDrawerActivity.class);
i.putExtra(NavDrawerActivity.INTENT_CONTACTS, true);
i.setFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);
TaskStackBuilder t = TaskStackBuilder.create(appContext);
t.addParentStack(NavDrawerActivity.class);
t.addNextIntent(i);
b.setContentIntent(t.getPendingIntent(nextRequestId++, 0));
Object o = appContext.getSystemService(NOTIFICATION_SERVICE);
NotificationManager nm = (NotificationManager) o;
nm.notify(INTRODUCTION_SUCCESS_NOTIFICATION_ID, b.build());
}
});
}
} }

View File

@@ -54,7 +54,7 @@ public abstract class BaseActivity extends AppCompatActivity {
((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT); ((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT);
} }
protected void hideSoftKeyboard(View view) { public void hideSoftKeyboard(View view) {
IBinder token = view.getWindowToken(); IBinder token = view.getWindowToken();
Object o = getSystemService(INPUT_METHOD_SERVICE); Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0); ((InputMethodManager) o).hideSoftInputFromWindow(token, 0);

View File

@@ -1,8 +1,12 @@
package org.briarproject.android.contact; package org.briarproject.android.contact;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.graphics.Color;
import android.content.res.Resources; import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.support.v7.util.SortedList; import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils; import android.text.format.DateUtils;
@@ -14,9 +18,8 @@ import android.widget.TextView;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.identity.AuthorId;
import java.util.List; import java.util.List;
@@ -53,13 +56,23 @@ public class ContactListAdapter
@Override @Override
public int compare(ContactListItem c1, public int compare(ContactListItem c1,
ContactListItem c2) { ContactListItem c2) {
// sort items by time int authorCompare = 0;
// and do not take unread messages into account if (chooser) {
long time1 = c1.getTimestamp(); authorCompare = c1.getLocalAuthor().getName()
long time2 = c2.getTimestamp(); .compareTo(
if (time1 < time2) return 1; c2.getLocalAuthor().getName());
if (time1 > time2) return -1; }
return 0; if (authorCompare == 0) {
// sort items by time
// and do not take unread messages into account
long time1 = c1.getTimestamp();
long time2 = c2.getTimestamp();
if (time1 < time2) return 1;
if (time1 > time2) return -1;
return 0;
} else {
return authorCompare;
}
} }
@Override @Override
@@ -86,10 +99,16 @@ public class ContactListAdapter
return true; return true;
} }
}); });
private final OnItemClickListener listener;
private final boolean chooser;
private Context ctx; private Context ctx;
private AuthorId localAuthorId;
public ContactListAdapter(Context context) { public ContactListAdapter(Context context, OnItemClickListener listener,
boolean chooser) {
ctx = context; ctx = context;
this.listener = listener;
this.chooser = chooser;
} }
@Override @Override
@@ -103,12 +122,11 @@ public class ContactListAdapter
@Override @Override
public void onBindViewHolder(final ContactHolder ui, final int position) { public void onBindViewHolder(final ContactHolder ui, final int position) {
final ContactListItem item = getItem(position); final ContactListItem item = getItem(position);
Resources res = ctx.getResources();
int unread = item.getUnreadCount(); int unread = item.getUnreadCount();
if (unread > 0) { if (!chooser && unread > 0) {
ui.layout.setBackgroundColor( ui.layout.setBackgroundColor(
res.getColor(R.color.unread_background)); ContextCompat.getColor(ctx, R.color.unread_background));
} }
if (item.isConnected()) { if (item.isConnected()) {
@@ -121,27 +139,37 @@ public class ContactListAdapter
ui.avatar.setImageDrawable( ui.avatar.setImageDrawable(
new IdenticonDrawable(author.getId().getBytes())); new IdenticonDrawable(author.getId().getBytes()));
String contactName = author.getName(); String contactName = author.getName();
if (unread > 0) {
if (!chooser && unread > 0) {
// TODO show these in a bubble on top of the avatar
ui.name.setText(contactName + " (" + unread + ")"); ui.name.setText(contactName + " (" + unread + ")");
} else { } else {
ui.name.setText(contactName); ui.name.setText(contactName);
} }
if (chooser) {
ui.identity.setText(item.getLocalAuthor().getName());
} else {
ui.identity.setVisibility(View.GONE);
}
if (item.isEmpty()) { if (item.isEmpty()) {
ui.date.setText(R.string.no_private_messages); ui.date.setText(R.string.no_private_messages);
} else { } else {
// TODO show this as X units ago
long timestamp = item.getTimestamp(); long timestamp = item.getTimestamp();
ui.date.setText( ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, timestamp)); DateUtils.getRelativeTimeSpanString(ctx, timestamp));
} }
if (chooser && !item.getLocalAuthor().getId().equals(localAuthorId)) {
grayOutItem(ui);
}
ui.layout.setOnClickListener(new View.OnClickListener() { ui.layout.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
GroupId groupId = item.getGroupId(); listener.onItemClick(ui.avatar, item);
Intent i = new Intent(ctx, ConversationActivity.class);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
ctx.startActivity(i);
} }
}); });
} }
@@ -151,6 +179,34 @@ public class ContactListAdapter
return contacts.size(); return contacts.size();
} }
/**
* Set the identity from whose perspective the contact shall be chosen.
* This is only used if chooser is true.
* @param authorId The ID of the local Author
*/
public void setLocalAuthor(AuthorId authorId) {
localAuthorId = authorId;
notifyDataSetChanged();
}
private void grayOutItem(final ContactHolder ui) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
float alpha = 0.25f;
ui.bulb.setAlpha(alpha);
ui.avatar.setAlpha(alpha);
ui.name.setAlpha(alpha);
ui.date.setAlpha(alpha);
ui.identity.setAlpha(alpha);
} else {
ColorFilter colorFilter = new PorterDuffColorFilter(Color.GRAY,
PorterDuff.Mode.MULTIPLY);
ui.bulb.setColorFilter(colorFilter);
ui.avatar.setColorFilter(colorFilter);
ui.name.setEnabled(false);
ui.date.setEnabled(false);
}
}
public ContactListItem getItem(int position) { public ContactListItem getItem(int position) {
if (position == INVALID_POSITION || contacts.size() <= position) { if (position == INVALID_POSITION || contacts.size() <= position) {
return null; // Not found return null; // Not found
@@ -162,10 +218,6 @@ public class ContactListAdapter
contacts.updateItemAt(position, item); contacts.updateItemAt(position, item);
} }
public int findItemPosition(ContactListItem item) {
return contacts.indexOf(item);
}
public int findItemPosition(ContactId c) { public int findItemPosition(ContactId c) {
int count = getItemCount(); int count = getItemCount();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -202,6 +254,7 @@ public class ContactListAdapter
public ImageView bulb; public ImageView bulb;
public ImageView avatar; public ImageView avatar;
public TextView name; public TextView name;
public TextView identity;
public TextView date; public TextView date;
public ContactHolder(View v) { public ContactHolder(View v) {
@@ -211,7 +264,13 @@ public class ContactListAdapter
bulb = (ImageView) v.findViewById(R.id.bulbView); bulb = (ImageView) v.findViewById(R.id.bulbView);
avatar = (ImageView) v.findViewById(R.id.avatarView); avatar = (ImageView) v.findViewById(R.id.avatarView);
name = (TextView) v.findViewById(R.id.nameView); name = (TextView) v.findViewById(R.id.nameView);
identity = (TextView) v.findViewById(R.id.identityView);
date = (TextView) v.findViewById(R.id.dateView); date = (TextView) v.findViewById(R.id.dateView);
} }
} }
public interface OnItemClickListener {
void onItemClick(View view, ContactListItem item);
}
} }

View File

@@ -1,9 +1,11 @@
package org.briarproject.android.contact; package org.briarproject.android.contact;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -11,23 +13,26 @@ import android.view.ViewGroup;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.android.AndroidComponent; import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarApplication;
import org.briarproject.android.fragment.BaseEventFragment; import org.briarproject.android.fragment.BaseEventFragment;
import org.briarproject.android.keyagreement.KeyAgreementActivity; import org.briarproject.android.keyagreement.KeyAgreementActivity;
import org.briarproject.android.util.BriarRecyclerView; import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager; import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.NoSuchContactException; import org.briarproject.api.db.NoSuchContactException;
import org.briarproject.api.event.ContactAddedEvent; import org.briarproject.api.event.ContactAddedEvent;
import org.briarproject.api.event.ContactConnectedEvent; import org.briarproject.api.event.ContactConnectedEvent;
import org.briarproject.api.event.ContactDisconnectedEvent; import org.briarproject.api.event.ContactDisconnectedEvent;
import org.briarproject.api.event.ContactRemovedEvent; import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.ContactStatusChangedEvent;
import org.briarproject.api.event.Event; import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.messaging.MessagingManager; import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.plugins.ConnectionRegistry; import org.briarproject.api.plugins.ConnectionRegistry;
@@ -74,8 +79,12 @@ public class ContactListFragment extends BaseEventFragment {
@Inject @Inject
protected volatile ContactManager contactManager; protected volatile ContactManager contactManager;
@Inject @Inject
protected volatile IdentityManager identityManager;
@Inject
protected volatile MessagingManager messagingManager; protected volatile MessagingManager messagingManager;
@Inject @Inject
protected volatile IntroductionManager introductionManager;
@Inject
protected volatile EventBus eventBus; protected volatile EventBus eventBus;
@Override @Override
@@ -91,7 +100,31 @@ public class ContactListFragment extends BaseEventFragment {
inflater.inflate(R.layout.activity_contact_list, container, inflater.inflate(R.layout.activity_contact_list, container,
false); false);
adapter = new ContactListAdapter(getContext()); ContactListAdapter.OnItemClickListener onItemClickListener =
new ContactListAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, ContactListItem item) {
GroupId groupId = item.getGroupId();
Intent i = new Intent(getActivity(),
ConversationActivity.class);
i.putExtra("briar.GROUP_ID", groupId.getBytes());
if (Build.VERSION.SDK_INT >= 16) {
ActivityOptionsCompat options =
ActivityOptionsCompat.
makeSceneTransitionAnimation(
getActivity(),
view, "avatar");
getActivity().startActivity(i, options.toBundle());
} else {
startActivity(i);
}
}
};
adapter = new ContactListAdapter(getContext(), onItemClickListener,
false);
list = (BriarRecyclerView) contentView.findViewById(R.id.contactList); list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
list.setLayoutManager(new LinearLayoutManager(getContext())); list.setLayoutManager(new LinearLayoutManager(getContext()));
list.setAdapter(adapter); list.setAdapter(adapter);
@@ -135,12 +168,14 @@ public class ContactListFragment extends BaseEventFragment {
ContactId id = c.getId(); ContactId id = c.getId();
GroupId groupId = GroupId groupId =
messagingManager.getConversationId(id); messagingManager.getConversationId(id);
Collection<PrivateMessageHeader> headers = Collection<ConversationItem> messages =
messagingManager.getMessageHeaders(id); getMessages(id);
boolean connected = boolean connected =
connectionRegistry.isConnected(c.getId()); connectionRegistry.isConnected(c.getId());
contacts.add(new ContactListItem(c, connected, LocalAuthor localAuthor = identityManager
groupId, headers)); .getLocalAuthor(c.getLocalAuthorId());
contacts.add(new ContactListItem(c, localAuthor,
connected, groupId, messages));
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
// Continue // Continue
} }
@@ -169,7 +204,12 @@ public class ContactListFragment extends BaseEventFragment {
public void eventOccurred(Event e) { public void eventOccurred(Event e) {
if (e instanceof ContactAddedEvent) { if (e instanceof ContactAddedEvent) {
LOG.info("Contact added, reloading"); if(((ContactAddedEvent) e).isActive()) {
LOG.info("Contact added as active, reloading");
loadContacts();
}
} else if (e instanceof ContactStatusChangedEvent) {
LOG.info("Contact Status changed, reloading");
loadContacts(); loadContacts();
} else if (e instanceof ContactConnectedEvent) { } else if (e instanceof ContactConnectedEvent) {
setConnected(((ContactConnectedEvent) e).getContactId(), true); setConnected(((ContactConnectedEvent) e).getContactId(), true);
@@ -181,7 +221,8 @@ public class ContactListFragment extends BaseEventFragment {
} else if (e instanceof MessageValidatedEvent) { } else if (e instanceof MessageValidatedEvent) {
MessageValidatedEvent m = (MessageValidatedEvent) e; MessageValidatedEvent m = (MessageValidatedEvent) e;
ClientId c = m.getClientId(); ClientId c = m.getClientId();
if (m.isValid() && c.equals(messagingManager.getClientId())) { if (m.isValid() && (c.equals(messagingManager.getClientId()) ||
c.equals(introductionManager.getClientId()))) {
LOG.info("Message added, reloading"); LOG.info("Message added, reloading");
reloadConversation(m.getMessage().getGroupId()); reloadConversation(m.getMessage().getGroupId());
} }
@@ -192,14 +233,10 @@ public class ContactListFragment extends BaseEventFragment {
listener.runOnDbThread(new Runnable() { listener.runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis();
ContactId c = messagingManager.getContactId(g); ContactId c = messagingManager.getContactId(g);
Collection<PrivateMessageHeader> headers = Collection<ConversationItem> messages =
messagingManager.getMessageHeaders(c); getMessages(c);
long duration = System.currentTimeMillis() - now; updateItem(c, messages);
if (LOG.isLoggable(INFO))
LOG.info("Partial load took " + duration + " ms");
updateItem(c, headers);
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
LOG.info("Contact removed"); LOG.info("Contact removed");
} catch (DbException e) { } catch (DbException e) {
@@ -211,13 +248,13 @@ public class ContactListFragment extends BaseEventFragment {
} }
private void updateItem(final ContactId c, private void updateItem(final ContactId c,
final Collection<PrivateMessageHeader> headers) { final Collection<ConversationItem> messages) {
listener.runOnUiThread(new Runnable() { listener.runOnUiThread(new Runnable() {
public void run() { public void run() {
int position = adapter.findItemPosition(c); int position = adapter.findItemPosition(c);
ContactListItem item = adapter.getItem(position); ContactListItem item = adapter.getItem(position);
if (item != null) { if (item != null) {
item.setHeaders(headers); item.setMessages(messages);
adapter.updateItem(position, item); adapter.updateItem(position, item);
} }
} }
@@ -246,4 +283,36 @@ public class ContactListFragment extends BaseEventFragment {
} }
}); });
} }
/** This needs to be called from the DbThread */
private Collection<ConversationItem> getMessages(ContactId id)
throws DbException {
long now = System.currentTimeMillis();
Collection<ConversationItem> messages =
new ArrayList<ConversationItem>();
Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(id);
for (PrivateMessageHeader h : headers) {
messages.add(ConversationItem.from(h));
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading message headers took " + duration + " ms");
now = System.currentTimeMillis();
Collection<IntroductionMessage> introductions =
introductionManager
.getIntroductionMessages(id);
for (IntroductionMessage m : introductions) {
messages.add(ConversationItem.from(m));
}
duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading introduction messages took " + duration + " ms");
return messages;
}
} }

View File

@@ -1,44 +1,55 @@
package org.briarproject.android.contact; package org.briarproject.android.contact;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import java.util.Collection; import java.util.Collection;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
// This class is not thread-safe // This class is not thread-safe
class ContactListItem { public class ContactListItem {
private final Contact contact; private final Contact contact;
private final LocalAuthor localAuthor;
private final GroupId groupId; private final GroupId groupId;
private boolean connected, empty; private boolean connected, empty;
private long timestamp; private long timestamp;
private int unread; private int unread;
ContactListItem(Contact contact, boolean connected, GroupId groupId, public ContactListItem(Contact contact, LocalAuthor localAuthor,
Collection<PrivateMessageHeader> headers) { boolean connected,
GroupId groupId,
Collection<ConversationItem> messages) {
this.contact = contact; this.contact = contact;
this.localAuthor = localAuthor;
this.groupId = groupId; this.groupId = groupId;
this.connected = connected; this.connected = connected;
setHeaders(headers); setMessages(messages);
} }
void setHeaders(Collection<PrivateMessageHeader> headers) { void setMessages(Collection<ConversationItem> messages) {
empty = headers.isEmpty(); empty = messages.isEmpty();
timestamp = 0; timestamp = 0;
unread = 0; unread = 0;
if (!empty) { if (!empty) {
for (PrivateMessageHeader h : headers) { for (ConversationItem i : messages) {
if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp(); if (i.getTime() > timestamp) timestamp = i.getTime();
if (!h.isRead()) unread++; if (i instanceof IncomingItem && !((IncomingItem) i).isRead())
unread++;
} }
} }
} }
Contact getContact() { public Contact getContact() {
return contact; return contact;
} }
public LocalAuthor getLocalAuthor() {
return localAuthor;
}
GroupId getGroupId() { GroupId getGroupId() {
return groupId; return groupId;
} }

View File

@@ -2,11 +2,15 @@ package org.briarproject.android.contact;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.Toolbar;
import android.util.SparseArray;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
@@ -14,14 +18,17 @@ import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.android.AndroidComponent; import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarActivity; import org.briarproject.android.BriarActivity;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.android.introduction.IntroductionActivity;
import org.briarproject.android.util.BriarRecyclerView; import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.android.api.AndroidNotificationManager;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager; import org.briarproject.api.contact.ContactManager;
@@ -35,9 +42,16 @@ import org.briarproject.api.event.ContactRemovedEvent;
import org.briarproject.api.event.Event; import org.briarproject.api.event.Event;
import org.briarproject.api.event.EventBus; import org.briarproject.api.event.EventBus;
import org.briarproject.api.event.EventListener; import org.briarproject.api.event.EventListener;
import org.briarproject.api.event.IntroductionRequestReceivedEvent;
import org.briarproject.api.event.IntroductionResponseReceivedEvent;
import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.MessageValidatedEvent;
import org.briarproject.api.event.MessagesAckedEvent; import org.briarproject.api.event.MessagesAckedEvent;
import org.briarproject.api.event.MessagesSentEvent; import org.briarproject.api.event.MessagesSentEvent;
import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.introduction.IntroductionResponse;
import org.briarproject.api.introduction.SessionId;
import org.briarproject.api.messaging.MessagingManager; import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessage; import org.briarproject.api.messaging.PrivateMessage;
import org.briarproject.api.messaging.PrivateMessageFactory; import org.briarproject.api.messaging.PrivateMessageFactory;
@@ -61,12 +75,18 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import de.hdodenhof.circleimageview.CircleImageView;
import im.delight.android.identicons.IdenticonDrawable;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.INFO; import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
public class ConversationActivity extends BriarActivity public class ConversationActivity extends BriarActivity
implements EventListener, OnClickListener { implements EventListener, OnClickListener,
ConversationAdapter.IntroductionHandler {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(ConversationActivity.class.getName()); Logger.getLogger(ConversationActivity.class.getName());
@@ -76,6 +96,9 @@ public class ConversationActivity extends BriarActivity
@Inject @CryptoExecutor protected Executor cryptoExecutor; @Inject @CryptoExecutor protected Executor cryptoExecutor;
private Map<MessageId, byte[]> bodyCache = new HashMap<MessageId, byte[]>(); private Map<MessageId, byte[]> bodyCache = new HashMap<MessageId, byte[]>();
private ConversationAdapter adapter = null; private ConversationAdapter adapter = null;
private CircleImageView toolbarAvatar;
private ImageView toolbarStatus;
private TextView toolbarTitle;
private BriarRecyclerView list = null; private BriarRecyclerView list = null;
private EditText content = null; private EditText content = null;
private ImageButton sendButton = null; private ImageButton sendButton = null;
@@ -85,6 +108,7 @@ public class ConversationActivity extends BriarActivity
@Inject protected volatile MessagingManager messagingManager; @Inject protected volatile MessagingManager messagingManager;
@Inject protected volatile EventBus eventBus; @Inject protected volatile EventBus eventBus;
@Inject protected volatile PrivateMessageFactory privateMessageFactory; @Inject protected volatile PrivateMessageFactory privateMessageFactory;
@Inject protected volatile IntroductionManager introductionManager;
private volatile GroupId groupId = null; private volatile GroupId groupId = null;
private volatile ContactId contactId = null; private volatile ContactId contactId = null;
private volatile String contactName = null; private volatile String contactName = null;
@@ -102,7 +126,21 @@ public class ConversationActivity extends BriarActivity
setContentView(R.layout.activity_conversation); setContentView(R.layout.activity_conversation);
adapter = new ConversationAdapter(this); // Custom Toolbar
Toolbar tb = (Toolbar) findViewById(R.id.toolbar);
toolbarAvatar = (CircleImageView) tb.findViewById(R.id.contactAvatar);
toolbarStatus = (ImageView) tb.findViewById(R.id.contactStatus);
toolbarTitle = (TextView) tb.findViewById(R.id.contactName);
setSupportActionBar(tb);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setDisplayShowHomeEnabled(true);
ab.setDisplayHomeAsUpEnabled(true);
ab.setDisplayShowCustomEnabled(true);
ab.setDisplayShowTitleEnabled(false);
}
adapter = new ConversationAdapter(this, this);
list = (BriarRecyclerView) findViewById(R.id.conversationView); list = (BriarRecyclerView) findViewById(R.id.conversationView);
list.setLayoutManager(new LinearLayoutManager(this)); list.setLayoutManager(new LinearLayoutManager(this));
list.setAdapter(adapter); list.setAdapter(adapter);
@@ -125,8 +163,7 @@ public class ConversationActivity extends BriarActivity
eventBus.addListener(this); eventBus.addListener(this);
notificationManager.blockNotification(groupId); notificationManager.blockNotification(groupId);
notificationManager.clearPrivateMessageNotification(groupId); notificationManager.clearPrivateMessageNotification(groupId);
loadContactDetails(); loadData();
loadHeaders();
} }
@Override @Override
@@ -141,12 +178,10 @@ public class ConversationActivity extends BriarActivity
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu items for use in the action bar // Inflate the menu items for use in the action bar
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.contact_actions, menu); inflater.inflate(R.menu.conversation_actions, menu);
// Adapt icon color to dark action bar hideIntroductionActionWhenOneContact(
menu.findItem(R.id.action_social_remove_person).getIcon().setColorFilter( menu.findItem(R.id.action_introduction));
getResources().getColor(R.color.action_bar_text),
PorterDuff.Mode.SRC_IN);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@@ -155,6 +190,19 @@ public class ConversationActivity extends BriarActivity
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items // Handle presses on the action bar items
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home:
supportFinishAfterTransition();
return true;
case R.id.action_introduction:
if (contactId == null) return false;
Intent intent = new Intent(this, IntroductionActivity.class);
intent.putExtra(IntroductionActivity.CONTACT_ID,
contactId.getInt());
ActivityOptionsCompat options = ActivityOptionsCompat
.makeCustomAnimation(this, android.R.anim.slide_in_left,
android.R.anim.slide_out_right);
ActivityCompat.startActivity(this, intent, options.toBundle());
return true;
case R.id.action_social_remove_person: case R.id.action_social_remove_person:
askToRemoveContact(); askToRemoveContact();
return true; return true;
@@ -163,20 +211,26 @@ public class ConversationActivity extends BriarActivity
} }
} }
private void loadContactDetails() { private void loadData() {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
contactId = messagingManager.getContactId(groupId); if (contactId == null)
Contact contact = contactManager.getContact(contactId); contactId = messagingManager.getContactId(groupId);
contactName = contact.getAuthor().getName(); if (contactName == null || contactIdenticonKey == null) {
contactIdenticonKey = contact.getAuthor().getId().getBytes(); Contact contact = contactManager.getContact(contactId);
contactName = contact.getAuthor().getName();
contactIdenticonKey =
contact.getAuthor().getId().getBytes();
}
connected = connectionRegistry.isConnected(contactId); connected = connectionRegistry.isConnected(contactId);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Loading contact took " + duration + " ms"); LOG.info("Loading contact took " + duration + " ms");
displayContactDetails(); displayContactDetails();
// Load the messages here to make sure we have a contactId
loadMessages();
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
finishOnUiThread(); finishOnUiThread();
} catch (DbException e) { } catch (DbException e) {
@@ -190,31 +244,44 @@ public class ConversationActivity extends BriarActivity
private void displayContactDetails() { private void displayContactDetails() {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
ActionBar actionBar = getSupportActionBar(); toolbarAvatar.setImageDrawable(
if (actionBar != null) { new IdenticonDrawable(contactIdenticonKey));
actionBar.setTitle(contactName); toolbarTitle.setText(contactName);
if (connected) {
actionBar.setSubtitle(getString(R.string.online)); if (connected) {
} else { toolbarStatus.setImageDrawable(ContextCompat
actionBar.setSubtitle(getString(R.string.offline)); .getDrawable(ConversationActivity.this,
} R.drawable.contact_online));
toolbarStatus
.setContentDescription(getString(R.string.online));
} else {
toolbarStatus.setImageDrawable(ContextCompat
.getDrawable(ConversationActivity.this,
R.drawable.contact_offline));
toolbarStatus
.setContentDescription(getString(R.string.offline));
} }
adapter.setIdenticonKey(contactIdenticonKey); adapter.setContactInformation(contactIdenticonKey, contactName);
} }
}); });
} }
private void loadHeaders() { private void loadMessages() {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (contactId == null)
contactId = messagingManager.getContactId(groupId);
Collection<PrivateMessageHeader> headers = Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(contactId); messagingManager.getMessageHeaders(contactId);
Collection<IntroductionMessage> introductions =
introductionManager
.getIntroductionMessages(contactId);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
LOG.info("Loading headers took " + duration + " ms"); LOG.info("Loading headers took " + duration + " ms");
displayHeaders(headers); displayMessages(headers, introductions);
} catch (NoSuchContactException e) { } catch (NoSuchContactException e) {
finishOnUiThread(); finishOnUiThread();
} catch (DbException e) { } catch (DbException e) {
@@ -225,23 +292,38 @@ public class ConversationActivity extends BriarActivity
}); });
} }
private void displayHeaders( private void displayMessages(final Collection<PrivateMessageHeader> headers,
final Collection<PrivateMessageHeader> headers) { final Collection<IntroductionMessage> introductions) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
sendButton.setEnabled(true); sendButton.setEnabled(true);
if (headers.isEmpty()) { if (headers.isEmpty() && introductions.isEmpty()) {
// we have no messages, // we have no messages,
// so let the list know to hide progress bar // so let the list know to hide progress bar
list.showData(); list.showData();
} else { } else {
for (PrivateMessageHeader h : headers) { for (PrivateMessageHeader h : headers) {
ConversationItem item = new ConversationItem(h); ConversationMessageItem item =
(ConversationMessageItem) ConversationItem
.from(h);
byte[] body = bodyCache.get(h.getId()); byte[] body = bodyCache.get(h.getId());
if (body == null) loadMessageBody(h); if (body == null) loadMessageBody(h);
else item.setBody(body); else item.setBody(body);
adapter.add(item); adapter.add(item);
} }
for (IntroductionMessage m : introductions) {
ConversationItem item;
if (m instanceof IntroductionRequest) {
item = ConversationItem
.from((IntroductionRequest) m);
} else {
item = ConversationItem
.from(ConversationActivity.this,
contactName,
(IntroductionResponse) m);
}
adapter.add(item);
}
// Scroll to the bottom // Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1); list.scrollToPosition(adapter.getItemCount() - 1);
} }
@@ -273,14 +355,14 @@ public class ConversationActivity extends BriarActivity
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
bodyCache.put(m, body); bodyCache.put(m, body);
int count = adapter.getItemCount(); SparseArray<ConversationMessageItem> messages =
for (int i = 0; i < count; i++) { adapter.getPrivateMessages();
ConversationItem item = adapter.getItem(i); for (int i = 0; i < messages.size(); i++) {
if (item.getHeader().getId().equals(m)) { ConversationMessageItem item = messages.valueAt(i);
if (item.getId().equals(m)) {
item.setBody(body); item.setBody(body);
adapter.notifyItemChanged(i); adapter.notifyItemChanged(messages.keyAt(i));
// Scroll to the bottom list.scrollToPosition(adapter.getItemCount() - 1);
list.scrollToPosition(count - 1);
return; return;
} }
} }
@@ -288,12 +370,24 @@ public class ConversationActivity extends BriarActivity
}); });
} }
private void addIntroduction(final ConversationItem item) {
runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.add(item);
// Scroll to the bottom
list.scrollToPosition(adapter.getItemCount() - 1);
}
});
}
private void markMessagesRead() { private void markMessagesRead() {
List<MessageId> unread = new ArrayList<MessageId>(); List<MessageId> unread = new ArrayList<MessageId>();
int count = adapter.getItemCount(); SparseArray<IncomingItem> list =
for (int i = 0; i < count; i++) { adapter.getIncomingMessages();
PrivateMessageHeader h = adapter.getItem(i).getHeader(); for (int i = 0; i < list.size(); i++) {
if (!h.isRead()) unread.add(h.getId()); IncomingItem item = list.valueAt(i);
if (!item.isRead()) unread.add(item.getId());
} }
if (unread.isEmpty()) return; if (unread.isEmpty()) return;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
@@ -307,6 +401,8 @@ public class ConversationActivity extends BriarActivity
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
for (MessageId m : unread) for (MessageId m : unread)
// not really clean, but the messaging manager can
// handle introduction messages as well
messagingManager.setReadFlag(m, true); messagingManager.setReadFlag(m, true);
long duration = System.currentTimeMillis() - now; long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO)) if (LOG.isLoggable(INFO))
@@ -331,7 +427,7 @@ public class ConversationActivity extends BriarActivity
if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) { if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) {
LOG.info("Message added, reloading"); LOG.info("Message added, reloading");
// Mark new incoming messages as read directly // Mark new incoming messages as read directly
if (m.isLocal()) loadHeaders(); if (m.isLocal()) loadMessages();
else markMessageReadIfNew(m.getMessage()); else markMessageReadIfNew(m.getMessage());
} }
} else if (e instanceof MessagesSentEvent) { } else if (e instanceof MessagesSentEvent) {
@@ -360,6 +456,23 @@ public class ConversationActivity extends BriarActivity
connected = false; connected = false;
displayContactDetails(); displayContactDetails();
} }
} else if (e instanceof IntroductionRequestReceivedEvent) {
IntroductionRequestReceivedEvent event =
(IntroductionRequestReceivedEvent) e;
if (event.getContactId().equals(contactId)) {
IntroductionRequest ir = event.getIntroductionRequest();
ConversationItem item = new ConversationIntroductionInItem(ir);
addIntroduction(item);
}
} else if (e instanceof IntroductionResponseReceivedEvent) {
IntroductionResponseReceivedEvent event =
(IntroductionResponseReceivedEvent) e;
if (event.getContactId().equals(contactId)) {
IntroductionResponse ir = event.getIntroductionResponse();
ConversationItem item =
ConversationItem.from(this, contactName, ir);
addIntroduction(item);
}
} }
} }
@@ -369,10 +482,13 @@ public class ConversationActivity extends BriarActivity
ConversationItem item = adapter.getLastItem(); ConversationItem item = adapter.getLastItem();
if (item != null) { if (item != null) {
// Mark the message read if it's the newest message // Mark the message read if it's the newest message
long lastMsgTime = item.getHeader().getTimestamp(); long lastMsgTime = item.getTime();
long newMsgTime = m.getTimestamp(); long newMsgTime = m.getTimestamp();
if (newMsgTime > lastMsgTime) markNewMessageRead(m); if (newMsgTime > lastMsgTime) markNewMessageRead(m);
else loadHeaders(); else loadMessages();
} else {
// mark the message as read as well if it is the first one
markNewMessageRead(m);
} }
} }
}); });
@@ -383,7 +499,7 @@ public class ConversationActivity extends BriarActivity
public void run() { public void run() {
try { try {
messagingManager.setReadFlag(m.getId(), true); messagingManager.setReadFlag(m.getId(), true);
loadHeaders(); loadMessages();
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
@@ -397,13 +513,14 @@ public class ConversationActivity extends BriarActivity
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
public void run() { public void run() {
Set<MessageId> messages = new HashSet<MessageId>(messageIds); Set<MessageId> messages = new HashSet<MessageId>(messageIds);
int count = adapter.getItemCount(); SparseArray<OutgoingItem> list =
for (int i = 0; i < count; i++) { adapter.getOutgoingMessages();
ConversationItem item = adapter.getItem(i); for (int i = 0; i < list.size(); i++) {
if (messages.contains(item.getHeader().getId())) { OutgoingItem item = list.valueAt(i);
if (messages.contains(item.getId())) {
item.setSent(sent); item.setSent(sent);
item.setSeen(seen); item.setSeen(seen);
adapter.notifyItemChanged(i); adapter.notifyItemChanged(list.keyAt(i));
} }
} }
} }
@@ -424,7 +541,7 @@ public class ConversationActivity extends BriarActivity
private long getMinTimestampForNewMessage() { private long getMinTimestampForNewMessage() {
// Don't use an earlier timestamp than the newest message // Don't use an earlier timestamp than the newest message
ConversationItem item = adapter.getLastItem(); ConversationItem item = adapter.getLastItem();
return item == null ? 0 : item.getHeader().getTimestamp() + 1; return item == null ? 0 : item.getTime() + 1;
} }
private void createMessage(final byte[] body, final long timestamp) { private void createMessage(final byte[] body, final long timestamp) {
@@ -466,7 +583,8 @@ public class ConversationActivity extends BriarActivity
} }
}; };
AlertDialog.Builder builder = AlertDialog.Builder builder =
new AlertDialog.Builder(ConversationActivity.this); new AlertDialog.Builder(ConversationActivity.this,
R.style.BriarDialogTheme);
builder.setTitle(getString(R.string.dialog_title_delete_contact)); builder.setTitle(getString(R.string.dialog_title_delete_contact));
builder.setMessage(getString(R.string.dialog_message_delete_contact)); builder.setMessage(getString(R.string.dialog_message_delete_contact));
builder.setPositiveButton(android.R.string.ok, okListener); builder.setPositiveButton(android.R.string.ok, okListener);
@@ -478,6 +596,10 @@ public class ConversationActivity extends BriarActivity
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
public void run() { public void run() {
try { try {
// make sure contactId is initialised
if (contactId == null)
contactId = messagingManager.getContactId(groupId);
// remove contact with that ID
contactManager.removeContact(contactId); contactManager.removeContact(contactId);
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
@@ -500,4 +622,73 @@ public class ConversationActivity extends BriarActivity
} }
}); });
} }
private void hideIntroductionActionWhenOneContact(final MenuItem item) {
runOnDbThread(new Runnable() {
public void run() {
try {
if (contactManager.getActiveContacts().size() < 2) {
hideIntroductionAction(item);
}
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void hideIntroductionAction(final MenuItem item) {
runOnUiThread(new Runnable() {
@Override
public void run() {
item.setVisible(false);
}
});
}
@Override
public void respondToIntroduction(final SessionId sessionId,
final boolean accept) {
runOnDbThread(new Runnable() {
@Override
public void run() {
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
try {
if (accept) {
introductionManager
.acceptIntroduction(contactId, sessionId,
timestamp);
} else {
introductionManager
.declineIntroduction(contactId, sessionId,
timestamp);
}
loadMessages();
} catch (DbException e) {
introductionResponseError();
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
} catch (FormatException e) {
introductionResponseError();
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void introductionResponseError() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ConversationActivity.this,
R.string.introduction_response_error,
Toast.LENGTH_SHORT).show();
}
});
}
} }

View File

@@ -4,29 +4,37 @@ import android.content.Context;
import android.support.v7.util.SortedList; import android.support.v7.util.SortedList;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.util.SparseArray;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.briarproject.R; import org.briarproject.R;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.introduction.SessionId;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.util.StringUtils; import org.briarproject.util.StringUtils;
import im.delight.android.identicons.IdenticonDrawable; import im.delight.android.identicons.IdenticonDrawable;
import static android.support.v7.util.SortedList.INVALID_POSITION; import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.support.v7.widget.RecyclerView.ViewHolder;
import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_IN;
import static org.briarproject.android.contact.ConversationItem.INTRODUCTION_OUT;
import static org.briarproject.android.contact.ConversationItem.MSG_IN;
import static org.briarproject.android.contact.ConversationItem.MSG_IN_UNREAD;
import static org.briarproject.android.contact.ConversationItem.MSG_OUT;
import static org.briarproject.android.contact.ConversationItem.NOTICE_IN;
import static org.briarproject.android.contact.ConversationItem.NOTICE_OUT;
import static org.briarproject.android.contact.ConversationItem.OutgoingItem;
import static org.briarproject.android.contact.ConversationItem.IncomingItem;
class ConversationAdapter extends class ConversationAdapter extends RecyclerView.Adapter {
RecyclerView.Adapter<ConversationAdapter.MessageHolder> {
private static final int MSG_OUT = 0; private final SortedList<ConversationItem> items =
private static final int MSG_IN = 1;
private static final int MSG_IN_UNREAD = 2;
private final SortedList<ConversationItem> messages =
new SortedList<ConversationItem>(ConversationItem.class, new SortedList<ConversationItem>(ConversationItem.class,
new SortedList.Callback<ConversationItem>() { new SortedList.Callback<ConversationItem>() {
@Override @Override
@@ -52,8 +60,8 @@ class ConversationAdapter extends
@Override @Override
public int compare(ConversationItem c1, public int compare(ConversationItem c1,
ConversationItem c2) { ConversationItem c2) {
long time1 = c1.getHeader().getTimestamp(); long time1 = c1.getTime();
long time2 = c2.getHeader().getTimestamp(); long time2 = c2.getTime();
if (time1 < time2) return -1; if (time1 < time2) return -1;
if (time1 > time2) return 1; if (time1 > time2) return 1;
return 0; return 0;
@@ -62,8 +70,7 @@ class ConversationAdapter extends
@Override @Override
public boolean areItemsTheSame(ConversationItem c1, public boolean areItemsTheSame(ConversationItem c1,
ConversationItem c2) { ConversationItem c2) {
return c1.getHeader().getId() return c1.getId().equals(c2.getId());
.equals(c2.getHeader().getId());
} }
@Override @Override
@@ -73,67 +80,103 @@ class ConversationAdapter extends
} }
}); });
private Context ctx; private Context ctx;
private IntroductionHandler intro;
private byte[] identiconKey; private byte[] identiconKey;
private String contactName;
public ConversationAdapter(Context context) { public ConversationAdapter(Context context,
IntroductionHandler introductionHandler) {
ctx = context; ctx = context;
intro = introductionHandler;
} }
public void setIdenticonKey(byte[] key) { public void setContactInformation(byte[] identiconKey, String contactName) {
this.identiconKey = key; this.identiconKey = identiconKey;
this.contactName = contactName;
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
// return different type for incoming and outgoing (local) messages return getItem(position).getType();
PrivateMessageHeader header = getItem(position).getHeader();
if (header.isLocal()) {
return MSG_OUT;
} else if (header.isRead()) {
return MSG_IN;
} else {
return MSG_IN_UNREAD;
}
} }
@Override @Override
public MessageHolder onCreateViewHolder(ViewGroup viewGroup, int type) { public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) {
View v; View v;
// outgoing message (local) // outgoing message (local)
if (type == MSG_OUT) { if (type == MSG_OUT) {
v = LayoutInflater.from(viewGroup.getContext()) v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_msg_out, viewGroup, false); .inflate(R.layout.list_item_msg_out, viewGroup, false);
return new MessageHolder(v, type);
}
else if (type == INTRODUCTION_IN) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_introduction_in, viewGroup, false);
return new IntroductionHolder(v, type);
}
else if (type == INTRODUCTION_OUT) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_introduction_out, viewGroup, false);
return new IntroductionHolder(v, type);
}
else if (type == NOTICE_IN) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_notice_in, viewGroup, false);
return new NoticeHolder(v, type);
}
else if (type == NOTICE_OUT) {
v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_notice_out, viewGroup, false);
return new NoticeHolder(v, type);
} }
// incoming message (non-local) // incoming message (non-local)
else { else {
v = LayoutInflater.from(viewGroup.getContext()) v = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.list_item_msg_in, viewGroup, false); .inflate(R.layout.list_item_msg_in, viewGroup, false);
return new MessageHolder(v, type);
} }
return new MessageHolder(v, type);
} }
@Override @Override
public void onBindViewHolder(final MessageHolder ui, final int position) { public void onBindViewHolder(ViewHolder ui, int position) {
ConversationItem item = getItem(position); ConversationItem item = getItem(position);
if (item instanceof ConversationMessageItem) {
bindMessage((MessageHolder) ui, (ConversationMessageItem) item);
} else if (item instanceof ConversationIntroductionOutItem) {
bindIntroduction((IntroductionHolder) ui,
(ConversationIntroductionOutItem) item, position);
} else if (item instanceof ConversationIntroductionInItem) {
bindIntroduction((IntroductionHolder) ui,
(ConversationIntroductionInItem) item, position);
} else if (item instanceof ConversationNoticeOutItem) {
bindNotice((NoticeHolder) ui, (ConversationNoticeOutItem) item);
} else if (item instanceof ConversationNoticeInItem) {
bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item);
} else {
throw new IllegalArgumentException("Unhandled Conversation Item");
}
}
private void bindMessage(MessageHolder ui, ConversationMessageItem item) {
PrivateMessageHeader header = item.getHeader(); PrivateMessageHeader header = item.getHeader();
if (header.isLocal()) { if (item instanceof ConversationItem.OutgoingItem) {
if (item.isSeen()) { if (((OutgoingItem) item).isSeen()) {
ui.status.setImageResource(R.drawable.message_delivered); ui.status.setImageResource(R.drawable.message_delivered_white);
} else if (item.isSent()) { } else if (((OutgoingItem) item).isSent()) {
ui.status.setImageResource(R.drawable.message_sent); ui.status.setImageResource(R.drawable.message_sent_white);
} else { } else {
ui.status.setImageResource(R.drawable.message_stored); ui.status.setImageResource(R.drawable.message_stored_white);
} }
} else { } else {
if (identiconKey != null) if (identiconKey != null)
ui.avatar.setImageDrawable( ui.avatar.setImageDrawable(new IdenticonDrawable(identiconKey));
new IdenticonDrawable(identiconKey)); if (item.getType() == MSG_IN_UNREAD) {
if (!header.isRead()) { // TODO implement new unread message highlight according to #232
int left = ui.layout.getPaddingLeft(); /* int left = ui.layout.getPaddingLeft();
int top = ui.layout.getPaddingTop(); int top = ui.layout.getPaddingTop();
int right = ui.layout.getPaddingRight(); int right = ui.layout.getPaddingRight();
int bottom = ui.layout.getPaddingBottom(); int bottom = ui.layout.getPaddingBottom();
@@ -144,6 +187,7 @@ class ConversationAdapter extends
// re-apply the previous padding due to bug in some Android versions // re-apply the previous padding due to bug in some Android versions
// see: https://code.google.com/p/android/issues/detail?id=17885 // see: https://code.google.com/p/android/issues/detail?id=17885
ui.layout.setPadding(left, top, right, bottom); ui.layout.setPadding(left, top, right, bottom);
*/
} }
} }
@@ -159,42 +203,171 @@ class ConversationAdapter extends
ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp)); ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp));
} }
private void bindIntroduction(IntroductionHolder ui,
final ConversationIntroductionItem item, final int position) {
final IntroductionRequest ir = item.getIntroductionRequest();
final String message = ir.getMessage();
if (StringUtils.isNullOrEmpty(message)) {
ui.messageLayout.setVisibility(View.GONE);
} else {
ui.messageLayout.setVisibility(View.VISIBLE);
if (item.getType() == INTRODUCTION_IN && identiconKey != null) {
ui.message.avatar.setImageDrawable(
new IdenticonDrawable(identiconKey));
}
ui.message.body.setText(message);
ui.message.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
}
// Outgoing Introduction Request
if (item instanceof ConversationIntroductionOutItem) {
ui.text.setText(ctx.getString(R.string.introduction_request_sent,
contactName, ir.getName()));
ConversationIntroductionOutItem i =
(ConversationIntroductionOutItem) item;
if (i.isSeen()) {
ui.status.setImageResource(R.drawable.message_delivered);
ui.message.status.setImageResource(R.drawable.message_delivered_white);
} else if (i.isSent()) {
ui.status.setImageResource(R.drawable.message_sent);
ui.message.status.setImageResource(R.drawable.message_sent_white);
} else {
ui.status.setImageResource(R.drawable.message_stored);
ui.message.status.setImageResource(R.drawable.message_stored_white);
}
}
// Incoming Introduction Request (Answered)
else if (item.wasAnswered()) {
ui.text.setText(ctx.getString(
R.string.introduction_request_answered_received,
contactName, ir.getName()));
ui.acceptButton.setVisibility(View.GONE);
ui.declineButton.setVisibility(View.GONE);
}
// Incoming Introduction Request (Not Answered)
else {
if (item.getIntroductionRequest().contactExists()) {
ui.text.setText(ctx.getString(
R.string.introduction_request_exists_received,
contactName, ir.getName()));
} else {
ui.text.setText(
ctx.getString(R.string.introduction_request_received,
contactName, ir.getName()));
}
ui.acceptButton.setVisibility(View.VISIBLE);
ui.acceptButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intro.respondToIntroduction(ir.getSessionId(), true);
item.setAnswered(true);
notifyItemChanged(position);
}
});
ui.declineButton.setVisibility(View.VISIBLE);
ui.declineButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intro.respondToIntroduction(ir.getSessionId(), false);
item.setAnswered(true);
notifyItemChanged(position);
}
});
}
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
}
private void bindNotice(NoticeHolder ui, ConversationNoticeItem item) {
ui.text.setText(item.getText());
ui.date.setText(
DateUtils.getRelativeTimeSpanString(ctx, item.getTime()));
if (item instanceof ConversationNoticeOutItem) {
ConversationNoticeOutItem n = (ConversationNoticeOutItem) item;
if (n.isSeen()) {
ui.status.setImageResource(R.drawable.message_delivered);
} else if (n.isSent()) {
ui.status.setImageResource(R.drawable.message_sent);
} else {
ui.status.setImageResource(R.drawable.message_stored);
}
}
}
@Override @Override
public int getItemCount() { public int getItemCount() {
return messages.size(); return items.size();
} }
public ConversationItem getItem(int position) { public ConversationItem getItem(int position) {
if (position == INVALID_POSITION || messages.size() <= position) { if (position == INVALID_POSITION || items.size() <= position) {
return null; // Not found return null; // Not found
} }
return messages.get(position); return items.get(position);
} }
public ConversationItem getLastItem() { public ConversationItem getLastItem() {
if (messages.size() > 0) { if (items.size() > 0) {
return messages.get(messages.size() - 1); return items.get(items.size() - 1);
} else { } else {
return null; return null;
} }
} }
public SparseArray<IncomingItem> getIncomingMessages() {
SparseArray<IncomingItem> messages =
new SparseArray<IncomingItem>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof IncomingItem) {
messages.put(i, (IncomingItem) item);
}
}
return messages;
}
public SparseArray<OutgoingItem> getOutgoingMessages() {
SparseArray<OutgoingItem> messages =
new SparseArray<OutgoingItem>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof OutgoingItem) {
messages.put(i, (OutgoingItem) item);
}
}
return messages;
}
public SparseArray<ConversationMessageItem> getPrivateMessages() {
SparseArray<ConversationMessageItem> messages =
new SparseArray<ConversationMessageItem>();
for (int i = 0; i < items.size(); i++) {
ConversationItem item = items.get(i);
if (item instanceof ConversationMessageItem) {
messages.put(i, (ConversationMessageItem) item);
}
}
return messages;
}
public void add(final ConversationItem message) { public void add(final ConversationItem message) {
this.messages.add(message); this.items.add(message);
} }
public void clear() { public void clear() {
this.messages.beginBatchedUpdates(); items.clear();
while(messages.size() != 0) {
messages.removeItemAt(0);
}
this.messages.endBatchedUpdates();
} }
// TODO: Does this class need to be public? private static class MessageHolder extends RecyclerView.ViewHolder {
public static class MessageHolder extends RecyclerView.ViewHolder {
public ViewGroup layout; public ViewGroup layout;
public TextView body; public TextView body;
@@ -217,4 +390,59 @@ class ConversationAdapter extends
} }
} }
} }
private static class IntroductionHolder extends RecyclerView.ViewHolder {
public ViewGroup layout;
public View messageLayout;
public MessageHolder message;
public TextView text;
public Button acceptButton;
public Button declineButton;
public TextView date;
public ImageView status;
public IntroductionHolder(View v, int type) {
super(v);
layout = (ViewGroup) v.findViewById(R.id.introductionLayout);
messageLayout = v.findViewById(R.id.messageLayout);
message = new MessageHolder(messageLayout,
type == INTRODUCTION_IN ? MSG_IN : MSG_OUT);
text = (TextView) v.findViewById(R.id.introductionText);
acceptButton = (Button) v.findViewById(R.id.acceptButton);
declineButton = (Button) v.findViewById(R.id.declineButton);
date = (TextView) v.findViewById(R.id.introductionTime);
if (type == INTRODUCTION_OUT) {
status = (ImageView) v.findViewById(R.id.introductionStatus);
}
}
}
private static class NoticeHolder extends RecyclerView.ViewHolder {
public ViewGroup layout;
public TextView text;
public TextView date;
public ImageView status;
public NoticeHolder(View v, int type) {
super(v);
layout = (ViewGroup) v.findViewById(R.id.noticeLayout);
text = (TextView) v.findViewById(R.id.noticeText);
date = (TextView) v.findViewById(R.id.noticeTime);
if (type == NOTICE_OUT) {
status = (ImageView) v.findViewById(R.id.noticeStatus);
}
}
}
public interface IntroductionHandler {
void respondToIntroduction(final SessionId sessionId,
final boolean accept);
}
} }

View File

@@ -0,0 +1,32 @@
package org.briarproject.android.contact;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.sync.MessageId;
public class ConversationIntroductionInItem extends ConversationIntroductionItem
implements ConversationItem.IncomingItem {
private boolean read;
public ConversationIntroductionInItem(IntroductionRequest ir) {
super(ir);
this.read = ir.isRead();
}
@Override
int getType() {
return INTRODUCTION_IN;
}
@Override
public boolean isRead() {
return read;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -0,0 +1,29 @@
package org.briarproject.android.contact;
import org.briarproject.api.introduction.IntroductionRequest;
abstract class ConversationIntroductionItem extends ConversationItem {
private IntroductionRequest ir;
private boolean answered;
public ConversationIntroductionItem(IntroductionRequest ir) {
super(ir.getMessageId(), ir.getTime());
this.ir = ir;
this.answered = ir.wasAnswered();
}
public IntroductionRequest getIntroductionRequest() {
return ir;
}
public boolean wasAnswered() {
return answered;
}
public void setAnswered(boolean answered) {
this.answered = answered;
}
}

View File

@@ -0,0 +1,47 @@
package org.briarproject.android.contact;
import org.briarproject.api.introduction.IntroductionRequest;
/**
* This class is needed and can not be replaced by an ConversationNoticeOutItem,
* because it carries the optional introduction message
* to be displayed as a regular private message.
*/
public class ConversationIntroductionOutItem
extends ConversationIntroductionItem
implements ConversationItem.OutgoingItem {
private boolean sent, seen;
public ConversationIntroductionOutItem(IntroductionRequest ir) {
super(ir);
this.sent = ir.isSent();
this.seen = ir.isSeen();
}
@Override
int getType() {
return INTRODUCTION_OUT;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -1,46 +1,114 @@
package org.briarproject.android.contact; package org.briarproject.android.contact;
import android.content.Context;
import org.briarproject.R;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.introduction.IntroductionRequest;
import org.briarproject.api.introduction.IntroductionResponse;
import org.briarproject.api.messaging.PrivateMessageHeader; import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.sync.MessageId;
// This class is not thread-safe // This class is not thread-safe
class ConversationItem { public abstract class ConversationItem {
private final PrivateMessageHeader header; // this is needed for RecyclerView adapter which requires an int type
private byte[] body; final static int MSG_IN = 0;
private boolean sent, seen; final static int MSG_IN_UNREAD = 1;
final static int MSG_OUT = 2;
final static int INTRODUCTION_IN = 3;
final static int INTRODUCTION_OUT = 4;
final static int NOTICE_IN = 5;
final static int NOTICE_OUT = 6;
ConversationItem(PrivateMessageHeader header) { private MessageId id;
this.header = header; private long time;
body = null;
sent = header.isSent(); public ConversationItem(MessageId id, long time) {
seen = header.isSeen(); this.id = id;
this.time = time;
} }
PrivateMessageHeader getHeader() { abstract int getType();
return header;
public MessageId getId() {
return id;
} }
byte[] getBody() { long getTime() {
return body; return time;
} }
void setBody(byte[] body) { public static ConversationItem from(PrivateMessageHeader h) {
this.body = body; if (h.isLocal())
return new ConversationMessageOutItem(h);
else
return new ConversationMessageInItem(h);
} }
boolean isSent() { public static ConversationItem from(IntroductionRequest ir) {
return sent; if (ir.isLocal()) {
return new ConversationIntroductionOutItem(ir);
} else {
return new ConversationIntroductionInItem(ir);
}
} }
void setSent(boolean sent) { public static ConversationItem from(Context ctx, String contactName,
this.sent = sent; IntroductionResponse ir) {
if (ir.isLocal()) {
String text;
if (ir.wasAccepted()) {
text = ctx.getString(
R.string.introduction_response_accepted_sent,
ir.getName());
} else {
text = ctx.getString(
R.string.introduction_response_declined_sent,
ir.getName());
}
return new ConversationNoticeOutItem(ir.getMessageId(), text,
ir.getTime(), ir.isSent(), ir.isSeen());
} else {
String text;
if (ir.wasAccepted()) {
text = ctx.getString(
R.string.introduction_response_accepted_received,
contactName, ir.getName());
} else {
text = ctx.getString(
R.string.introduction_response_declined_received,
contactName, ir.getName());
}
return new ConversationNoticeInItem(ir.getMessageId(), text,
ir.getTime(), ir.isRead());
}
} }
boolean isSeen() { /** This method should not be used to get user-facing objects,
return seen; * Its purpose is to provider data for the contact list.
*/
public static ConversationItem from(IntroductionMessage im) {
if (im.isLocal())
return new ConversationNoticeOutItem(im.getMessageId(), "",
im.getTime(), false, false);
return new ConversationNoticeInItem(im.getMessageId(), "", im.getTime(),
im.isRead());
} }
void setSeen(boolean seen) { protected interface OutgoingItem {
this.seen = seen; MessageId getId();
boolean isSent();
void setSent(boolean sent);
boolean isSeen();
void setSeen(boolean seen);
} }
protected interface IncomingItem {
MessageId getId();
boolean isRead();
void setRead(boolean read);
}
} }

View File

@@ -0,0 +1,32 @@
package org.briarproject.android.contact;
import org.briarproject.api.messaging.PrivateMessageHeader;
// This class is not thread-safe
public class ConversationMessageInItem extends ConversationMessageItem
implements ConversationItem.IncomingItem {
private boolean read;
public ConversationMessageInItem(PrivateMessageHeader header) {
super(header);
read = header.isRead();
}
@Override
int getType() {
return MSG_IN;
}
@Override
public boolean isRead() {
return read;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -0,0 +1,30 @@
package org.briarproject.android.contact;
import org.briarproject.api.messaging.PrivateMessageHeader;
// This class is not thread-safe
abstract class ConversationMessageItem extends ConversationItem {
private final PrivateMessageHeader header;
private byte[] body;
public ConversationMessageItem(PrivateMessageHeader header) {
super(header.getId(), header.getTimestamp());
this.header = header;
body = null;
}
PrivateMessageHeader getHeader() {
return header;
}
byte[] getBody() {
return body;
}
void setBody(byte[] body) {
this.body = body;
}
}

View File

@@ -0,0 +1,43 @@
package org.briarproject.android.contact;
import org.briarproject.api.messaging.PrivateMessageHeader;
// This class is not thread-safe
public class ConversationMessageOutItem extends ConversationMessageItem
implements ConversationItem.OutgoingItem {
private boolean sent, seen;
public ConversationMessageOutItem(PrivateMessageHeader header) {
super(header);
sent = header.isSent();
seen = header.isSeen();
}
@Override
int getType() {
return MSG_OUT;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -0,0 +1,32 @@
package org.briarproject.android.contact;
import org.briarproject.api.sync.MessageId;
public class ConversationNoticeInItem extends ConversationNoticeItem implements
ConversationItem.IncomingItem {
private boolean read;
public ConversationNoticeInItem(MessageId id, String text, long time,
boolean read) {
super(id, text, time);
this.read = read;
}
@Override
int getType() {
return NOTICE_IN;
}
@Override
public boolean isRead() {
return read;
}
@Override
public void setRead(boolean read) {
this.read = read;
}
}

View File

@@ -0,0 +1,19 @@
package org.briarproject.android.contact;
import org.briarproject.api.sync.MessageId;
abstract class ConversationNoticeItem extends ConversationItem {
private String text;
public ConversationNoticeItem(MessageId id, String text, long time) {
super(id, time);
this.text = text;
}
public String getText() {
return text;
}
}

View File

@@ -0,0 +1,44 @@
package org.briarproject.android.contact;
import org.briarproject.api.sync.MessageId;
public class ConversationNoticeOutItem extends ConversationNoticeItem implements
ConversationItem.OutgoingItem {
private boolean sent, seen;
public ConversationNoticeOutItem(MessageId id, String text, long time,
boolean sent, boolean seen) {
super(id, text, time);
this.sent = sent;
this.seen = seen;
}
@Override
int getType() {
return NOTICE_OUT;
}
@Override
public boolean isSent() {
return sent;
}
@Override
public void setSent(boolean sent) {
this.sent = sent;
}
@Override
public boolean isSeen() {
return seen;
}
@Override
public void setSeen(boolean seen) {
this.seen = seen;
}
}

View File

@@ -0,0 +1,238 @@
package org.briarproject.android.introduction;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.transition.Fade;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.contact.ContactListAdapter;
import org.briarproject.android.contact.ContactListItem;
import org.briarproject.android.contact.ConversationItem;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.android.util.BriarRecyclerView;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DbException;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.identity.IdentityManager;
import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.IntroductionMessage;
import org.briarproject.api.messaging.MessagingManager;
import org.briarproject.api.messaging.PrivateMessageHeader;
import org.briarproject.api.plugins.ConnectionRegistry;
import org.briarproject.api.sync.GroupId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
public class ContactChooserFragment extends BaseFragment {
public final static String TAG = "ContactChooserFragment";
private IntroductionActivity introductionActivity;
private BriarRecyclerView list;
private ContactListAdapter adapter;
private int contactId;
private static final Logger LOG =
Logger.getLogger(ContactChooserFragment.class.getName());
// Fields that are accessed from background threads must be volatile
protected volatile Contact c1;
@Inject
protected volatile ContactManager contactManager;
@Inject
protected volatile IdentityManager identityManager;
@Inject
protected volatile MessagingManager messagingManager;
@Inject
protected volatile IntroductionManager introductionManager;
@Inject
protected volatile ConnectionRegistry connectionRegistry;
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
introductionActivity = (IntroductionActivity) context;
} catch (ClassCastException e) {
throw new java.lang.InstantiationError(
"This fragment is only meant to be attached to the IntroductionActivity");
}
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View contentView =
inflater.inflate(R.layout.introduction_contact_chooser,
container, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setExitTransition(new Fade());
}
ContactListAdapter.OnItemClickListener onItemClickListener =
new ContactListAdapter.OnItemClickListener() {
@Override
public void onItemClick(View view, ContactListItem item) {
if (c1 == null) {
throw new RuntimeException("c1 not initialized");
}
Contact c2 = item.getContact();
if (!c1.getLocalAuthorId()
.equals(c2.getLocalAuthorId())) {
warnAboutDifferentIdentities(view, c1, c2);
} else {
introductionActivity.showMessageScreen(view, c1, c2);
}
}
};
adapter =
new ContactListAdapter(getActivity(), onItemClickListener, true);
list = (BriarRecyclerView) contentView.findViewById(R.id.contactList);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(adapter);
list.setEmptyText(getString(R.string.no_contacts));
contactId = introductionActivity.getContactId();
return contentView;
}
@Override
public void onResume() {
super.onResume();
loadContacts();
}
@Override
public String getUniqueTag() {
return TAG;
}
private void loadContacts() {
introductionActivity.runOnDbThread(new Runnable() {
public void run() {
try {
List<ContactListItem> contacts =
new ArrayList<ContactListItem>();
AuthorId localAuthorId= null;
for (Contact c : contactManager.getActiveContacts()) {
if (c.getId().getInt() == contactId) {
c1 = c;
localAuthorId = c1.getLocalAuthorId();
} else {
ContactId id = c.getId();
GroupId groupId =
messagingManager.getConversationId(id);
Collection<ConversationItem> messages =
getMessages(id);
boolean connected =
connectionRegistry.isConnected(c.getId());
LocalAuthor localAuthor = identityManager
.getLocalAuthor(c.getLocalAuthorId());
contacts.add(new ContactListItem(c, localAuthor,
connected, groupId, messages));
}
}
displayContacts(localAuthorId, contacts);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void displayContacts(final AuthorId localAuthorId,
final List<ContactListItem> contacts) {
introductionActivity.runOnUiThread(new Runnable() {
public void run() {
adapter.setLocalAuthor(localAuthorId);
adapter.clear();
if (contacts.size() == 0) list.showData();
else adapter.addAll(contacts);
}
});
}
private void warnAboutDifferentIdentities(final View view, final Contact c1,
final Contact c2) {
DialogInterface.OnClickListener okListener =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
introductionActivity.showMessageScreen(view, c1, c2);
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
R.style.BriarDialogTheme);
builder.setTitle(getString(
R.string.introduction_warn_different_identities_title));
builder.setMessage(getString(
R.string.introduction_warn_different_identities_text));
builder.setPositiveButton(R.string.dialog_button_introduce, okListener);
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
/** This needs to be called from the DbThread */
private Collection<ConversationItem> getMessages(ContactId id)
throws DbException {
long now = System.currentTimeMillis();
Collection<ConversationItem> messages =
new ArrayList<ConversationItem>();
Collection<PrivateMessageHeader> headers =
messagingManager.getMessageHeaders(id);
for (PrivateMessageHeader h : headers) {
messages.add(ConversationItem.from(h));
}
long duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading message headers took " + duration + " ms");
now = System.currentTimeMillis();
Collection<IntroductionMessage> introductions =
introductionManager
.getIntroductionMessages(id);
for (IntroductionMessage m : introductions) {
messages.add(ConversationItem.from(m));
}
duration = System.currentTimeMillis() - now;
if (LOG.isLoggable(INFO))
LOG.info("Loading introduction messages took " + duration + " ms");
return messages;
}
}

View File

@@ -0,0 +1,109 @@
package org.briarproject.android.introduction;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.view.MenuItem;
import android.view.View;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.BriarActivity;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.api.contact.Contact;
public class IntroductionActivity extends BriarActivity implements
BaseFragment.BaseFragmentListener {
public static final String CONTACT_ID = "briar.CONTACT_ID";
private int contactId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
contactId = intent.getIntExtra(CONTACT_ID, -1);
if (contactId == -1)
throw new IllegalArgumentException("Wrong ContactId");
setContentView(R.layout.activity_introduction);
if (savedInstanceState == null) {
ContactChooserFragment chooserFragment =
new ContactChooserFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.introductionContainer, chooserFragment).commit();
}
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Override
public void showLoadingScreen(boolean isBlocking, int stringId) {
// this is handled by the recycler view in ContactChooserFragment
}
@Override
public void hideLoadingScreen() {
// this is handled by the recycler view in ContactChooserFragment
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// Handle presses on the action bar items
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onBackPressed() {
FragmentManager fm = getSupportFragmentManager();
if (fm.getBackStackEntryCount() == 1) {
fm.popBackStack();
} else {
super.onBackPressed();
}
}
public int getContactId() {
return contactId;
}
public void showMessageScreen(final View view, final Contact c1,
final Contact c2) {
IntroductionMessageFragment messageFragment =
IntroductionMessageFragment
.newInstance(c1.getId().getInt(), c2.getId().getInt());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
messageFragment.setSharedElementEnterTransition(new ChangeBounds());
messageFragment.setEnterTransition(new Fade());
messageFragment.setSharedElementReturnTransition(new ChangeBounds());
}
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(android.R.anim.fade_in,
android.R.anim.fade_out,
android.R.anim.slide_in_left,
android.R.anim.slide_out_right)
.addSharedElement(view, "avatar")
.replace(R.id.introductionContainer, messageFragment,
ContactChooserFragment.TAG)
.addToBackStack(null)
.commit();
}
}

View File

@@ -0,0 +1,229 @@
package org.briarproject.android.introduction;
import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import org.briarproject.R;
import org.briarproject.android.AndroidComponent;
import org.briarproject.android.fragment.BaseFragment;
import org.briarproject.api.FormatException;
import org.briarproject.api.contact.Contact;
import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.db.DbException;
import org.briarproject.api.introduction.IntroductionManager;
import java.util.logging.Logger;
import javax.inject.Inject;
import de.hdodenhof.circleimageview.CircleImageView;
import im.delight.android.identicons.IdenticonDrawable;
import static java.util.logging.Level.WARNING;
public class IntroductionMessageFragment extends BaseFragment {
private static final Logger LOG =
Logger.getLogger(IntroductionMessageFragment.class.getName());
public final static String TAG = "IntroductionMessageFragment";
private IntroductionActivity introductionActivity;
private ViewHolder ui;
private final static String CONTACT_ID_1 = "contact1";
private final static String CONTACT_ID_2 = "contact2";
// Fields that are accessed from background threads must be volatile
@Inject
protected volatile ContactManager contactManager;
@Inject
protected volatile IntroductionManager introductionManager;
public static IntroductionMessageFragment newInstance(int contactId1,
int contactId2) {
IntroductionMessageFragment f = new IntroductionMessageFragment();
Bundle args = new Bundle();
args.putInt(CONTACT_ID_1, contactId1);
args.putInt(CONTACT_ID_2, contactId2);
f.setArguments(args);
return f;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
try {
introductionActivity = (IntroductionActivity) context;
} catch (ClassCastException e) {
throw new java.lang.InstantiationError(
"This fragment is only meant to be attached to the IntroductionActivity");
}
}
@Override
public void injectActivity(AndroidComponent component) {
component.inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// change toolbar text
ActionBar actionBar = introductionActivity.getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(R.string.introduction_message_title);
}
// inflate view
View v =
inflater.inflate(R.layout.introduction_message, container,
false);
// show progress bar until contacts have been loaded
ui = new ViewHolder(v);
ui.text.setVisibility(View.GONE);
ui.button.setEnabled(false);
// get contact IDs from fragment arguments
int contactId1 = getArguments().getInt(CONTACT_ID_1, -1);
int contactId2 = getArguments().getInt(CONTACT_ID_2, -1);
if (contactId1 == -1 || contactId2 == -1) {
throw new java.lang.InstantiationError(
"You need to use newInstance() to instantiate");
}
// get contacts and then show view
prepareToSetUpViews(contactId1, contactId2);
return v;
}
@Override
public String getUniqueTag() {
return TAG;
}
private void prepareToSetUpViews(final int contactId1,
final int contactId2) {
introductionActivity.runOnDbThread(new Runnable() {
public void run() {
try {
Contact c1 = contactManager
.getContact(new ContactId(contactId1));
Contact c2 = contactManager
.getContact(new ContactId(contactId2));
setUpViews(c1, c2);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
}
}
});
}
private void setUpViews(final Contact c1, final Contact c2) {
introductionActivity.runOnUiThread(new Runnable() {
public void run() {
// set avatars
ui.avatar1.setImageDrawable(new IdenticonDrawable(
c1.getAuthor().getId().getBytes()));
ui.avatar2.setImageDrawable(new IdenticonDrawable(
c2.getAuthor().getId().getBytes()));
// set introduction text
ui.text.setText(String.format(
getString(R.string.introduction_message_text),
c1.getAuthor().getName(), c2.getAuthor().getName()));
// set button action
ui.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onButtonClick(c1, c2);
}
});
// hide progress bar and show views
ui.progressBar.setVisibility(View.GONE);
ui.text.setVisibility(View.VISIBLE);
ui.button.setEnabled(true);
}
});
}
public void onButtonClick(final Contact c1, final Contact c2) {
// disable button to prevent accidental double invitations
ui.button.setEnabled(false);
String msg = ui.message.getText().toString();
makeIntroduction(c1, c2, msg);
// don't wait for the introduction to be made before finishing activity
introductionActivity.hideSoftKeyboard(ui.message);
introductionActivity.finish();
}
private void makeIntroduction(final Contact c1, final Contact c2,
final String msg) {
introductionActivity.runOnDbThread(new Runnable() {
public void run() {
// actually make the introduction
try {
long timestamp = System.currentTimeMillis();
introductionManager.makeIntroduction(c1, c2, msg, timestamp);
} catch (DbException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
introductionError();
} catch (FormatException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
introductionError();
}
}
});
}
private void introductionError() {
introductionActivity.runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(introductionActivity,
R.string.introduction_error, Toast.LENGTH_SHORT)
.show();
}
});
}
private static class ViewHolder {
ProgressBar progressBar;
ViewGroup header;
CircleImageView avatar1;
CircleImageView avatar2;
TextView text;
EditText message;
Button button;
ViewHolder(View v) {
progressBar = (ProgressBar) v.findViewById(R.id.progressBar);
header = (ViewGroup) v.findViewById(R.id.introductionHeader);
avatar1 = (CircleImageView) v.findViewById(R.id.avatarContact1);
avatar2 = (CircleImageView) v.findViewById(R.id.avatarContact2);
text = (TextView) v.findViewById(R.id.introductionText);
message = (EditText) v.findViewById(R.id.introductionMessageView);
button = (Button) v.findViewById(R.id.makeIntroductionButton);
}
}
}

View File

@@ -73,15 +73,10 @@ public class BriarRecyclerView extends FrameLayout {
} }
emptyObserver = new RecyclerView.AdapterDataObserver() { emptyObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
showData();
}
@Override @Override
public void onItemRangeInserted(int positionStart, int itemCount) { public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount); super.onItemRangeInserted(positionStart, itemCount);
onChanged(); if (itemCount > 0) showData();
} }
}; };
} }

View File

@@ -8,6 +8,7 @@ import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction; import org.briarproject.api.db.Transaction;
import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import java.util.Collection; import java.util.Collection;
@@ -20,19 +21,22 @@ public interface IntroductionManager {
/** /**
* sends two initial introduction messages * sends two initial introduction messages
*/ */
void makeIntroduction(Contact c1, Contact c2, String msg) void makeIntroduction(Contact c1, Contact c2, String msg,
final long timestamp)
throws DbException, FormatException; throws DbException, FormatException;
/** /**
* Accept an introduction that had been made * Accept an introduction that had been made
*/ */
void acceptIntroduction(final SessionId sessionId) void acceptIntroduction(final ContactId contactId,
final SessionId sessionId, final long timestamp)
throws DbException, FormatException; throws DbException, FormatException;
/** /**
* Decline an introduction that had been made * Decline an introduction that had been made
*/ */
void declineIntroduction(final SessionId sessionId) void declineIntroduction(final ContactId contactId,
final SessionId sessionId, final long timestamp)
throws DbException, FormatException; throws DbException, FormatException;
/** /**
@@ -46,8 +50,8 @@ public interface IntroductionManager {
/** Get the session state for the given session ID */ /** Get the session state for the given session ID */
BdfDictionary getSessionState(Transaction txn, byte[] sessionId) BdfDictionary getSessionState(Transaction txn, GroupId groupId,
throws DbException, FormatException; byte[] sessionId) throws DbException, FormatException;
/** Gets the group used for introductions with Contact c */ /** Gets the group used for introductions with Contact c */
Group getIntroductionGroup(Contact c); Group getIntroductionGroup(Contact c);

View File

@@ -29,7 +29,7 @@ public class IntroductionRequest extends IntroductionResponse {
return answered; return answered;
} }
public boolean doesExist() { public boolean contactExists() {
return exists; return exists;
} }

View File

@@ -20,6 +20,14 @@ public interface TransportPropertyManager {
Map<TransportId, TransportProperties> getLocalProperties() Map<TransportId, TransportProperties> getLocalProperties()
throws DbException; throws DbException;
/**
* Returns the local transport properties for all transports.
* <br/>
* Read-Only
* */
Map<TransportId, TransportProperties> getLocalProperties(Transaction txn)
throws DbException;
/** Returns the local transport properties for the given transport. */ /** Returns the local transport properties for the given transport. */
TransportProperties getLocalProperties(TransportId t) throws DbException; TransportProperties getLocalProperties(TransportId t) throws DbException;

View File

@@ -109,6 +109,7 @@ public class IntroduceeEngine
msg.put(E_PUBLIC_KEY, localState.getRaw(OUR_PUBLIC_KEY)); msg.put(E_PUBLIC_KEY, localState.getRaw(OUR_PUBLIC_KEY));
msg.put(TRANSPORT, localAction.getDictionary(TRANSPORT)); msg.put(TRANSPORT, localAction.getDictionary(TRANSPORT));
} }
msg.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
messages.add(msg); messages.add(msg);
logAction(currentState, localState, msg); logAction(currentState, localState, msg);

View File

@@ -28,6 +28,7 @@ import org.briarproject.api.introduction.IntroductionManager;
import org.briarproject.api.introduction.SessionId; import org.briarproject.api.introduction.SessionId;
import org.briarproject.api.properties.TransportProperties; import org.briarproject.api.properties.TransportProperties;
import org.briarproject.api.properties.TransportPropertyManager; import org.briarproject.api.properties.TransportPropertyManager;
import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.GroupId;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
@@ -51,6 +52,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_K
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER; import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER;
import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID; import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
import static org.briarproject.api.introduction.IntroductionConstants.NAME; import static org.briarproject.api.introduction.IntroductionConstants.NAME;
import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
import static org.briarproject.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY; import static org.briarproject.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY;
@@ -157,11 +159,15 @@ class IntroduceeManager {
processStateUpdate(txn, engine.onMessageReceived(state, message)); processStateUpdate(txn, engine.onMessageReceived(state, message));
} }
public void acceptIntroduction(Transaction txn, public void acceptIntroduction(Transaction txn, final ContactId contactId,
final SessionId sessionId) throws DbException, FormatException { final SessionId sessionId, final long timestamp)
throws DbException, FormatException {
BdfDictionary state = Contact c = db.getContact(txn, contactId);
introductionManager.getSessionState(txn, sessionId.getBytes()); Group g = introductionManager.getIntroductionGroup(c);
BdfDictionary state = introductionManager
.getSessionState(txn, g.getId(), sessionId.getBytes());
// get data to connect and derive a shared secret later // get data to connect and derive a shared secret later
long now = clock.currentTimeMillis(); long now = clock.currentTimeMillis();
@@ -169,7 +175,7 @@ class IntroduceeManager {
byte[] publicKey = keyPair.getPublic().getEncoded(); byte[] publicKey = keyPair.getPublic().getEncoded();
byte[] privateKey = keyPair.getPrivate().getEncoded(); byte[] privateKey = keyPair.getPrivate().getEncoded();
Map<TransportId, TransportProperties> transportProperties = Map<TransportId, TransportProperties> transportProperties =
transportPropertyManager.getLocalProperties(); transportPropertyManager.getLocalProperties(txn);
// update session state for later // update session state for later
state.put(ACCEPT, true); state.put(ACCEPT, true);
@@ -182,17 +188,22 @@ class IntroduceeManager {
localAction.put(TYPE, TYPE_RESPONSE); localAction.put(TYPE, TYPE_RESPONSE);
localAction.put(TRANSPORT, localAction.put(TRANSPORT,
encodeTransportProperties(transportProperties)); encodeTransportProperties(transportProperties));
localAction.put(MESSAGE_TIME, timestamp);
// start engine and process its state update // start engine and process its state update
IntroduceeEngine engine = new IntroduceeEngine(); IntroduceeEngine engine = new IntroduceeEngine();
processStateUpdate(txn, engine.onLocalAction(state, localAction)); processStateUpdate(txn, engine.onLocalAction(state, localAction));
} }
public void declineIntroduction(Transaction txn, final SessionId sessionId) public void declineIntroduction(Transaction txn, final ContactId contactId,
final SessionId sessionId, final long timestamp)
throws DbException, FormatException { throws DbException, FormatException {
BdfDictionary state = Contact c = db.getContact(txn, contactId);
introductionManager.getSessionState(txn, sessionId.getBytes()); Group g = introductionManager.getIntroductionGroup(c);
BdfDictionary state = introductionManager
.getSessionState(txn, g.getId(), sessionId.getBytes());
// update session state // update session state
state.put(ACCEPT, false); state.put(ACCEPT, false);
@@ -200,6 +211,7 @@ class IntroduceeManager {
// define action // define action
BdfDictionary localAction = new BdfDictionary(); BdfDictionary localAction = new BdfDictionary();
localAction.put(TYPE, TYPE_RESPONSE); localAction.put(TYPE, TYPE_RESPONSE);
localAction.put(MESSAGE_TIME, timestamp);
// start engine and process its state update // start engine and process its state update
IntroduceeEngine engine = new IntroduceeEngine(); IntroduceeEngine engine = new IntroduceeEngine();

View File

@@ -56,6 +56,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1
import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2; import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2;
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
import static org.briarproject.api.introduction.IntroductionConstants.STATE; import static org.briarproject.api.introduction.IntroductionConstants.STATE;
import static org.briarproject.api.introduction.IntroductionConstants.TIME;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE; import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
@@ -104,6 +105,7 @@ public class IntroducerEngine
if (localAction.containsKey(MSG)) { if (localAction.containsKey(MSG)) {
msg1.put(MSG, localAction.getString(MSG)); msg1.put(MSG, localAction.getString(MSG));
} }
msg1.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
messages.add(msg1); messages.add(msg1);
logLocalAction(currentState, localState, msg1); logLocalAction(currentState, localState, msg1);
BdfDictionary msg2 = new BdfDictionary(); BdfDictionary msg2 = new BdfDictionary();
@@ -115,6 +117,7 @@ public class IntroducerEngine
if (localAction.containsKey(MSG)) { if (localAction.containsKey(MSG)) {
msg2.put(MSG, localAction.getString(MSG)); msg2.put(MSG, localAction.getString(MSG));
} }
msg2.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME));
messages.add(msg2); messages.add(msg2);
logLocalAction(currentState, localState, msg2); logLocalAction(currentState, localState, msg2);

View File

@@ -30,6 +30,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2; import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1; import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1;
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2; import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2;
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
import static org.briarproject.api.introduction.IntroductionConstants.MSG; import static org.briarproject.api.introduction.IntroductionConstants.MSG;
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1; import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1;
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2; import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2;
@@ -38,6 +39,7 @@ import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRO
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
import static org.briarproject.api.introduction.IntroductionConstants.STATE; import static org.briarproject.api.introduction.IntroductionConstants.STATE;
import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID; import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID;
import static org.briarproject.api.introduction.IntroductionConstants.TIME;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE; import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
@@ -99,7 +101,7 @@ class IntroducerManager {
} }
public void makeIntroduction(Transaction txn, Contact c1, Contact c2, public void makeIntroduction(Transaction txn, Contact c1, Contact c2,
String msg) throws DbException, FormatException { String msg, long timestamp) throws DbException, FormatException {
// TODO check for existing session with those contacts? // TODO check for existing session with those contacts?
// deny new introduction under which conditions? // deny new introduction under which conditions?
@@ -115,6 +117,7 @@ class IntroducerManager {
} }
localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey()); localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey());
localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey()); localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey());
localAction.put(MESSAGE_TIME, timestamp);
// start engine and process its state update // start engine and process its state update
IntroducerEngine engine = new IntroducerEngine(); IntroducerEngine engine = new IntroducerEngine();

View File

@@ -44,7 +44,6 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -62,6 +61,8 @@ import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2; import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
import static org.briarproject.api.introduction.IntroductionConstants.EXISTS; import static org.briarproject.api.introduction.IntroductionConstants.EXISTS;
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1;
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2;
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME; import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
import static org.briarproject.api.introduction.IntroductionConstants.MSG; import static org.briarproject.api.introduction.IntroductionConstants.MSG;
import static org.briarproject.api.introduction.IntroductionConstants.NAME; import static org.briarproject.api.introduction.IntroductionConstants.NAME;
@@ -227,8 +228,8 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) { else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) {
BdfDictionary state; BdfDictionary state;
try { try {
state = getSessionState(txn, state = getSessionState(txn, groupId,
message.getRaw(SESSION_ID, new byte[0])); message.getRaw(SESSION_ID));
} catch (FormatException e) { } catch (FormatException e) {
LOG.warning("Could not find state for message, deleting..."); LOG.warning("Could not find state for message, deleting...");
deleteMessage(txn, m.getId()); deleteMessage(txn, m.getId());
@@ -266,12 +267,13 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
} }
@Override @Override
public void makeIntroduction(Contact c1, Contact c2, String msg) public void makeIntroduction(Contact c1, Contact c2, String msg,
final long timestamp)
throws DbException, FormatException { throws DbException, FormatException {
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
introducerManager.makeIntroduction(txn, c1, c2, msg); introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp);
txn.setComplete(); txn.setComplete();
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
@@ -279,12 +281,14 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
} }
@Override @Override
public void acceptIntroduction(final SessionId sessionId) public void acceptIntroduction(final ContactId contactId,
final SessionId sessionId, final long timestamp)
throws DbException, FormatException { throws DbException, FormatException {
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
introduceeManager.acceptIntroduction(txn, sessionId); introduceeManager
.acceptIntroduction(txn, contactId, sessionId, timestamp);
txn.setComplete(); txn.setComplete();
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
@@ -292,12 +296,14 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
} }
@Override @Override
public void declineIntroduction(final SessionId sessionId) public void declineIntroduction(final ContactId contactId,
final SessionId sessionId, final long timestamp)
throws DbException, FormatException { throws DbException, FormatException {
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
introduceeManager.declineIntroduction(txn, sessionId); introduceeManager
.declineIntroduction(txn, contactId, sessionId, timestamp);
txn.setComplete(); txn.setComplete();
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
@@ -322,8 +328,6 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
statuses = db.getMessageStatus(txn, contactId, g); statuses = db.getMessageStatus(txn, contactId, g);
// turn messages into classes for the UI // turn messages into classes for the UI
Map<SessionId, BdfDictionary> sessionStates =
new HashMap<SessionId, BdfDictionary>();
for (MessageStatus s : statuses) { for (MessageStatus s : statuses) {
MessageId messageId = s.getMessageId(); MessageId messageId = s.getMessageId();
BdfDictionary msg = metadata.get(messageId); BdfDictionary msg = metadata.get(messageId);
@@ -335,11 +339,8 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
// get session state // get session state
SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID)); SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID));
BdfDictionary state = sessionStates.get(sessionId); BdfDictionary state =
if (state == null) { getSessionState(txn, g, sessionId.getBytes());
state = getSessionState(txn, sessionId.getBytes());
}
sessionStates.put(sessionId, state);
boolean local; boolean local;
long time = msg.getLong(MESSAGE_TIME); long time = msg.getLong(MESSAGE_TIME);
@@ -453,19 +454,31 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
} }
} }
public BdfDictionary getSessionState(Transaction txn, byte[] sessionId) public BdfDictionary getSessionState(Transaction txn, GroupId groupId,
throws DbException, FormatException { byte[] sessionId) throws DbException, FormatException {
try { try {
return clientHelper.getMessageMetadataAsDictionary(txn, // See if we can find the state directly for the introducer
new MessageId(sessionId)); BdfDictionary state = clientHelper
.getMessageMetadataAsDictionary(txn,
new MessageId(sessionId));
GroupId g1 = new GroupId(state.getRaw(GROUP_ID_1));
GroupId g2 = new GroupId(state.getRaw(GROUP_ID_2));
if (!g1.equals(groupId) && !g2.equals(groupId)) {
throw new NoSuchMessageException();
}
return state;
} catch (NoSuchMessageException e) { } catch (NoSuchMessageException e) {
// State not found directly, so iterate over all states
// to find state for introducee
Map<MessageId, BdfDictionary> map = clientHelper Map<MessageId, BdfDictionary> map = clientHelper
.getMessageMetadataAsDictionary(txn, .getMessageMetadataAsDictionary(txn,
localGroup.getId()); localGroup.getId());
for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
if (Arrays.equals(m.getValue().getRaw(SESSION_ID), sessionId)) { if (Arrays.equals(m.getValue().getRaw(SESSION_ID), sessionId)) {
return m.getValue(); BdfDictionary state = m.getValue();
GroupId g = new GroupId(state.getRaw(GROUP_ID));
if (g.equals(groupId)) return state;
} }
} }
if (LOG.isLoggable(WARNING)) { if (LOG.isLoggable(WARNING)) {
@@ -492,9 +505,10 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook
byte[] body = clientHelper.toByteArray(bdfList); byte[] body = clientHelper.toByteArray(bdfList);
GroupId groupId = new GroupId(message.getRaw(GROUP_ID)); GroupId groupId = new GroupId(message.getRaw(GROUP_ID));
Group group = db.getGroup(txn, groupId); Group group = db.getGroup(txn, groupId);
long timestamp = System.currentTimeMillis(); long timestamp =
message.getLong(MESSAGE_TIME, System.currentTimeMillis());
message.put(MESSAGE_TIME, timestamp); message.put(MESSAGE_TIME, timestamp);
Metadata metadata = metadataEncoder.encode(message); Metadata metadata = metadataEncoder.encode(message);
messageQueueManager messageQueueManager

View File

@@ -108,6 +108,27 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
return Collections.unmodifiableMap(local); return Collections.unmodifiableMap(local);
} }
@Override
public Map<TransportId, TransportProperties> getLocalProperties(
Transaction txn) throws DbException {
try {
Map<TransportId, TransportProperties> local =
new HashMap<TransportId, TransportProperties>();
// Find the latest local update for each transport
Map<TransportId, LatestUpdate> latest = findLatest(txn,
localGroup.getId(), true);
// Retrieve and parse the latest local properties
for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
BdfList message = clientHelper.getMessageAsList(txn,
e.getValue().messageId);
local.put(e.getKey(), parseProperties(message));
}
return local;
} catch (FormatException e) {
throw new DbException(e);
}
}
@Override @Override
public TransportProperties getLocalProperties(TransportId t) public TransportProperties getLocalProperties(TransportId t)
throws DbException { throws DbException {
@@ -212,26 +233,6 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
} }
private Map<TransportId, TransportProperties> getLocalProperties(
Transaction txn) throws DbException {
try {
Map<TransportId, TransportProperties> local =
new HashMap<TransportId, TransportProperties>();
// Find the latest local update for each transport
Map<TransportId, LatestUpdate> latest = findLatest(txn,
localGroup.getId(), true);
// Retrieve and parse the latest local properties
for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
BdfList message = clientHelper.getMessageAsList(txn,
e.getValue().messageId);
local.put(e.getKey(), parseProperties(message));
}
return local;
} catch (FormatException e) {
throw new DbException(e);
}
}
private void storeMessage(Transaction txn, GroupId g, TransportId t, private void storeMessage(Transaction txn, GroupId g, TransportId t,
TransportProperties p, long version, boolean local, boolean shared) TransportProperties p, long version, boolean local, boolean shared)
throws DbException { throws DbException {