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..78f0681bdd2397f0326262a28ab718ea322031f4 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.ContactListDevider"/> </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..08da32d7784cefedac3c3d9aa00eca01fb6e47ba --- /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/acceptButton" + android:layout_alignRight="@+id/acceptButton" + 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..d3e1a85aa633167d6e235d72199b87998524d0ce --- /dev/null +++ b/briar-android/res/layout/list_item_introduction_out.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <include + android:id="@+id/messageLayout" + layout="@layout/list_item_msg_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:minWidth="175dp" + 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..e7c912f9151ee2b45ef0520d8ca51cd415744e08 --- /dev/null +++ b/briar-android/res/layout/list_item_notice_in.xml @@ -0,0 +1,34 @@ +<?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:minWidth="80dp" + 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..01090aaec6156de99a48133f0bac428ed52f1e9a 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/make_introduction" + 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..c37fa7ea6b8fbedab751f4180dccd69b6a902fea 100644 --- a/briar-android/res/values/strings.xml +++ b/briar-android/res/values/strings.xml @@ -141,6 +141,26 @@ <string name="transport_lan">Wi-Fi</string> <string name="no_data">No data</string> <string name="unknown_app">an unknown app</string> + <string name="make_introduction">Make Introduction</string> + <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 introduced %1$s to %2$s.</string> + <string name="introduction_request_received">%1$s introduced you to %2$s. Do you want to add %2$s to your contact list?</string> + <string name="introduction_request_exists_received">%1$s introduced 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 introduced 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 successfully introduced to %1$s who was now added to your contact list.</string> <!-- Dialogs --> <string name="dialog_title_lost_password">Lost Password</string> @@ -152,6 +172,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..9b4ebbf6cae735fdd84b5c244fced4020f4ff8cc 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.ContactListDevider" 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..2330aac866a4388a63e3c7d78327d846e0dfb564 100644 --- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java +++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java @@ -14,10 +14,14 @@ 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.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 +61,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 +116,7 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, public Void call() { clearPrivateMessageNotification(); clearForumPostNotification(); + clearIntroductionSuccessNotification(); return null; } }); @@ -135,6 +141,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 +160,25 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, else if (c.equals(forumManager.getClientId())) showForumPostNotification(m.getMessage().getGroupId()); } + } else if (e instanceof IntroductionRequestReceivedEvent) { + try { + GroupId group = messagingManager.getConversationId( + ((IntroductionRequestReceivedEvent) e).getContactId()); + showPrivateMessageNotification(group); + } catch (DbException ex) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, ex.toString(), ex); + } + } else if (e instanceof IntroductionResponseReceivedEvent) { + try { + GroupId group = messagingManager.getConversationId( + ((IntroductionResponseReceivedEvent) e).getContactId()); + showPrivateMessageNotification(group); + } catch (DbException ex) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, ex.toString(), ex); + } + } else if (e instanceof IntroductionSucceededEvent) { + Contact c = ((IntroductionSucceededEvent) e).getContact(); + showIntroductionSucceededNotification(c); } } @@ -335,4 +366,35 @@ class AndroidNotificationManagerImpl implements AndroidNotificationManager, } }); } + + 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..3cba02fadbe734e9f9ed9c98a9baeeff2ab06c50 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -1,9 +1,12 @@ 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.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.view.LayoutInflater; import android.view.View; @@ -11,23 +14,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 +80,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 +101,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 +169,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 +205,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 +222,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 +234,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 +249,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 +284,35 @@ 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(new ConversationMessageItem(h)); + } + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading message headers took " + duration + " ms"); + + 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..747c08caafb6398ce53dbd3a856dc29cc3ea41a6 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,21 +75,31 @@ 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()); + private static final int INTRODUCTION_REQUEST_CODE = 0; @Inject protected AndroidNotificationManager notificationManager; @Inject protected ConnectionRegistry connectionRegistry; @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 +109,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 +127,21 @@ public class ConversationActivity extends BriarActivity setContentView(R.layout.activity_conversation); - adapter = new ConversationAdapter(this); + // Custom Toolbar + final 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); + final 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 +164,7 @@ public class ConversationActivity extends BriarActivity eventBus.addListener(this); notificationManager.blockNotification(groupId); notificationManager.clearPrivateMessageNotification(groupId); - loadContactDetails(); - loadHeaders(); + loadData(); } @Override @@ -141,12 +179,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 +191,20 @@ 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.startActivityForResult(this, intent, + INTRODUCTION_REQUEST_CODE, options.toBundle()); + return true; case R.id.action_social_remove_person: askToRemoveContact(); return true; @@ -163,20 +213,37 @@ public class ConversationActivity extends BriarActivity } } - private void loadContactDetails() { + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + + if (requestCode == INTRODUCTION_REQUEST_CODE) { + if (resultCode == RESULT_OK) { + loadData(); + } + } + } + + 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 +257,42 @@ 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.setIdenticonKey(contactIdenticonKey, contactName); } }); } - private void loadHeaders() { + private void loadMessages() { runOnDbThread(new Runnable() { public void run() { try { long now = System.currentTimeMillis(); 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 +303,37 @@ 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 = + new ConversationMessageItem(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,27 +365,42 @@ 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)); return; } } + // Scroll to the bottom + list.scrollToPosition(adapter.getItemCount() - 1); + } + }); + } + + private void addIntroduction(final ConversationItem item) { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (adapter != null) { + 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 +414,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 +440,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 +469,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 +495,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 +512,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 +526,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 +554,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 +596,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); @@ -500,4 +631,66 @@ 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() { + try { + if (accept) { + introductionManager.acceptIntroduction(sessionId); + } else { + introductionManager.declineIntroduction(sessionId); + } + 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..83cb88cb6446c674142af08b5d8f0f2b96ef098d 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,113 @@ 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; + setHasStableIds(true); } - public void setIdenticonKey(byte[] key) { + public void setIdenticonKey(byte[] key, String contactName) { this.identiconKey = key; + this.contactName = contactName; + // FIXME this breaks the progress animation because it is called early before data is loaded notifyDataSetChanged(); } + @Override + public long getItemId(int position) { + return getItem(position).getId().hashCode(); + } + @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(final ViewHolder ui, final int position) { ConversationItem item = getItem(position); + if (item instanceof ConversationMessageItem) { + bindMessage((MessageHolder) ui, (ConversationMessageItem) item, + position); + } 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, + position); + } else if (item instanceof ConversationNoticeInItem) { + bindNotice((NoticeHolder) ui, (ConversationNoticeInItem) item, + position); + } else { + throw new IllegalArgumentException("Unhandled Conversation Item"); + } + } + + private void bindMessage(final MessageHolder ui, + ConversationMessageItem item, final int position) { PrivateMessageHeader header = item.getHeader(); - if (header.isLocal()) { + if (item.getType() == MSG_OUT) { if (item.isSeen()) { - ui.status.setImageResource(R.drawable.message_delivered); + ui.status.setImageResource(R.drawable.message_delivered_white); } else if (item.isSent()) { - ui.status.setImageResource(R.drawable.message_sent); + 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 +197,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 +213,178 @@ class ConversationAdapter extends ui.date.setText(DateUtils.getRelativeTimeSpanString(ctx, timestamp)); } + private void bindIntroduction(final IntroductionHolder ui, + final ConversationIntroductionInItem 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().doesExist()) { + 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(final NoticeHolder ui, + final ConversationNoticeItem item, final int position) { + + 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 SparseArray<IncomingItem> getIncomingMessages() { + SparseArray<IncomingItem> messages = + new SparseArray<IncomingItem>(); + + for (int i = 0; i < items.size(); i++) { + ConversationItem item = items.get(i); + if (item instanceof IncomingItem) { + messages.put(i, (IncomingItem) item); + } + } + return messages; + } + + public SparseArray<OutgoingItem> getOutgoingMessages() { + SparseArray<OutgoingItem> messages = + new SparseArray<OutgoingItem>(); + + for (int i = 0; i < items.size(); i++) { + ConversationItem item = items.get(i); + if (item instanceof OutgoingItem) { + messages.put(i, (OutgoingItem) item); + } + } + return messages; + } + + public SparseArray<ConversationMessageItem> getPrivateMessages() { + SparseArray<ConversationMessageItem> messages = + new SparseArray<ConversationMessageItem>(); + + for (int i = 0; i < items.size(); i++) { + ConversationItem item = items.get(i); + if (item instanceof ConversationMessageItem) { + messages.put(i, (ConversationMessageItem) item); + } + } + return messages; + } + public void add(final ConversationItem message) { - this.messages.add(message); + this.items.add(message); } public void clear() { - this.messages.beginBatchedUpdates(); + this.items.beginBatchedUpdates(); - while(messages.size() != 0) { - messages.removeItemAt(0); + while(items.size() != 0) { + items.removeItemAt(0); } - this.messages.endBatchedUpdates(); + this.items.endBatchedUpdates(); } - // TODO: Does this class need to be public? - public static class MessageHolder extends RecyclerView.ViewHolder { + protected class MessageHolder extends RecyclerView.ViewHolder { public ViewGroup layout; public TextView body; @@ -217,4 +407,59 @@ class ConversationAdapter extends } } } -} \ No newline at end of file + + protected 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); + } + } + } + + protected 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..939f808792d5ef89862a4fb86eac4264cf2f21e5 --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionInItem.java @@ -0,0 +1,47 @@ +package org.briarproject.android.contact; + +import org.briarproject.api.introduction.IntroductionRequest; +import org.briarproject.api.sync.MessageId; + +public class ConversationIntroductionInItem extends ConversationItem implements + ConversationItem.IncomingItem { + + private IntroductionRequest ir; + private boolean answered, read; + + public ConversationIntroductionInItem(IntroductionRequest ir) { + super(ir.getMessageId(), ir.getTime()); + + this.ir = ir; + this.answered = ir.wasAnswered(); + this.read = ir.isRead(); + } + + @Override + int getType() { + return INTRODUCTION_IN; + } + + public IntroductionRequest getIntroductionRequest() { + return ir; + } + + public boolean wasAnswered() { + return answered; + } + + public void setAnswered(boolean answered) { + this.answered = answered; + } + + @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/ConversationIntroductionOutItem.java b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java new file mode 100644 index 0000000000000000000000000000000000000000..37f32a83d977e35997a3ddd23e5d60c3420aa0dc --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationIntroductionOutItem.java @@ -0,0 +1,48 @@ +package org.briarproject.android.contact; + +import org.briarproject.api.introduction.IntroductionRequest; +import org.briarproject.api.sync.MessageId; + +/** + * 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 ConversationIntroductionInItem + 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..ad037996c47c5a9447e15c2f78324fb669c5b52f 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationItem.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationItem.java @@ -1,46 +1,105 @@ package org.briarproject.android.contact; -import org.briarproject.api.messaging.PrivateMessageHeader; +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.sync.MessageId; // This class is not thread-safe -class ConversationItem { +public abstract class ConversationItem { + + 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(IntroductionRequest ir) { + if (ir.isLocal()) { + return new ConversationIntroductionOutItem(ir); + } else { + return new ConversationIntroductionInItem(ir); + } } - boolean isSent() { - return 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()); + } } - void setSent(boolean sent) { - this.sent = sent; + /** 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()); } - boolean isSeen() { - return seen; + protected interface OutgoingItem { + MessageId getId(); + boolean isSent(); + void setSent(boolean sent); + boolean isSeen(); + void setSeen(boolean seen); } - void setSeen(boolean seen) { - this.seen = seen; + protected interface IncomingItem { + MessageId getId(); + boolean isRead(); + void setRead(boolean 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..38e5afab0c3cf1ef60d11d262261e1889b0a11ec --- /dev/null +++ b/briar-android/src/org/briarproject/android/contact/ConversationMessageItem.java @@ -0,0 +1,73 @@ +package org.briarproject.android.contact; + +import org.briarproject.api.messaging.PrivateMessageHeader; +import org.briarproject.api.sync.MessageId; + +// This class is not thread-safe +public class ConversationMessageItem extends ConversationItem implements + ConversationItem.OutgoingItem, ConversationItem.IncomingItem { + + private final PrivateMessageHeader header; + private byte[] body; + private boolean sent, seen, read; + + public ConversationMessageItem(PrivateMessageHeader header) { + super(header.getId(), header.getTimestamp()); + + this.header = header; + body = null; + sent = header.isSent(); + seen = header.isSeen(); + read = header.isRead(); + } + + @Override + int getType() { + if (getHeader().isLocal()) return MSG_OUT; + if (getHeader().isRead()) return MSG_IN; + return MSG_IN_UNREAD; + } + + PrivateMessageHeader getHeader() { + return header; + } + + byte[] getBody() { + return body; + } + + void setBody(byte[] body) { + this.body = body; + } + + @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; + } + + @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/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..66e57686a44c3a4740a823f4c23f077aa560f1fe --- /dev/null +++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java @@ -0,0 +1,243 @@ +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.ChangeBounds; +import android.transition.Fade; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +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.contact.ConversationMessageItem; +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) { + Toast.makeText(getActivity(), + R.string.introduction_error, + Toast.LENGTH_SHORT).show(); + return; + } + 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(new ConversationMessageItem(h)); + } + long duration = System.currentTimeMillis() - now; + if (LOG.isLoggable(INFO)) + LOG.info("Loading message headers took " + duration + " ms"); + + 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..c3905df14c91b6ad50680bf645b77fc530dd4a98 --- /dev/null +++ b/briar-android/src/org/briarproject/android/introduction/IntroductionActivity.java @@ -0,0 +1,107 @@ +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); + + 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..7b06c9bd7cb77488039849e7295c1b270bb5a0f7 --- /dev/null +++ b/briar-android/src/org/briarproject/android/introduction/IntroductionMessageFragment.java @@ -0,0 +1,235 @@ +package org.briarproject.android.introduction; + +import android.app.Activity; +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 = "ContactChooserFragment"; + 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 + private volatile boolean introductionWasMade = false; + @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) { + // TODO + e.printStackTrace(); + } + } + }); + } + + 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) { + String msg = ui.message.getText().toString(); + makeIntroduction(c1, c2, msg); + } + + private void makeIntroduction(final Contact c1, final Contact c2, + final String msg) { + introductionActivity.runOnDbThread(new Runnable() { + public void run() { + // prevent double introductions + if (introductionWasMade) return; + + // actually make the introduction + try { + introductionManager.makeIntroduction(c1, c2, msg); + introductionWasMade = true; + postIntroduction(false); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + postIntroduction(true); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + postIntroduction(true); + } + } + }); + } + + private void postIntroduction(final boolean error) { + introductionActivity.runOnUiThread(new Runnable() { + public void run() { + introductionActivity.hideSoftKeyboard(ui.message); + if (error) { + Toast.makeText(introductionActivity, + R.string.introduction_error, Toast.LENGTH_SHORT) + .show(); + introductionActivity.setResult(Activity.RESULT_CANCELED); + } else { + introductionActivity.setResult(Activity.RESULT_OK); + } + introductionActivity.finish(); + } + }); + } + + private 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(); } }; }