diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index cb723afa40e7011d2ce44d7eb0810328c5093bc1..dc0874e3b0cd967d1022cc2af0b6e26c3cda169a 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -90,6 +90,7 @@ <activity android:name=".android.contact.ConversationActivity" android:label="@string/app_name" + android:theme="@style/BriarThemeNoActionBar.Default" android:parentActivityName=".android.NavDrawerActivity" android:windowSoftInputMode="stateHidden"> <meta-data @@ -174,7 +175,16 @@ android:parentActivityName=".android.NavDrawerActivity"> <meta-data 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 diff --git a/briar-android/res/drawable-hdpi/msg_in.9.png b/briar-android/res/drawable-hdpi/msg_in.9.png index 974d60e2d7aeec5229de2b26495af671ddb579ba..20202244fa3d456a7edb956718dbbafdc1a69139 100644 Binary files a/briar-android/res/drawable-hdpi/msg_in.9.png and b/briar-android/res/drawable-hdpi/msg_in.9.png differ diff --git a/briar-android/res/drawable-hdpi/msg_out.9.png b/briar-android/res/drawable-hdpi/msg_out.9.png index 08fd35b4cbdba08150bb224a4981df94431749e7..f8cfdc72674df433a123725aa7cea9593a5cd172 100644 Binary files a/briar-android/res/drawable-hdpi/msg_out.9.png and b/briar-android/res/drawable-hdpi/msg_out.9.png differ diff --git a/briar-android/res/drawable-hdpi/notice_in.9.png b/briar-android/res/drawable-hdpi/notice_in.9.png new file mode 100644 index 0000000000000000000000000000000000000000..5b8dd92ec4b00adc062222f13b8a6545683ca6da Binary files /dev/null and b/briar-android/res/drawable-hdpi/notice_in.9.png differ diff --git a/briar-android/res/drawable-hdpi/notice_out.9.png b/briar-android/res/drawable-hdpi/notice_out.9.png new file mode 100644 index 0000000000000000000000000000000000000000..504fe64d660ae044bd04e325fe8726c139123cc2 Binary files /dev/null and b/briar-android/res/drawable-hdpi/notice_out.9.png differ diff --git a/briar-android/res/drawable-mdpi/msg_in.9.png b/briar-android/res/drawable-mdpi/msg_in.9.png index f9a0267b6e8cc706a1350b1d0fccf6f4a0887b16..cedf69450c3d62696584cb416c5b1c8e98e8afd2 100644 Binary files a/briar-android/res/drawable-mdpi/msg_in.9.png and b/briar-android/res/drawable-mdpi/msg_in.9.png differ diff --git a/briar-android/res/drawable-mdpi/msg_out.9.png b/briar-android/res/drawable-mdpi/msg_out.9.png index f22c541f7f9087c833dd863c6cf07faab5c1d58e..bfca75665bd844e7c47cf8f275aa6e54e7b50ea2 100644 Binary files a/briar-android/res/drawable-mdpi/msg_out.9.png and b/briar-android/res/drawable-mdpi/msg_out.9.png differ diff --git a/briar-android/res/drawable-mdpi/notice_in.9.png b/briar-android/res/drawable-mdpi/notice_in.9.png new file mode 100644 index 0000000000000000000000000000000000000000..afad3fe8655de3f8e46d8fe0ac32afbb07f111dc Binary files /dev/null and b/briar-android/res/drawable-mdpi/notice_in.9.png differ diff --git a/briar-android/res/drawable-mdpi/notice_out.9.png b/briar-android/res/drawable-mdpi/notice_out.9.png new file mode 100644 index 0000000000000000000000000000000000000000..c84866764c4ef993f6179685adcb6a58dd7d84d8 Binary files /dev/null and b/briar-android/res/drawable-mdpi/notice_out.9.png differ diff --git a/briar-android/res/drawable-v21/round_button.xml b/briar-android/res/drawable-v21/round_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..788768ef6e0e69caad294de4161e142d9aac1677 --- /dev/null +++ b/briar-android/res/drawable-v21/round_button.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/drawable-xhdpi/msg_in.9.png b/briar-android/res/drawable-xhdpi/msg_in.9.png index f5db8372dda83faf1082e8700e0c2996c6801fa6..8bf845198fc2c05e65ecd032eedcbae5d706568c 100644 Binary files a/briar-android/res/drawable-xhdpi/msg_in.9.png and b/briar-android/res/drawable-xhdpi/msg_in.9.png differ diff --git a/briar-android/res/drawable-xhdpi/msg_out.9.png b/briar-android/res/drawable-xhdpi/msg_out.9.png index d7c2816f1339f91a2abe9fa4bd1b72b05a5b5f47..dd5521a5fcab3d213175fafc946f172ff22c412d 100644 Binary files a/briar-android/res/drawable-xhdpi/msg_out.9.png and b/briar-android/res/drawable-xhdpi/msg_out.9.png differ diff --git a/briar-android/res/drawable-xhdpi/notice_in.9.png b/briar-android/res/drawable-xhdpi/notice_in.9.png new file mode 100644 index 0000000000000000000000000000000000000000..9af342757d22b3d1e0860f508895ba02d6eb4cdb Binary files /dev/null and b/briar-android/res/drawable-xhdpi/notice_in.9.png differ diff --git a/briar-android/res/drawable-xhdpi/notice_out.9.png b/briar-android/res/drawable-xhdpi/notice_out.9.png new file mode 100644 index 0000000000000000000000000000000000000000..3cd51f5900209c456ab2d48f3505603ba716c8d4 Binary files /dev/null and b/briar-android/res/drawable-xhdpi/notice_out.9.png differ diff --git a/briar-android/res/drawable-xxhdpi/msg_in.9.png b/briar-android/res/drawable-xxhdpi/msg_in.9.png index 3db9979cf1b13128da38558494e6604e730098b6..1330b80faccd4128c7efd9298323a1f0dba317e0 100644 Binary files a/briar-android/res/drawable-xxhdpi/msg_in.9.png and b/briar-android/res/drawable-xxhdpi/msg_in.9.png differ diff --git a/briar-android/res/drawable-xxhdpi/msg_out.9.png b/briar-android/res/drawable-xxhdpi/msg_out.9.png index b7aa02377fd49c6a462c58ed7972719f41ccd978..866bc8c29dd37708cfba9ba592f4c6728fb2d177 100644 Binary files a/briar-android/res/drawable-xxhdpi/msg_out.9.png and b/briar-android/res/drawable-xxhdpi/msg_out.9.png differ diff --git a/briar-android/res/drawable-xxhdpi/notice_in.9.png b/briar-android/res/drawable-xxhdpi/notice_in.9.png new file mode 100644 index 0000000000000000000000000000000000000000..690bdd2994b21459c1d2b3b0ba2c89ddd3f7ef45 Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/notice_in.9.png differ diff --git a/briar-android/res/drawable-xxhdpi/notice_out.9.png b/briar-android/res/drawable-xxhdpi/notice_out.9.png new file mode 100644 index 0000000000000000000000000000000000000000..d1a632b6ed1b0c86dc03f88513465d4068e509ad Binary files /dev/null and b/briar-android/res/drawable-xxhdpi/notice_out.9.png differ diff --git a/briar-android/res/drawable/contact_offline.xml b/briar-android/res/drawable/contact_offline.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac18913fcb5eb66321d44729888b43d03680cde3 --- /dev/null +++ b/briar-android/res/drawable/contact_offline.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/drawable/contact_online.xml b/briar-android/res/drawable/contact_online.xml new file mode 100644 index 0000000000000000000000000000000000000000..f68b831022c7acbe802b7f76664c1a4ddc13cec0 --- /dev/null +++ b/briar-android/res/drawable/contact_online.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/drawable/ic_contact_introduction.xml b/briar-android/res/drawable/ic_contact_introduction.xml new file mode 100644 index 0000000000000000000000000000000000000000..9395c7b93e79675f0ab31d05363b7aff5b91cfd1 --- /dev/null +++ b/briar-android/res/drawable/ic_contact_introduction.xml @@ -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> diff --git a/briar-android/res/drawable/introduction_notification.xml b/briar-android/res/drawable/introduction_notification.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac4328d1210e12e8446ff705716d5364a3532887 --- /dev/null +++ b/briar-android/res/drawable/introduction_notification.xml @@ -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> diff --git a/briar-android/res/drawable/introduction_white.xml b/briar-android/res/drawable/introduction_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac4328d1210e12e8446ff705716d5364a3532887 --- /dev/null +++ b/briar-android/res/drawable/introduction_white.xml @@ -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> diff --git a/briar-android/res/drawable/message_delivered_white.xml b/briar-android/res/drawable/message_delivered_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..720dab1f7101da2f5d3d04724491c6829680eadb --- /dev/null +++ b/briar-android/res/drawable/message_delivered_white.xml @@ -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> diff --git a/briar-android/res/drawable/message_sent_white.xml b/briar-android/res/drawable/message_sent_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..59e6d6d1dde7e9662152b1ae9dc6b3b7ac6bef80 --- /dev/null +++ b/briar-android/res/drawable/message_sent_white.xml @@ -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> diff --git a/briar-android/res/drawable/message_stored_white.xml b/briar-android/res/drawable/message_stored_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..71ee22feaa013232c6218686372d94c418ebcace --- /dev/null +++ b/briar-android/res/drawable/message_stored_white.xml @@ -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> diff --git a/briar-android/res/drawable/round_button.xml b/briar-android/res/drawable/round_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..421deb97a50c82b5f98e895718bbd82feb3afa71 --- /dev/null +++ b/briar-android/res/drawable/round_button.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/drawable/social_send_now_white.xml b/briar-android/res/drawable/social_send_now_white.xml new file mode 100644 index 0000000000000000000000000000000000000000..43662f48b70e19b8963903ddfa5566568e10a2e0 --- /dev/null +++ b/briar-android/res/drawable/social_send_now_white.xml @@ -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> diff --git a/briar-android/res/layout/activity_contact_list.xml b/briar-android/res/layout/activity_contact_list.xml index 333dac2035f20f4006695966a4c3890c68c6373b..5f1bb33709f91ac590c48bfdbc0a3f509675bd7a 100644 --- a/briar-android/res/layout/activity_contact_list.xml +++ b/briar-android/res/layout/activity_contact_list.xml @@ -7,7 +7,6 @@ android:layout_height="match_parent"> <org.briarproject.android.util.BriarRecyclerView - xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/contactList" android:layout_width="match_parent" android:layout_height="match_parent"/> diff --git a/briar-android/res/layout/activity_conversation.xml b/briar-android/res/layout/activity_conversation.xml index 88066e9956e58f39b0f99d1b5730e5eb36b71490..d9c86d64d6204b7dcd2a10bc4f523969d00001d2 100644 --- a/briar-android/res/layout/activity_conversation.xml +++ b/briar-android/res/layout/activity_conversation.xml @@ -1,46 +1,79 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" 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 android:id="@+id/conversationView" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_weight="1"/> + android:layout_weight="1" + android:background="@color/conversation_background"/> <View style="@style/Divider.Horizontal"/> <LinearLayout - android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/button_bar_background" + android:orientation="horizontal" android:paddingLeft="@dimen/margin_medium" android:paddingStart="@dimen/margin_medium"> <EditText android:id="@+id/contentView" android:layout_width="0dp" - android:layout_height="wrap_content" - android:hint="@string/private_message_hint" + android:layout_height="match_parent" android:layout_weight="1" + android:hint="@string/private_message_hint" android:inputType="text|textMultiLine|textCapSentences"/> <ImageButton android:id="@+id/sendButton" android:layout_width="38dp" android:layout_height="38dp" - android:layout_gravity="bottom" - android:src="@drawable/social_send_now" - android:background="?attr/selectableItemBackground" - android:scaleType="fitEnd" + android:layout_margin="@dimen/margin_small" + android:background="@drawable/round_button" + android:src="@drawable/social_send_now_white" android:contentDescription="@string/send" - android:paddingRight="@dimen/margin_medium" - android:paddingEnd="@dimen/margin_medium" - android:paddingBottom="@dimen/margin_medium"/> + android:elevation="@dimen/margin_tiny" + /> + </LinearLayout> </LinearLayout> \ No newline at end of file diff --git a/briar-android/res/layout/activity_introduction.xml b/briar-android/res/layout/activity_introduction.xml new file mode 100644 index 0000000000000000000000000000000000000000..f351897d0f7b281b42f63847603b676d7f5bf7d2 --- /dev/null +++ b/briar-android/res/layout/activity_introduction.xml @@ -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"/> \ No newline at end of file diff --git a/briar-android/res/layout/contact_avatar_status.xml b/briar-android/res/layout/contact_avatar_status.xml new file mode 100644 index 0000000000000000000000000000000000000000..2ceb5954726ca3654f09ba326b9ac8b1978fc0f9 --- /dev/null +++ b/briar-android/res/layout/contact_avatar_status.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/introduction_contact_chooser.xml b/briar-android/res/layout/introduction_contact_chooser.xml new file mode 100644 index 0000000000000000000000000000000000000000..8363191ac0db145c3086706f2f9408fbc479a1f6 --- /dev/null +++ b/briar-android/res/layout/introduction_contact_chooser.xml @@ -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"/> diff --git a/briar-android/res/layout/introduction_message.xml b/briar-android/res/layout/introduction_message.xml new file mode 100644 index 0000000000000000000000000000000000000000..7faf8bf555526cf4a6180935c92788d9b1e6e09b --- /dev/null +++ b/briar-android/res/layout/introduction_message.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_contact.xml b/briar-android/res/layout/list_item_contact.xml index b566dc171388e1eee1ef863011dd40470211cff6..8f879c90a041b99df3882897e259ba47db12e6b0 100644 --- a/briar-android/res/layout/list_item_contact.xml +++ b/briar-android/res/layout/list_item_contact.xml @@ -1,16 +1,19 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout 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:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" - android:layout_height="@dimen/listitem_height_one_line_avatar" - android:background="?attr/selectableItemBackground"> + android:layout_height="wrap_content" + android:paddingTop="@dimen/listitem_horizontal_margin" + android:paddingBottom="@dimen/listitem_horizontal_margin" + android:background="?attr/selectableItemBackground" + > <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/avatarView" @@ -21,56 +24,62 @@ android:layout_centerVertical="true" android:layout_marginLeft="@dimen/listitem_horizontal_margin" android:layout_marginStart="@dimen/listitem_horizontal_margin" + android:transitionName="avatar" + app:civ_border_color="@color/briar_text_primary" app:civ_border_width="@dimen/avatar_border_width" - app:civ_border_color="@color/briar_text_primary"/> + tools:src="@drawable/ic_launcher"/> <LinearLayout - android:id="@+id/bulbHolder" + android:id="@+id/textViews" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentEnd="true" - android:layout_alignParentRight="true" + android:orientation="vertical" android:layout_centerVertical="true" - android:layout_marginEnd="@dimen/listitem_horizontal_margin" - android:layout_marginRight="@dimen/listitem_horizontal_margin" - android:gravity="right" - android:orientation="vertical"> + android:layout_marginLeft="@dimen/listitem_horizontal_margin" + android:layout_marginStart="@dimen/listitem_horizontal_margin" + android:layout_toLeftOf="@+id/bulbView" + android:layout_toRightOf="@+id/avatarView" + android:layout_toEndOf="@+id/avatarView"> - <ImageView - android:id="@+id/bulbView" + <TextView + android:id="@+id/nameView" android:layout_width="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 android:id="@+id/dateView" android:layout_width="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"/> + <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> - <TextView - android:id="@+id/nameView" + <ImageView + android:id="@+id/bulbView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentLeft="true" - android:layout_alignParentStart="true" + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true" android:layout_centerVertical="true" - android:layout_marginEnd="@dimen/margin_small" - android:layout_marginLeft="@dimen/listitem_text_left_margin" - 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."/> + android:layout_marginRight="@dimen/listitem_horizontal_margin" + tools:src="@drawable/contact_connected"/> </RelativeLayout> - <View style="@style/Divider.Horizontal"/> + <View style="@style/Divider.ContactList"/> </LinearLayout> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_introduction_in.xml b/briar-android/res/layout/list_item_introduction_in.xml new file mode 100644 index 0000000000000000000000000000000000000000..5daea060852a74a324f23c79cd851ad1fee3ea7a --- /dev/null +++ b/briar-android/res/layout/list_item_introduction_in.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_introduction_out.xml b/briar-android/res/layout/list_item_introduction_out.xml new file mode 100644 index 0000000000000000000000000000000000000000..88070ea669ea57ab6402247f6c18cebae68eecb9 --- /dev/null +++ b/briar-android/res/layout/list_item_introduction_out.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_msg_in.xml b/briar-android/res/layout/list_item_msg_in.xml index fa1f793ab7cea5e936a14e89100be769182ff855..13ec9b28703386ba75908e1bfe1c03357941f12e 100644 --- a/briar-android/res/layout/list_item_msg_in.xml +++ b/briar-android/res/layout/list_item_msg_in.xml @@ -1,53 +1,51 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout 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:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal" - android:paddingRight="@dimen/margin_medium" - android:paddingEnd="@dimen/margin_medium" - android:paddingTop="@dimen/margin_small" - android:paddingBottom="@dimen/margin_small"> + android:orientation="horizontal"> <de.hdodenhof.circleimageview.CircleImageView android:id="@+id/msgAvatar" android:layout_width="@dimen/listitem_picture_size" android:layout_height="@dimen/listitem_picture_size" - android:layout_marginLeft="@dimen/listitem_horizontal_margin" - android:layout_marginStart="@dimen/listitem_horizontal_margin" + android:layout_marginLeft="@dimen/margin_medium" + 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_color="@color/briar_text_primary"/> + tools:src="@drawable/ic_launcher"/> - <RelativeLayout + <LinearLayout android:id="@+id/msgLayout" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="left|start" android:background="@drawable/msg_in" - android:paddingLeft="17dp" - android:paddingTop="5dp" - android:paddingRight="7dp" - android:paddingBottom="5dp"> + android:orientation="vertical" + android:layout_marginLeft="@dimen/message_bubble_margin_tail" + android:layout_marginRight="@dimen/message_bubble_margin_non_tail"> <TextView android:id="@+id/msgBody" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:minWidth="80dp" android:textIsSelectable="true" + android:textSize="@dimen/text_size_medium" tools:text="Short message"/> <TextView android:id="@+id/msgTime" android:layout_width="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:layout_below="@+id/msgBody" + android:textSize="@dimen/text_size_tiny" tools:text="Dec 24, 13:37"/> - </RelativeLayout> + </LinearLayout> </LinearLayout> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_msg_out.xml b/briar-android/res/layout/list_item_msg_out.xml index a82a07f1e058be89c27bbaa346025775ac888005..5902b7381e122a3791fa3e9e8843920020f20ce9 100644 --- a/briar-android/res/layout/list_item_msg_out.xml +++ b/briar-android/res/layout/list_item_msg_out.xml @@ -4,11 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:paddingLeft="@dimen/margin_medium" - android:paddingStart="@dimen/margin_medium" - android:paddingTop="@dimen/margin_small" - android:paddingBottom="@dimen/margin_small"> + android:orientation="vertical"> <RelativeLayout android:id="@+id/msgLayout" @@ -16,28 +12,29 @@ android:layout_height="wrap_content" android:layout_gravity="right|end" android:background="@drawable/msg_out" - android:paddingLeft="7dp" - android:paddingTop="5dp" - android:paddingRight="17dp" - android:paddingBottom="5dp"> + android:layout_marginLeft="@dimen/message_bubble_margin_non_tail" + android:layout_marginRight="@dimen/message_bubble_margin_tail"> <TextView android:id="@+id/msgBody" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:textColor="@color/briar_text_primary_inverse" 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."/> <TextView android:id="@+id/msgTime" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/message_bubble_timestamp_margin" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" android:layout_below="@+id/msgBody" - android:layout_toLeftOf="@+id/msgStatus" - android:textSize="10sp" - android:textColor="@color/private_message_date" android:singleLine="true" + android:textColor="@color/private_message_date_inverse" + android:textSize="@dimen/text_size_tiny" tools:text="Dec 24, 13:37"/> <ImageView @@ -45,10 +42,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@+id/msgTime" - android:layout_alignRight="@+id/msgBody" - android:layout_alignEnd="@+id/msgBody" - android:layout_marginLeft="3dp" - tools:src="@drawable/message_delivered"/> + android:layout_marginLeft="@dimen/margin_medium" + android:layout_toEndOf="@+id/msgTime" + android:layout_toRightOf="@+id/msgTime" + tools:ignore="ContentDescription" + tools:src="@drawable/message_delivered_white"/> </RelativeLayout> diff --git a/briar-android/res/layout/list_item_notice_in.xml b/briar-android/res/layout/list_item_notice_in.xml new file mode 100644 index 0000000000000000000000000000000000000000..8f0daa0267b56790c8e1e9d4397ad01ae9819f13 --- /dev/null +++ b/briar-android/res/layout/list_item_notice_in.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/layout/list_item_notice_out.xml b/briar-android/res/layout/list_item_notice_out.xml new file mode 100644 index 0000000000000000000000000000000000000000..499e1506fd282fc39b5dcaba798c0d9149c8c644 --- /dev/null +++ b/briar-android/res/layout/list_item_notice_out.xml @@ -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> \ No newline at end of file diff --git a/briar-android/res/menu/contact_actions.xml b/briar-android/res/menu/conversation_actions.xml similarity index 53% rename from briar-android/res/menu/contact_actions.xml rename to briar-android/res/menu/conversation_actions.xml index 3cce37ad83fcdac5083f7620d935e4184ce66038..0128ee56f42686c6db67bb3934aab796cb06ecb0 100644 --- a/briar-android/res/menu/contact_actions.xml +++ b/briar-android/res/menu/conversation_actions.xml @@ -3,10 +3,16 @@ xmlns:android="http://schemas.android.com/apk/res/android" 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 android:id="@+id/action_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> \ No newline at end of file diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml index bcb8d2b0ae8896df2e09721f3a8f0dad98fea9d7..0351b52966f0e49dfea5076cc3c39fa60a37c108 100644 --- a/briar-android/res/values/color.xml +++ b/briar-android/res/values/color.xml @@ -8,11 +8,13 @@ <color name="briar_red">#C1392B</color> <color name="window_background">#EEEEEE</color> + <color name="conversation_background">#efebe9</color> <color name="action_bar_text">#FFFFFF</color> <color name="action_bar_background">@color/briar_blue</color> <color name="button_bar_background">#FFFFFF</color> <color name="dashboard_background">#FFFFFF</color> <color name="private_message_date">#AAAAAA</color> + <color name="private_message_date_inverse">#e0e0e0</color> <color name="unread_background">#FFFFFF</color> <color name="horizontal_border">#CCCCCC</color> <color name="forums_available_background">@color/briar_gold</color> @@ -28,6 +30,8 @@ <color name="briar_text_link">@color/briar_green_dark</color> <color name="briar_text_primary">@color/briar_primary</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 --> <color name="preference_fallback_accent_color">@color/briar_accent</color> diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml index 41af17d52a0e4d0a1bc44e1c5ff5adbe7d955904..b1985532c12ca9fcfb5e2018686ec5a1e1dcdc65 100644 --- a/briar-android/res/values/dimens.xml +++ b/briar-android/res/values/dimens.xml @@ -23,8 +23,12 @@ <dimen name="listitem_horizontal_margin">16dp</dimen> <dimen name="listitem_text_left_margin">72dp</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="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> diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml index f75d8a90d2a549e18c3bbd8fc1bd0938867c8f70..bfd82ba9f3f31685d8a3ea480a0b6979f9c9d5d8 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -142,6 +142,27 @@ <string name="no_data">No data</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 --> <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> @@ -152,6 +173,9 @@ <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_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 --> <string name="dashboard_toolbar_header">Briar</string> <string name="settings_toolbar_header">Settings</string> diff --git a/briar-android/res/values/styles.xml b/briar-android/res/values/styles.xml index 4c80c10edea097d5ccb541a2a71d07e278b296d2..12c1c35971598950fed3e2c806f81038895ae49d 100644 --- a/briar-android/res/values/styles.xml +++ b/briar-android/res/values/styles.xml @@ -51,13 +51,31 @@ <item name="elevation">1dp</item> </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:padding">@dimen/margin_large</item> </style> <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"> <item name="android:textSize">@dimen/text_size_medium</item> <item name="android:textColor">@android:color/primary_text_light</item> @@ -76,11 +94,17 @@ <item name="android:background">?android:attr/listDivider</item> </style> - <style name="Divider.Horizontal"> + <style name="Divider.Horizontal" parent="Divider"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">1px</item> </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"> <item name="android:textSize">@dimen/text_size_medium</item> <item name="android:textColor">@android:color/tertiary_text_light</item> diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index 4b3caa9967b667c6ea56f04058dfc86c9550c4f8..5b73d5e53bb1e57ab287a93ecebe04a9a5e3a414 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -12,6 +12,9 @@ import org.briarproject.android.forum.ReadForumPostActivity; import org.briarproject.android.forum.ShareForumActivity; import org.briarproject.android.forum.WriteForumPostActivity; 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.keyagreement.ChooseIdentityFragment; import org.briarproject.android.keyagreement.KeyAgreementActivity; @@ -80,6 +83,12 @@ public interface AndroidComponent extends CoreEagerSingletons { void inject(ShowQrCodeFragment fragment); + void inject(IntroductionActivity activity); + + void inject(ContactChooserFragment fragment); + + void inject(IntroductionMessageFragment fragment); + // Eager singleton load void inject(AppModule.EagerSingletons init); } diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java index 3cd074aea55c923ec0588a9772376e5640aa9e5f..17f8fbac79365c602896712e0ceaf7060d379967 100644 --- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java @@ -14,10 +14,15 @@ import org.briarproject.android.api.AndroidExecutor; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.contact.ConversationActivity; import org.briarproject.android.forum.ForumActivity; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.db.DbException; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.IntroductionRequestReceivedEvent; +import org.briarproject.api.event.IntroductionResponseReceivedEvent; +import org.briarproject.api.event.IntroductionSucceededEvent; import org.briarproject.api.event.MessageValidatedEvent; import org.briarproject.api.event.SettingsUpdatedEvent; import org.briarproject.api.forum.ForumManager; @@ -57,6 +62,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3; private static final int FORUM_POST_NOTIFICATION_ID = 4; + private static final int INTRODUCTION_SUCCESS_NOTIFICATION_ID = 5; private static final String CONTACT_URI = "content://org.briarproject/contact"; private static final String FORUM_URI = @@ -111,6 +117,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, public Void call() { clearPrivateMessageNotification(); clearForumPostNotification(); + clearIntroductionSuccessNotification(); return null; } }); @@ -135,6 +142,12 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, 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) { if (e instanceof SettingsUpdatedEvent) { SettingsUpdatedEvent s = (SettingsUpdatedEvent) e; @@ -148,6 +161,15 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, else if (c.equals(forumManager.getClientId())) 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()); + } + }); + } + } diff --git a/briar-android/src/org/briarproject/android/BaseActivity.java b/briar-android/src/org/briarproject/android/BaseActivity.java index 8e92c0b56f44ed5fa793e710303f44e523b62544..1a93a006ec6b200e2522e8431c4168eefc1efdbc 100644 --- a/briar-android/src/org/briarproject/android/BaseActivity.java +++ b/briar-android/src/org/briarproject/android/BaseActivity.java @@ -54,7 +54,7 @@ public abstract class BaseActivity extends AppCompatActivity { ((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT); } - protected void hideSoftKeyboard(View view) { + public void hideSoftKeyboard(View view) { IBinder token = view.getWindowToken(); Object o = getSystemService(INPUT_METHOD_SERVICE); ((InputMethodManager) o).hideSoftInputFromWindow(token, 0); diff --git a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java index 38c64e839cdb90e56c11d1c0b936846256de1597..becdaa9220d13699c91b10f66f6f4f3fe9c3366a 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListAdapter.java @@ -1,8 +1,12 @@ package org.briarproject.android.contact; import android.content.Context; -import android.content.Intent; -import android.content.res.Resources; +import android.graphics.Color; +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.widget.RecyclerView; import android.text.format.DateUtils; @@ -14,9 +18,8 @@ import android.widget.TextView; import org.briarproject.R; import org.briarproject.api.contact.ContactId; -import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.identity.Author; -import org.briarproject.api.sync.GroupId; +import org.briarproject.api.identity.AuthorId; import java.util.List; @@ -53,13 +56,23 @@ public class ContactListAdapter @Override public int compare(ContactListItem c1, ContactListItem c2) { - // 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; + int authorCompare = 0; + if (chooser) { + authorCompare = c1.getLocalAuthor().getName() + .compareTo( + c2.getLocalAuthor().getName()); + } + 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 @@ -86,10 +99,16 @@ public class ContactListAdapter return true; } }); + private final OnItemClickListener listener; + private final boolean chooser; private Context ctx; + private AuthorId localAuthorId; - public ContactListAdapter(Context context) { + public ContactListAdapter(Context context, OnItemClickListener listener, + boolean chooser) { ctx = context; + this.listener = listener; + this.chooser = chooser; } @Override @@ -103,12 +122,11 @@ public class ContactListAdapter @Override public void onBindViewHolder(final ContactHolder ui, final int position) { final ContactListItem item = getItem(position); - Resources res = ctx.getResources(); int unread = item.getUnreadCount(); - if (unread > 0) { + if (!chooser && unread > 0) { ui.layout.setBackgroundColor( - res.getColor(R.color.unread_background)); + ContextCompat.getColor(ctx, R.color.unread_background)); } if (item.isConnected()) { @@ -121,27 +139,37 @@ public class ContactListAdapter ui.avatar.setImageDrawable( new IdenticonDrawable(author.getId().getBytes())); 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 + ")"); } else { ui.name.setText(contactName); } + if (chooser) { + ui.identity.setText(item.getLocalAuthor().getName()); + } else { + ui.identity.setVisibility(View.GONE); + } + if (item.isEmpty()) { ui.date.setText(R.string.no_private_messages); } else { + // TODO show this as X units ago long timestamp = item.getTimestamp(); ui.date.setText( DateUtils.getRelativeTimeSpanString(ctx, timestamp)); } + if (chooser && !item.getLocalAuthor().getId().equals(localAuthorId)) { + grayOutItem(ui); + } + ui.layout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - GroupId groupId = item.getGroupId(); - Intent i = new Intent(ctx, ConversationActivity.class); - i.putExtra("briar.GROUP_ID", groupId.getBytes()); - ctx.startActivity(i); + listener.onItemClick(ui.avatar, item); } }); } @@ -151,6 +179,34 @@ public class ContactListAdapter 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) { if (position == INVALID_POSITION || contacts.size() <= position) { return null; // Not found @@ -162,10 +218,6 @@ public class ContactListAdapter contacts.updateItemAt(position, item); } - public int findItemPosition(ContactListItem item) { - return contacts.indexOf(item); - } - public int findItemPosition(ContactId c) { int count = getItemCount(); for (int i = 0; i < count; i++) { @@ -202,6 +254,7 @@ public class ContactListAdapter public ImageView bulb; public ImageView avatar; public TextView name; + public TextView identity; public TextView date; public ContactHolder(View v) { @@ -211,7 +264,13 @@ public class ContactListAdapter bulb = (ImageView) v.findViewById(R.id.bulbView); avatar = (ImageView) v.findViewById(R.id.avatarView); name = (TextView) v.findViewById(R.id.nameView); + identity = (TextView) v.findViewById(R.id.identityView); date = (TextView) v.findViewById(R.id.dateView); } } + + public interface OnItemClickListener { + void onItemClick(View view, ContactListItem item); + } + } diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java index 0d7c5d32356547ca4c661554b99c46866979422b..0556310213d62d97d2a6aaad5e546dc51d250386 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -1,9 +1,11 @@ package org.briarproject.android.contact; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.FloatingActionButton; +import android.support.v4.app.ActivityOptionsCompat; import android.support.v7.widget.LinearLayoutManager; import android.view.LayoutInflater; import android.view.View; @@ -11,23 +13,26 @@ import android.view.ViewGroup; import org.briarproject.R; import org.briarproject.android.AndroidComponent; -import org.briarproject.android.BriarApplication; import org.briarproject.android.fragment.BaseEventFragment; import org.briarproject.android.keyagreement.KeyAgreementActivity; 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.crypto.CryptoComponent; import org.briarproject.api.db.DbException; import org.briarproject.api.db.NoSuchContactException; import org.briarproject.api.event.ContactAddedEvent; import org.briarproject.api.event.ContactConnectedEvent; import org.briarproject.api.event.ContactDisconnectedEvent; import org.briarproject.api.event.ContactRemovedEvent; +import org.briarproject.api.event.ContactStatusChangedEvent; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; 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.PrivateMessageHeader; import org.briarproject.api.plugins.ConnectionRegistry; @@ -74,8 +79,12 @@ public class ContactListFragment extends BaseEventFragment { @Inject protected volatile ContactManager contactManager; @Inject + protected volatile IdentityManager identityManager; + @Inject protected volatile MessagingManager messagingManager; @Inject + protected volatile IntroductionManager introductionManager; + @Inject protected volatile EventBus eventBus; @Override @@ -91,7 +100,31 @@ public class ContactListFragment extends BaseEventFragment { inflater.inflate(R.layout.activity_contact_list, container, 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.setLayoutManager(new LinearLayoutManager(getContext())); list.setAdapter(adapter); @@ -135,12 +168,14 @@ public class ContactListFragment extends BaseEventFragment { ContactId id = c.getId(); GroupId groupId = messagingManager.getConversationId(id); - Collection<PrivateMessageHeader> headers = - messagingManager.getMessageHeaders(id); + Collection<ConversationItem> messages = + getMessages(id); boolean connected = connectionRegistry.isConnected(c.getId()); - contacts.add(new ContactListItem(c, connected, - groupId, headers)); + LocalAuthor localAuthor = identityManager + .getLocalAuthor(c.getLocalAuthorId()); + contacts.add(new ContactListItem(c, localAuthor, + connected, groupId, messages)); } catch (NoSuchContactException e) { // Continue } @@ -169,7 +204,12 @@ public class ContactListFragment extends BaseEventFragment { public void eventOccurred(Event e) { 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(); } else if (e instanceof ContactConnectedEvent) { setConnected(((ContactConnectedEvent) e).getContactId(), true); @@ -181,7 +221,8 @@ public class ContactListFragment extends BaseEventFragment { } else if (e instanceof MessageValidatedEvent) { MessageValidatedEvent m = (MessageValidatedEvent) e; 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"); reloadConversation(m.getMessage().getGroupId()); } @@ -192,14 +233,10 @@ public class ContactListFragment extends BaseEventFragment { listener.runOnDbThread(new Runnable() { public void run() { try { - long now = System.currentTimeMillis(); ContactId c = messagingManager.getContactId(g); - Collection<PrivateMessageHeader> headers = - messagingManager.getMessageHeaders(c); - long duration = System.currentTimeMillis() - now; - if (LOG.isLoggable(INFO)) - LOG.info("Partial load took " + duration + " ms"); - updateItem(c, headers); + Collection<ConversationItem> messages = + getMessages(c); + updateItem(c, messages); } catch (NoSuchContactException e) { LOG.info("Contact removed"); } catch (DbException e) { @@ -211,13 +248,13 @@ public class ContactListFragment extends BaseEventFragment { } private void updateItem(final ContactId c, - final Collection<PrivateMessageHeader> headers) { + final Collection<ConversationItem> messages) { listener.runOnUiThread(new Runnable() { public void run() { int position = adapter.findItemPosition(c); ContactListItem item = adapter.getItem(position); if (item != null) { - item.setHeaders(headers); + item.setMessages(messages); 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; + } } diff --git a/briar-android/src/org/briarproject/android/contact/ContactListItem.java b/briar-android/src/org/briarproject/android/contact/ContactListItem.java index 2addb9e9d157a234a4554778e76099b7d7f3f636..c152d20c8b3ffdf18b73a39adc217b1a10bad8d9 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListItem.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListItem.java @@ -1,44 +1,55 @@ package org.briarproject.android.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 java.util.Collection; +import static org.briarproject.android.contact.ConversationItem.IncomingItem; + // This class is not thread-safe -class ContactListItem { +public class ContactListItem { private final Contact contact; + private final LocalAuthor localAuthor; private final GroupId groupId; private boolean connected, empty; private long timestamp; private int unread; - ContactListItem(Contact contact, boolean connected, GroupId groupId, - Collection<PrivateMessageHeader> headers) { + public ContactListItem(Contact contact, LocalAuthor localAuthor, + boolean connected, + GroupId groupId, + Collection<ConversationItem> messages) { this.contact = contact; + this.localAuthor = localAuthor; this.groupId = groupId; this.connected = connected; - setHeaders(headers); + setMessages(messages); } - void setHeaders(Collection<PrivateMessageHeader> headers) { - empty = headers.isEmpty(); + void setMessages(Collection<ConversationItem> messages) { + empty = messages.isEmpty(); timestamp = 0; unread = 0; if (!empty) { - for (PrivateMessageHeader h : headers) { - if (h.getTimestamp() > timestamp) timestamp = h.getTimestamp(); - if (!h.isRead()) unread++; + for (ConversationItem i : messages) { + if (i.getTime() > timestamp) timestamp = i.getTime(); + if (i instanceof IncomingItem && !((IncomingItem) i).isRead()) + unread++; } } } - Contact getContact() { + public Contact getContact() { return contact; } + public LocalAuthor getLocalAuthor() { + return localAuthor; + } + GroupId getGroupId() { return groupId; } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index 45ebe6eab9dbbe9bf053731d975763393f8f7658..ae09f0a416a405e78eb7225816b5b411a6450688 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -2,11 +2,15 @@ package org.briarproject.android.contact; import android.content.DialogInterface; import android.content.Intent; -import android.graphics.PorterDuff; 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.AlertDialog; import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.Toolbar; +import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -14,14 +18,17 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.EditText; import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; import android.widget.Toast; import org.briarproject.R; import org.briarproject.android.AndroidComponent; 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.api.FormatException; -import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; 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.EventBus; 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.MessagesAckedEvent; 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.PrivateMessage; import org.briarproject.api.messaging.PrivateMessageFactory; @@ -61,12 +75,18 @@ import java.util.logging.Logger; import javax.inject.Inject; +import de.hdodenhof.circleimageview.CircleImageView; +import im.delight.android.identicons.IdenticonDrawable; + import static android.widget.Toast.LENGTH_SHORT; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; +import static org.briarproject.android.contact.ConversationItem.OutgoingItem; +import static org.briarproject.android.contact.ConversationItem.IncomingItem; public class ConversationActivity extends BriarActivity - implements EventListener, OnClickListener { + implements EventListener, OnClickListener, + ConversationAdapter.IntroductionHandler { private static final Logger LOG = Logger.getLogger(ConversationActivity.class.getName()); @@ -76,6 +96,9 @@ public class ConversationActivity extends BriarActivity @Inject @CryptoExecutor protected Executor cryptoExecutor; private Map<MessageId, byte[]> bodyCache = new HashMap<MessageId, byte[]>(); private ConversationAdapter adapter = null; + private CircleImageView toolbarAvatar; + private ImageView toolbarStatus; + private TextView toolbarTitle; private BriarRecyclerView list = null; private EditText content = null; private ImageButton sendButton = null; @@ -85,6 +108,7 @@ public class ConversationActivity extends BriarActivity @Inject protected volatile MessagingManager messagingManager; @Inject protected volatile EventBus eventBus; @Inject protected volatile PrivateMessageFactory privateMessageFactory; + @Inject protected volatile IntroductionManager introductionManager; private volatile GroupId groupId = null; private volatile ContactId contactId = null; private volatile String contactName = null; @@ -102,7 +126,21 @@ public class ConversationActivity extends BriarActivity 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.setLayoutManager(new LinearLayoutManager(this)); list.setAdapter(adapter); @@ -125,8 +163,7 @@ public class ConversationActivity extends BriarActivity eventBus.addListener(this); notificationManager.blockNotification(groupId); notificationManager.clearPrivateMessageNotification(groupId); - loadContactDetails(); - loadHeaders(); + loadData(); } @Override @@ -141,12 +178,10 @@ public class ConversationActivity extends BriarActivity public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu items for use in the action bar MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.contact_actions, menu); + inflater.inflate(R.menu.conversation_actions, menu); - // Adapt icon color to dark action bar - menu.findItem(R.id.action_social_remove_person).getIcon().setColorFilter( - getResources().getColor(R.color.action_bar_text), - PorterDuff.Mode.SRC_IN); + hideIntroductionActionWhenOneContact( + menu.findItem(R.id.action_introduction)); return super.onCreateOptionsMenu(menu); } @@ -155,6 +190,19 @@ public class ConversationActivity extends BriarActivity public boolean onOptionsItemSelected(final MenuItem item) { // Handle presses on the action bar items 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: askToRemoveContact(); return true; @@ -163,20 +211,26 @@ public class ConversationActivity extends BriarActivity } } - private void loadContactDetails() { + private void loadData() { runOnDbThread(new Runnable() { public void run() { try { long now = System.currentTimeMillis(); - contactId = messagingManager.getContactId(groupId); - Contact contact = contactManager.getContact(contactId); - contactName = contact.getAuthor().getName(); - contactIdenticonKey = contact.getAuthor().getId().getBytes(); + if (contactId == null) + contactId = messagingManager.getContactId(groupId); + if (contactName == null || contactIdenticonKey == null) { + Contact contact = contactManager.getContact(contactId); + contactName = contact.getAuthor().getName(); + contactIdenticonKey = + contact.getAuthor().getId().getBytes(); + } connected = connectionRegistry.isConnected(contactId); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Loading contact took " + duration + " ms"); displayContactDetails(); + // Load the messages here to make sure we have a contactId + loadMessages(); } catch (NoSuchContactException e) { finishOnUiThread(); } catch (DbException e) { @@ -190,31 +244,44 @@ public class ConversationActivity extends BriarActivity private void displayContactDetails() { runOnUiThread(new Runnable() { public void run() { - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(contactName); - if (connected) { - actionBar.setSubtitle(getString(R.string.online)); - } else { - actionBar.setSubtitle(getString(R.string.offline)); - } + toolbarAvatar.setImageDrawable( + new IdenticonDrawable(contactIdenticonKey)); + toolbarTitle.setText(contactName); + + if (connected) { + toolbarStatus.setImageDrawable(ContextCompat + .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() { public void run() { try { long now = System.currentTimeMillis(); + if (contactId == null) + contactId = messagingManager.getContactId(groupId); Collection<PrivateMessageHeader> headers = messagingManager.getMessageHeaders(contactId); + Collection<IntroductionMessage> introductions = + introductionManager + .getIntroductionMessages(contactId); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) LOG.info("Loading headers took " + duration + " ms"); - displayHeaders(headers); + displayMessages(headers, introductions); } catch (NoSuchContactException e) { finishOnUiThread(); } catch (DbException e) { @@ -225,23 +292,38 @@ public class ConversationActivity extends BriarActivity }); } - private void displayHeaders( - final Collection<PrivateMessageHeader> headers) { + private void displayMessages(final Collection<PrivateMessageHeader> headers, + final Collection<IntroductionMessage> introductions) { runOnUiThread(new Runnable() { public void run() { sendButton.setEnabled(true); - if (headers.isEmpty()) { + if (headers.isEmpty() && introductions.isEmpty()) { // we have no messages, // so let the list know to hide progress bar list.showData(); } else { for (PrivateMessageHeader h : headers) { - ConversationItem item = new ConversationItem(h); + ConversationMessageItem item = + (ConversationMessageItem) ConversationItem + .from(h); byte[] body = bodyCache.get(h.getId()); if (body == null) loadMessageBody(h); else item.setBody(body); 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 list.scrollToPosition(adapter.getItemCount() - 1); } @@ -273,14 +355,14 @@ public class ConversationActivity extends BriarActivity runOnUiThread(new Runnable() { public void run() { bodyCache.put(m, body); - int count = adapter.getItemCount(); - for (int i = 0; i < count; i++) { - ConversationItem item = adapter.getItem(i); - if (item.getHeader().getId().equals(m)) { + SparseArray<ConversationMessageItem> messages = + adapter.getPrivateMessages(); + for (int i = 0; i < messages.size(); i++) { + ConversationMessageItem item = messages.valueAt(i); + if (item.getId().equals(m)) { item.setBody(body); - adapter.notifyItemChanged(i); - // Scroll to the bottom - list.scrollToPosition(count - 1); + adapter.notifyItemChanged(messages.keyAt(i)); + list.scrollToPosition(adapter.getItemCount() - 1); 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() { List<MessageId> unread = new ArrayList<MessageId>(); - int count = adapter.getItemCount(); - for (int i = 0; i < count; i++) { - PrivateMessageHeader h = adapter.getItem(i).getHeader(); - if (!h.isRead()) unread.add(h.getId()); + SparseArray<IncomingItem> list = + adapter.getIncomingMessages(); + for (int i = 0; i < list.size(); i++) { + IncomingItem item = list.valueAt(i); + if (!item.isRead()) unread.add(item.getId()); } if (unread.isEmpty()) return; if (LOG.isLoggable(INFO)) @@ -307,6 +401,8 @@ public class ConversationActivity extends BriarActivity try { long now = System.currentTimeMillis(); for (MessageId m : unread) + // not really clean, but the messaging manager can + // handle introduction messages as well messagingManager.setReadFlag(m, true); long duration = System.currentTimeMillis() - now; if (LOG.isLoggable(INFO)) @@ -331,7 +427,7 @@ public class ConversationActivity extends BriarActivity if (m.isValid() && m.getMessage().getGroupId().equals(groupId)) { LOG.info("Message added, reloading"); // Mark new incoming messages as read directly - if (m.isLocal()) loadHeaders(); + if (m.isLocal()) loadMessages(); else markMessageReadIfNew(m.getMessage()); } } else if (e instanceof MessagesSentEvent) { @@ -360,6 +456,23 @@ public class ConversationActivity extends BriarActivity connected = false; 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(); if (item != null) { // Mark the message read if it's the newest message - long lastMsgTime = item.getHeader().getTimestamp(); + long lastMsgTime = item.getTime(); long newMsgTime = m.getTimestamp(); 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() { try { messagingManager.setReadFlag(m.getId(), true); - loadHeaders(); + loadMessages(); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -397,13 +513,14 @@ public class ConversationActivity extends BriarActivity runOnUiThread(new Runnable() { public void run() { Set<MessageId> messages = new HashSet<MessageId>(messageIds); - int count = adapter.getItemCount(); - for (int i = 0; i < count; i++) { - ConversationItem item = adapter.getItem(i); - if (messages.contains(item.getHeader().getId())) { + SparseArray<OutgoingItem> list = + adapter.getOutgoingMessages(); + for (int i = 0; i < list.size(); i++) { + OutgoingItem item = list.valueAt(i); + if (messages.contains(item.getId())) { item.setSent(sent); item.setSeen(seen); - adapter.notifyItemChanged(i); + adapter.notifyItemChanged(list.keyAt(i)); } } } @@ -424,7 +541,7 @@ public class ConversationActivity extends BriarActivity private long getMinTimestampForNewMessage() { // Don't use an earlier timestamp than the newest message 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) { @@ -466,7 +583,8 @@ public class ConversationActivity extends BriarActivity } }; 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.setMessage(getString(R.string.dialog_message_delete_contact)); builder.setPositiveButton(android.R.string.ok, okListener); @@ -478,6 +596,10 @@ public class ConversationActivity extends BriarActivity runOnDbThread(new Runnable() { public void run() { try { + // make sure contactId is initialised + if (contactId == null) + contactId = messagingManager.getContactId(groupId); + // remove contact with that ID contactManager.removeContact(contactId); } catch (DbException e) { 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(); + } + }); + } + } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java index 7cb7227325a837b24ba7b214e32c56c157b57df3..e4c297788918c911d2b6c7ff9e620d795c42bd07 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationAdapter.java @@ -4,29 +4,37 @@ import android.content.Context; import android.support.v7.util.SortedList; import android.support.v7.widget.RecyclerView; import android.text.format.DateUtils; +import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; 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.util.StringUtils; import im.delight.android.identicons.IdenticonDrawable; import static android.support.v7.util.SortedList.INVALID_POSITION; - -class ConversationAdapter extends - RecyclerView.Adapter<ConversationAdapter.MessageHolder> { - - private static final int MSG_OUT = 0; - private static final int MSG_IN = 1; - private static final int MSG_IN_UNREAD = 2; - - private final SortedList<ConversationItem> messages = +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 RecyclerView.Adapter { + + private final SortedList<ConversationItem> items = new SortedList<ConversationItem>(ConversationItem.class, new SortedList.Callback<ConversationItem>() { @Override @@ -52,8 +60,8 @@ class ConversationAdapter extends @Override public int compare(ConversationItem c1, ConversationItem c2) { - long time1 = c1.getHeader().getTimestamp(); - long time2 = c2.getHeader().getTimestamp(); + long time1 = c1.getTime(); + long time2 = c2.getTime(); if (time1 < time2) return -1; if (time1 > time2) return 1; return 0; @@ -62,8 +70,7 @@ class ConversationAdapter extends @Override public boolean areItemsTheSame(ConversationItem c1, ConversationItem c2) { - return c1.getHeader().getId() - .equals(c2.getHeader().getId()); + return c1.getId().equals(c2.getId()); } @Override @@ -73,67 +80,103 @@ class ConversationAdapter extends } }); private Context ctx; + private IntroductionHandler intro; private byte[] identiconKey; + private String contactName; - public ConversationAdapter(Context context) { + public ConversationAdapter(Context context, + IntroductionHandler introductionHandler) { ctx = context; + intro = introductionHandler; } - public void setIdenticonKey(byte[] key) { - this.identiconKey = key; + public void setContactInformation(byte[] identiconKey, String contactName) { + this.identiconKey = identiconKey; + this.contactName = contactName; notifyDataSetChanged(); } @Override public int getItemViewType(int position) { - // return different type for incoming and outgoing (local) messages - PrivateMessageHeader header = getItem(position).getHeader(); - if (header.isLocal()) { - return MSG_OUT; - } else if (header.isRead()) { - return MSG_IN; - } else { - return MSG_IN_UNREAD; - } + return getItem(position).getType(); } @Override - public MessageHolder onCreateViewHolder(ViewGroup viewGroup, int type) { + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) { View v; // outgoing message (local) if (type == MSG_OUT) { v = LayoutInflater.from(viewGroup.getContext()) .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) else { v = LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.list_item_msg_in, viewGroup, false); + return new MessageHolder(v, type); } - - return new MessageHolder(v, type); } @Override - public void onBindViewHolder(final MessageHolder ui, final int position) { + public void onBindViewHolder(ViewHolder ui, int 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(); - if (header.isLocal()) { - if (item.isSeen()) { - ui.status.setImageResource(R.drawable.message_delivered); - } else if (item.isSent()) { - ui.status.setImageResource(R.drawable.message_sent); + if (item instanceof ConversationItem.OutgoingItem) { + if (((OutgoingItem) item).isSeen()) { + ui.status.setImageResource(R.drawable.message_delivered_white); + } else if (((OutgoingItem) item).isSent()) { + ui.status.setImageResource(R.drawable.message_sent_white); } else { - ui.status.setImageResource(R.drawable.message_stored); + ui.status.setImageResource(R.drawable.message_stored_white); } } else { if (identiconKey != null) - ui.avatar.setImageDrawable( - new IdenticonDrawable(identiconKey)); - if (!header.isRead()) { - int left = ui.layout.getPaddingLeft(); + ui.avatar.setImageDrawable(new IdenticonDrawable(identiconKey)); + if (item.getType() == MSG_IN_UNREAD) { + // TODO implement new unread message highlight according to #232 +/* int left = ui.layout.getPaddingLeft(); int top = ui.layout.getPaddingTop(); int right = ui.layout.getPaddingRight(); int bottom = ui.layout.getPaddingBottom(); @@ -144,6 +187,7 @@ class ConversationAdapter extends // re-apply the previous padding due to bug in some Android versions // see: https://code.google.com/p/android/issues/detail?id=17885 ui.layout.setPadding(left, top, right, bottom); +*/ } } @@ -159,42 +203,171 @@ class ConversationAdapter extends 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 public int getItemCount() { - return messages.size(); + return items.size(); } public ConversationItem getItem(int position) { - if (position == INVALID_POSITION || messages.size() <= position) { + if (position == INVALID_POSITION || items.size() <= position) { return null; // Not found } - return messages.get(position); + return items.get(position); } public ConversationItem getLastItem() { - if (messages.size() > 0) { - return messages.get(messages.size() - 1); + if (items.size() > 0) { + return items.get(items.size() - 1); } else { return null; } } - public void add(final ConversationItem message) { - this.messages.add(message); + 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 void clear() { - this.messages.beginBatchedUpdates(); + 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; + } - while(messages.size() != 0) { - messages.removeItemAt(0); + 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; + } - this.messages.endBatchedUpdates(); + public void add(final ConversationItem message) { + this.items.add(message); + } + + public void clear() { + items.clear(); } - // TODO: Does this class need to be public? - public static class MessageHolder extends RecyclerView.ViewHolder { + private static class MessageHolder extends RecyclerView.ViewHolder { public ViewGroup layout; public TextView body; @@ -217,4 +390,59 @@ class ConversationAdapter extends } } } -} \ No newline at end of file + + 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); + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java new file mode 100644 index 0000000000000000000000000000000000000000..fb891d0a1b3b5bb84bac329c397587bfa5cc44ea --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java new file mode 100644 index 0000000000000000000000000000000000000000..e955ea3a477061a32928660fceb4e4023527193f --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java new file mode 100644 index 0000000000000000000000000000000000000000..a2aba398f93dee2f6305b346b2d60ad206dff389 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationItem.java b/briar-android/src/org/briarproject/android/contact/ConversationItem.java index 7603c5c8f19f0ee4dd064f616b3a737007ff3de5..2c1492a8c3ee05706023f12dfa415a589272fb7b 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java @@ -1,46 +1,114 @@ 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.sync.MessageId; // This class is not thread-safe -class ConversationItem { +public abstract class ConversationItem { + + // this is needed for RecyclerView adapter which requires an int type + final static int MSG_IN = 0; + 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; - private final PrivateMessageHeader header; - private byte[] body; - private boolean sent, seen; + private MessageId id; + private long time; - ConversationItem(PrivateMessageHeader header) { - this.header = header; - body = null; - sent = header.isSent(); - seen = header.isSeen(); + public ConversationItem(MessageId id, long time) { + this.id = id; + this.time = time; } - PrivateMessageHeader getHeader() { - return header; + abstract int getType(); + + public MessageId getId() { + return id; } - byte[] getBody() { - return body; + long getTime() { + return time; } - void setBody(byte[] body) { - this.body = body; + public static ConversationItem from(PrivateMessageHeader h) { + if (h.isLocal()) + return new ConversationMessageOutItem(h); + else + return new ConversationMessageInItem(h); } - boolean isSent() { - return sent; + public static ConversationItem from(IntroductionRequest ir) { + if (ir.isLocal()) { + return new ConversationIntroductionOutItem(ir); + } else { + return new ConversationIntroductionInItem(ir); + } } - void setSent(boolean sent) { - this.sent = sent; + public static ConversationItem from(Context ctx, String contactName, + 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() { - return seen; + /** This method should not be used to get user-facing objects, + * 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) { - this.seen = seen; + protected interface OutgoingItem { + 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); + } + } diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java new file mode 100644 index 0000000000000000000000000000000000000000..c24f86a1288376748fc0a8ba05390047d53368c2 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageInItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java new file mode 100644 index 0000000000000000000000000000000000000000..db780efbcad65a42a7f0654d0b63666fedf8f6d5 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java new file mode 100644 index 0000000000000000000000000000000000000000..cfdf871afb2421b58a2dce072bbff583b4d0083e --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageOutItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java new file mode 100644 index 0000000000000000000000000000000000000000..610b703c17ada2defc6cd39233376929092d2658 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeInItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java new file mode 100644 index 0000000000000000000000000000000000000000..eabc73970aafd87593be54283b909dddce5d50c4 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java new file mode 100644 index 0000000000000000000000000000000000000000..b398897013f82366dbb252d43e7b679032b5a811 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationNoticeOutItem.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..ec5c276797df3a8665246396f7b3f721d1824892 --- /dev/null +++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java @@ -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; + } + +} diff --git a/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java b/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..2faee1a8ab22b30d7757d91702bca976a796eb6a --- /dev/null +++ b/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java @@ -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(); + } + +} diff --git a/briar-android/src/org/briarproject/android/introduction/IntroductionMessageFragment.java b/briar-android/src/org/briarproject/android/introduction/IntroductionMessageFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..ed0547da4e0b9eb8b69e7c8d6ea7433ba392e0b6 --- /dev/null +++ b/briar-android/src/org/briarproject/android/introduction/IntroductionMessageFragment.java @@ -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); + } + } +} diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java index 89c213502405727bf4d733038253fc2d309831c4..5e786322908abf39d4c479ae0fcb5bf8369ae9d7 100644 --- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java +++ b/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java @@ -73,15 +73,10 @@ public class BriarRecyclerView extends FrameLayout { } emptyObserver = new RecyclerView.AdapterDataObserver() { - @Override - public void onChanged() { - showData(); - } - @Override public void onItemRangeInserted(int positionStart, int itemCount) { super.onItemRangeInserted(positionStart, itemCount); - onChanged(); + if (itemCount > 0) showData(); } }; } diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java index 882ccbf112a67be92c15f711fbc869384a3cb029..18c5da1c691ef626deb5634194cde763a1730b1f 100644 --- a/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java @@ -8,6 +8,7 @@ import org.briarproject.api.db.DbException; import org.briarproject.api.db.Transaction; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; import java.util.Collection; @@ -20,19 +21,22 @@ public interface IntroductionManager { /** * 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; /** * 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; /** * 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; /** @@ -46,8 +50,8 @@ public interface IntroductionManager { /** Get the session state for the given session ID */ - BdfDictionary getSessionState(Transaction txn, byte[] sessionId) - throws DbException, FormatException; + BdfDictionary getSessionState(Transaction txn, GroupId groupId, + byte[] sessionId) throws DbException, FormatException; /** Gets the group used for introductions with Contact c */ Group getIntroductionGroup(Contact c); diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java index facd7151fbf564b1afe1f1327c3b119781416dfe..227c0500ce5b2f6cc459c87a9104407a068076b0 100644 --- a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java @@ -29,7 +29,7 @@ public class IntroductionRequest extends IntroductionResponse { return answered; } - public boolean doesExist() { + public boolean contactExists() { return exists; } diff --git a/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java b/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java index 8b63ae612bc4705e779b44e503e57e810fed4ccf..38fedd9fb76d9c19083296134fb391ef2f90e86e 100644 --- a/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java +++ b/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java @@ -20,6 +20,14 @@ public interface TransportPropertyManager { Map<TransportId, TransportProperties> getLocalProperties() 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. */ TransportProperties getLocalProperties(TransportId t) throws DbException; diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java index 6f76540d081a4ef7baf314ead7d9709eccb4c664..bc1e3d5f95c9f4b4431d0aa6fedeea318d84c7ba 100644 --- a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java +++ b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java @@ -109,6 +109,7 @@ public class IntroduceeEngine msg.put(E_PUBLIC_KEY, localState.getRaw(OUR_PUBLIC_KEY)); msg.put(TRANSPORT, localAction.getDictionary(TRANSPORT)); } + msg.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME)); messages.add(msg); logAction(currentState, localState, msg); diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java index be16ff1e495206ec06006de66e8e4708b808a19c..64af33b44d59cd2f8ff89d194cefb9fb7e97ae7d 100644 --- a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java +++ b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java @@ -28,6 +28,7 @@ import org.briarproject.api.introduction.IntroductionManager; import org.briarproject.api.introduction.SessionId; import org.briarproject.api.properties.TransportProperties; import org.briarproject.api.properties.TransportPropertyManager; +import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; 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.INTRODUCER; 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.NOT_OUR_RESPONSE; import static org.briarproject.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY; @@ -157,11 +159,15 @@ class IntroduceeManager { processStateUpdate(txn, engine.onMessageReceived(state, message)); } - public void acceptIntroduction(Transaction txn, - final SessionId sessionId) throws DbException, FormatException { + public void acceptIntroduction(Transaction txn, final ContactId contactId, + final SessionId sessionId, final long timestamp) + throws DbException, FormatException { + + Contact c = db.getContact(txn, contactId); + Group g = introductionManager.getIntroductionGroup(c); - BdfDictionary state = - introductionManager.getSessionState(txn, sessionId.getBytes()); + BdfDictionary state = introductionManager + .getSessionState(txn, g.getId(), sessionId.getBytes()); // get data to connect and derive a shared secret later long now = clock.currentTimeMillis(); @@ -169,7 +175,7 @@ class IntroduceeManager { byte[] publicKey = keyPair.getPublic().getEncoded(); byte[] privateKey = keyPair.getPrivate().getEncoded(); Map<TransportId, TransportProperties> transportProperties = - transportPropertyManager.getLocalProperties(); + transportPropertyManager.getLocalProperties(txn); // update session state for later state.put(ACCEPT, true); @@ -182,17 +188,22 @@ class IntroduceeManager { localAction.put(TYPE, TYPE_RESPONSE); localAction.put(TRANSPORT, encodeTransportProperties(transportProperties)); + localAction.put(MESSAGE_TIME, timestamp); // start engine and process its state update IntroduceeEngine engine = new IntroduceeEngine(); 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 { - BdfDictionary state = - introductionManager.getSessionState(txn, sessionId.getBytes()); + Contact c = db.getContact(txn, contactId); + Group g = introductionManager.getIntroductionGroup(c); + + BdfDictionary state = introductionManager + .getSessionState(txn, g.getId(), sessionId.getBytes()); // update session state state.put(ACCEPT, false); @@ -200,6 +211,7 @@ class IntroduceeManager { // define action BdfDictionary localAction = new BdfDictionary(); localAction.put(TYPE, TYPE_RESPONSE); + localAction.put(MESSAGE_TIME, timestamp); // start engine and process its state update IntroduceeEngine engine = new IntroduceeEngine(); diff --git a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java index fb88c28df61de2cb1d8bb1a1eedbd0e2cf62a14e..796d528f1d09b5fb9b49963fef8a732efbcb25de 100644 --- a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java +++ b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java @@ -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.SESSION_ID; 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_ABORT; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; @@ -104,6 +105,7 @@ public class IntroducerEngine if (localAction.containsKey(MSG)) { msg1.put(MSG, localAction.getString(MSG)); } + msg1.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME)); messages.add(msg1); logLocalAction(currentState, localState, msg1); BdfDictionary msg2 = new BdfDictionary(); @@ -115,6 +117,7 @@ public class IntroducerEngine if (localAction.containsKey(MSG)) { msg2.put(MSG, localAction.getString(MSG)); } + msg2.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME)); messages.add(msg2); logLocalAction(currentState, localState, msg2); diff --git a/briar-core/src/org/briarproject/introduction/IntroducerManager.java b/briar-core/src/org/briarproject/introduction/IntroducerManager.java index 3cc906aedf32c085c132acd818a1152a773f19e2..93481367df01a89508d541d4ca6be4b21072288d 100644 --- a/briar-core/src/org/briarproject/introduction/IntroducerManager.java +++ b/briar-core/src/org/briarproject/introduction/IntroducerManager.java @@ -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.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.MSG; import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1; 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.STATE; 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_ABORT; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; @@ -99,7 +101,7 @@ class IntroducerManager { } 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? // deny new introduction under which conditions? @@ -115,6 +117,7 @@ class IntroducerManager { } localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey()); localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey()); + localAction.put(MESSAGE_TIME, timestamp); // start engine and process its state update IntroducerEngine engine = new IntroducerEngine(); diff --git a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java index e28af29e4d1c0ffbc886e342739d6afc977d3b98..edec27bef53a4d1042639557c96d65f03f4bca06 100644 --- a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java +++ b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java @@ -44,7 +44,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; import java.util.Map; 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.EXISTS; 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.MSG; 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) { BdfDictionary state; try { - state = getSessionState(txn, - message.getRaw(SESSION_ID, new byte[0])); + state = getSessionState(txn, groupId, + message.getRaw(SESSION_ID)); } catch (FormatException e) { LOG.warning("Could not find state for message, deleting..."); deleteMessage(txn, m.getId()); @@ -266,12 +267,13 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook } @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 { Transaction txn = db.startTransaction(false); try { - introducerManager.makeIntroduction(txn, c1, c2, msg); + introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp); txn.setComplete(); } finally { db.endTransaction(txn); @@ -279,12 +281,14 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook } @Override - public void acceptIntroduction(final SessionId sessionId) + public void acceptIntroduction(final ContactId contactId, + final SessionId sessionId, final long timestamp) throws DbException, FormatException { Transaction txn = db.startTransaction(false); try { - introduceeManager.acceptIntroduction(txn, sessionId); + introduceeManager + .acceptIntroduction(txn, contactId, sessionId, timestamp); txn.setComplete(); } finally { db.endTransaction(txn); @@ -292,12 +296,14 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook } @Override - public void declineIntroduction(final SessionId sessionId) + public void declineIntroduction(final ContactId contactId, + final SessionId sessionId, final long timestamp) throws DbException, FormatException { Transaction txn = db.startTransaction(false); try { - introduceeManager.declineIntroduction(txn, sessionId); + introduceeManager + .declineIntroduction(txn, contactId, sessionId, timestamp); txn.setComplete(); } finally { db.endTransaction(txn); @@ -322,8 +328,6 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook statuses = db.getMessageStatus(txn, contactId, g); // turn messages into classes for the UI - Map<SessionId, BdfDictionary> sessionStates = - new HashMap<SessionId, BdfDictionary>(); for (MessageStatus s : statuses) { MessageId messageId = s.getMessageId(); BdfDictionary msg = metadata.get(messageId); @@ -335,11 +339,8 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook // get session state SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID)); - BdfDictionary state = sessionStates.get(sessionId); - if (state == null) { - state = getSessionState(txn, sessionId.getBytes()); - } - sessionStates.put(sessionId, state); + BdfDictionary state = + getSessionState(txn, g, sessionId.getBytes()); boolean local; long time = msg.getLong(MESSAGE_TIME); @@ -453,19 +454,31 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook } } - public BdfDictionary getSessionState(Transaction txn, byte[] sessionId) - throws DbException, FormatException { + public BdfDictionary getSessionState(Transaction txn, GroupId groupId, + byte[] sessionId) throws DbException, FormatException { try { - return clientHelper.getMessageMetadataAsDictionary(txn, - new MessageId(sessionId)); + // See if we can find the state directly for the introducer + 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) { + // State not found directly, so iterate over all states + // to find state for introducee Map<MessageId, BdfDictionary> map = clientHelper .getMessageMetadataAsDictionary(txn, localGroup.getId()); for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) { 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)) { @@ -492,9 +505,10 @@ class IntroductionManagerImpl extends BdfIncomingMessageHook byte[] body = clientHelper.toByteArray(bdfList); GroupId groupId = new GroupId(message.getRaw(GROUP_ID)); Group group = db.getGroup(txn, groupId); - long timestamp = System.currentTimeMillis(); - + long timestamp = + message.getLong(MESSAGE_TIME, System.currentTimeMillis()); message.put(MESSAGE_TIME, timestamp); + Metadata metadata = metadataEncoder.encode(message); messageQueueManager diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java index 5663b6151a823399eaf800f8031bdf2dfd281ea8..71c93b4fd23a84588f59afc557bfec1d0915336a 100644 --- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java +++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java @@ -108,6 +108,27 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, 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 public TransportProperties getLocalProperties(TransportId t) throws DbException { @@ -212,26 +233,6 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, 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, TransportProperties p, long version, boolean local, boolean shared) throws DbException {