From 47a10a0288638ef5c58e3c5a16f07f834e188163 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 8 Nov 2018 23:33:37 -0800 Subject: [PATCH] Added support for multi-image receive. --- res/drawable-hdpi/ic_caption.png | Bin 0 -> 661 bytes res/drawable-mdpi/ic_caption.png | Bin 0 -> 336 bytes res/drawable-xhdpi/ic_caption.png | Bin 0 -> 725 bytes res/drawable-xxhdpi/ic_caption.png | Bin 0 -> 1196 bytes res/drawable-xxxhdpi/ic_caption.png | Bin 0 -> 1764 bytes res/drawable/album_rail_item_background.xml | 5 + res/layout-v16/video_player.xml | 21 ++- res/layout/album_thumbnail_2.xml | 23 +++ res/layout/album_thumbnail_3.xml | 29 ++++ res/layout/album_thumbnail_4.xml | 36 ++++ res/layout/album_thumbnail_5.xml | 43 +++++ res/layout/album_thumbnail_many.xml | 61 +++++++ res/layout/album_thumbnail_view.xml | 21 +++ res/layout/conversation_item_thumbnail.xml | 14 +- res/layout/media_preview_activity.xml | 66 +++++++- res/layout/media_preview_album_rail_item.xml | 12 ++ res/layout/media_preview_exoplayer_layout.xml | 13 ++ res/layout/media_view.xml | 2 + res/layout/thumbnail_view.xml | 8 + res/layout/transfer_controls_view.xml | 37 ++-- res/values/attrs.xml | 5 + res/values/strings.xml | 9 + res/values/themes.xml | 2 + .../securesms/ConversationItem.java | 62 ++++--- .../securesms/MediaPreviewActivity.java | 132 ++++++++++++++- .../RecipientPreferenceActivity.java | 1 + .../securesms/attachments/Attachment.java | 10 +- .../attachments/DatabaseAttachment.java | 4 +- .../MmsNotificationAttachment.java | 2 +- .../attachments/PointerAttachment.java | 10 +- .../securesms/attachments/UriAttachment.java | 8 +- .../components/AlbumThumbnailView.java | 159 ++++++++++++++++++ .../components/ConversationItemThumbnail.java | 113 +++++++------ .../components/MaxHeightScrollView.java | 42 +++++ .../securesms/components/MediaView.java | 14 ++ .../securesms/components/ThumbnailView.java | 26 ++- .../components/TransferControlView.java | 156 +++++++++++++---- .../securesms/contactshare/Contact.java | 2 +- .../database/AttachmentDatabase.java | 17 +- .../securesms/database/MediaDatabase.java | 5 +- .../securesms/database/MmsDatabase.java | 6 +- .../securesms/database/MmsSmsDatabase.java | 4 +- .../database/helpers/SQLCipherOpenHelper.java | 7 +- .../securesms/groups/GroupManager.java | 2 +- .../securesms/jobs/AttachmentDownloadJob.java | 10 ++ .../securesms/jobs/MmsDownloadJob.java | 2 +- .../securesms/jobs/PushSendJob.java | 2 + .../mediapreview/AlbumRailAdapter.java | 100 +++++++++++ .../mediapreview/MediaPreviewViewModel.java | 110 ++++++++++++ .../securesms/mms/AttachmentManager.java | 2 + .../securesms/mms/AudioSlide.java | 2 +- src/org/thoughtcrime/securesms/mms/Slide.java | 11 +- .../thoughtcrime/securesms/mms/SlideDeck.java | 6 + .../securesms/mms/SlidesClickedListener.java | 9 + .../securesms/video/VideoPlayer.java | 20 +++ 55 files changed, 1277 insertions(+), 186 deletions(-) create mode 100644 res/drawable-hdpi/ic_caption.png create mode 100644 res/drawable-mdpi/ic_caption.png create mode 100644 res/drawable-xhdpi/ic_caption.png create mode 100644 res/drawable-xxhdpi/ic_caption.png create mode 100644 res/drawable-xxxhdpi/ic_caption.png create mode 100644 res/drawable/album_rail_item_background.xml create mode 100644 res/layout/album_thumbnail_2.xml create mode 100644 res/layout/album_thumbnail_3.xml create mode 100644 res/layout/album_thumbnail_4.xml create mode 100644 res/layout/album_thumbnail_5.xml create mode 100644 res/layout/album_thumbnail_many.xml create mode 100644 res/layout/album_thumbnail_view.xml create mode 100644 res/layout/media_preview_album_rail_item.xml create mode 100644 res/layout/media_preview_exoplayer_layout.xml create mode 100644 src/org/thoughtcrime/securesms/components/AlbumThumbnailView.java create mode 100644 src/org/thoughtcrime/securesms/components/MaxHeightScrollView.java create mode 100644 src/org/thoughtcrime/securesms/mediapreview/AlbumRailAdapter.java create mode 100644 src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java create mode 100644 src/org/thoughtcrime/securesms/mms/SlidesClickedListener.java diff --git a/res/drawable-hdpi/ic_caption.png b/res/drawable-hdpi/ic_caption.png new file mode 100644 index 0000000000000000000000000000000000000000..d63dfa1d273f6cc4af7516b70d7b447b683d16a2 GIT binary patch literal 661 zcmV;G0&4wPx%Pf0{UR9Fe^RZDIfK@e?b_!H-kV_U-V2_k`%RaUt~4#GjWKz7;q1X)MI8&*iL zWoINWtVoFs#xo%^llR(FqxN_#h#BK7x}-NI8RcVe!qc72xjJc4v8RsSk z<>>FtM=La+&ud2PQQg^WR!0I>C8>lEVt|NBrP3daTd{JvyhI|FO7bVn7;u4Cz#(t| zJO>zi3=6R7C2$6u0zU!9q9Q(GnrW}s`_O8&J}nlDN)QB27=|*R%jh_ca$Ps9*X#at zI{n`5cBj}lX=ouNfl9mG{@CesK7)+Pn~a<&CZ&eM;cvvg#c|C+2+3N79#Yk6RpFv( zBojG0g+f6Q%#}7F#2%vg(WOBlsfHZX^ zL^v|}Yv2fAQ||yIENkGdWt5&t=D;lWF(k%$Hn0cqQDj431G=d(CdTB4gTdfy9`9nm z-#^BF?*Sfe-@qRHcfcEfhvqw!NSQ8~i&AmH#Ozw%g3Z?BPkPlUiw3N=ozBBz+0dJu&-nd+?6ILrz$^ybAY`>H8{7@ z%w$Z?T9yIcN2>RbTgK!p75^6a&QP6n$q>pmlMsD^Bt#i=K6{6|Q7HpSh%#%b!>!DO v_xX2It`{*$nEradwyUfHRspNP6DjZ)jND2xBIKY600000NkvXXu0mjf2+1Yh literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_caption.png b/res/drawable-mdpi/ic_caption.png new file mode 100644 index 0000000000000000000000000000000000000000..bc2a88f74752fc0adc1562ed36e1361adfb1ac51 GIT binary patch literal 336 zcmV-W0k8gvP)Px$3Q0skR7ef&Qe6vzFcjUI1(F0oM9)3+2m1g2MPx%k4Z#9RA>e5mQ8NkFc5|1pSmd03P^$;p{p(fr1#(E3Oz%c#`!7Q#CG1tY6yy= z-N>-REM|Zwk|T&JPfRLI7@mt}Ca3>%J3nJ$K`n~HrjXrNSo9{hu<-ue!`63E` zIz%>kdT=6$YU7Nz(}5G*(ySTeH#ZSgJ8eW^VD?V_R{**7d+>OD2b|#66Hs~wcxi5w zbKlgS2@9dtiNNKr@Kd-E822m}fWj2K@r-GE$~8W=84u%NW7=pX0Ytl9E|*uU)v5ui zNCEMwSLi4M8Zev90@}9ys%?J>I|V>3d z#59@D=T*rYDHCC1wQ!85kf)H{eCSladg>%ZLl*&^J*b6vbl}eg;0skq0;~$Xmv3-% zOx)A#rKAPBjQfwmb%)Va2*tKxPr3a28tdH)kRqzvM6fqN0KX7?2M~ZST7ClD3{^;# z=PJ%RLPx1_sbvuYxssUxNCSjeFys7O05?dNlQ`pe9ex`5NZA**w3CT|xMAlBSejT{ zTJ%sph&%}v*=_5Z2y1)u@)LkK6UohE8~^qQAhLs(Gua$IxkX+UIu+}Cr z9&I_fe9VF+-{_5!2TDXszT^?_dIUTI9s!SlN5CWSHY4y1NFB%mXOzm&00000NkvXX Hu0mjfT82DD literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_caption.png b/res/drawable-xxhdpi/ic_caption.png new file mode 100644 index 0000000000000000000000000000000000000000..6892ef867ceab5c229b4b023f0851f21208a2597 GIT binary patch literal 1196 zcmV;d1XKHoP)Px(W=TXrRCodHnA?)mFbsy%Z8<}M$KaAH26+D&cmiGny33ii=*#9WN@|JIh^AjD*NBFcK#?{85kYeme8b1=Taf|;9Fz!R-??u(SHU;Gx8iq8Qh?M! zQB>-qzEe%{4Lt%QhjPwq0LP%;;Wch9DZb$&BA>5I&2J76&FtiCT%Re-RPYTS52sew z&(bLdS$6nK+wEUl`qcD+DPxLZY3*YaAm9@~zM6hA<%q{eMUiFOQUb(;gdh`sF$D^f zv8f@Y=)3MQu3tgmNdIn%Pxyw9{~SRY00flC;iAZ{KZginkpaVHQ41}ft?G}IB8Y)Z zwBBqs@3z}*wcG7#wTokgPDyN=sL+dxi&~eLxObVQVI~BYexsr!_{h4 zPYNIpXDW~YgLEO`o2j-8x*V-~3XtGHwf?gTZO9AGRKVb|7XeagZT#27M@106w;-he zS)YS2U;!LNV3gLC6Z-Ly)Rh~pHtKaruD&x-?WCw8$){>_!_udi;1EH)P8c&b z!7dyk0(e!D&RKm|)X%4iAnJ}J6+ym}VEMW+riuPsyv?>7I@tIth$c z=xE?ASFl+FMtg{&;8e3b5T9p!t6Yj8I${K;#@K`t3`qYADCbWL4xLJXSQU)e*s1Z# zQZ9u(plBL{8FjidyKhXIJyVx{;z$*IN|z_5uAA$olP#D;v* z1@Byj=Csnf)qt3UGOycrfCik^U5%yNAJ6bGbA9{0wnCw z-a~z_k^-cMVvIxvDsluzsrsEel`W*Q0lU1a?cC3<=Su;iL52yEj01?Yo?xIKLGUTI zsoa|xIY2y+ha(uGNa?0^hXHN&-3|I`7Ac&9M09B z<)J-H(uLK-BVx)R5Cnoi5C{T6AP5A3AP@wCKoAH5K_Cdsg1~P;}^khVpbr5j)PzGllUwio|km9wJ+!JcJlF^uUP12~%`BJzQ$x z^tv-lVYX?eowDd`Zr8{-p<}5z_b1%_@cF!7pV#a2+vkT*dDvmBmO4@$0H75d zAlnHi94w~R5r0L$emy~yQTyPzICjUiZx1YV-<}M~%!xV*qx$OELIYY=4T~+q{zpjP z598rN{Aq4!LUmW0v`l!Hm4#Pf-1H|rv|(|y*1>flbiN`esHfL44^LJuG7%^Y7iL2_ ze#K=uk|!@puXEY8{UHl!5;Z7<-Sd9)9gOa~OEG)RyZviTsO~G(9GGY^2xsy+a-Lx@ z_db$pxBaN$bLj0wEoIKOP|};?0G6n4?+$8N`@QnvAb=|a@$KS3w2}PCc9+TK{Z`31Y$5Pda zWmk}_f4g;!+2TKvLjm0xRPa#t!i1kkAHV_f&r4HzZ}yLHVVmNMk;&7mGx?)|(@z^U zar;em9z{B*#l`4V#w&N-s)ULhRt|5m_=yN+nYl4Q@M;=KIz%n6{PTj|a?*13tvm#2 zZ~3B;-?@qdIYv2mEKy%Unq1?VYx*PHh8AmjbX_<&$E;u7P16RUbpvHX*&2HNAN$wW z6&(Mi2^o#vY>VygC8sE=s#g~Y$BH`CF!0e znfmpohmqge`Lu8PiVm)xyZVI(g(0)_5VGr}9aF>@eiCuWLEO+Nii?Ze&LojY^$`Vo zo0{(3xon^?7$_CU_;5H}=f&jYH6Tug$8XhFRWd|toN;-=Usxh@X1ADNmR(e~mUCDBAl3vR1#2cb zz8c<##^u62)eO285S)U7h_>6yynI7oD%o2-zo^*M>;856Svk&FH#@}j&}BS#1#&GG z5bMkqB(>6hvnZfobtgRira^SEBRqC20`CaW$FgQw@3Dh16W%cjE0uOTjN^{XFdAblN~XHgUPr|HGG6tt|)1Ca0DI#wYdUpK`)n_2Cuc$2|GgoQ?`v%47V? zqHVb|V?}?2MGXWcU-=1wvCPku5bJNhM?bsTtm|)~qiOr>C|@ya zzSwrxu@6kC*)@40V}4)3F)+j!UBL#%h zy@FszfSjZ3UVhTLk+Jk?l9TvbM)9WG$a9-)kpaxtE6V9Ol;3Vt^lIA{pl4K=B~~vL zR{FZp9f+5S?EzcOnYx*7@`pQnVbyyeb9{}#p(7I<{b(C6-souDy6uZe3LM}}BX3}E&7k_|LdLX4qB1XDt z@A=P3-Yr(35v%R%VB-_xs!r*p6lbQ<`jy2?Wf$#iHP~G(_W)~XO$PYg5slr~4=Ep<6RuNM+_A9WVU#ck}Z!$$^GCF|B&8 zFZj|~1$#E-CdsG`@O@Z1xUg|@u2R}Q8(Si7z@7THr33QoL;{0Ies(-)Z205o@{xQ< zx<1o-+4F*`{S3} zo;AWALX6NL3uJ#zHUSA6lgB}y$$sIj{t?i`6wBp^WGJl5w9_h zB&1R)UCA?Y#PrNakK+&5p(t0>E{AgoKb@*wb95?iuhkSV{)iF`G0<0N*Yjk9Dcr(9 zsb|}|{AA##%~)7-HjhwUFYP2e-w-|jUfLO}h~Bm3FGZv{K3e?ltg!2yX6MrUz3h&b zL;0~##<7jmtm7rAcTT5vdj + + + + \ No newline at end of file diff --git a/res/layout-v16/video_player.xml b/res/layout-v16/video_player.xml index ba3075f06f..b855fca65a 100644 --- a/res/layout-v16/video_player.xml +++ b/res/layout-v16/video_player.xml @@ -1,14 +1,17 @@ - + + android:id="@+id/video_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:gravity="center" + app:player_layout_id="@layout/media_preview_exoplayer_layout"/> \ No newline at end of file diff --git a/res/layout/album_thumbnail_2.xml b/res/layout/album_thumbnail_2.xml new file mode 100644 index 0000000000..8750abd1eb --- /dev/null +++ b/res/layout/album_thumbnail_2.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/album_thumbnail_3.xml b/res/layout/album_thumbnail_3.xml new file mode 100644 index 0000000000..64b7b55ea8 --- /dev/null +++ b/res/layout/album_thumbnail_3.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/album_thumbnail_4.xml b/res/layout/album_thumbnail_4.xml new file mode 100644 index 0000000000..205148365e --- /dev/null +++ b/res/layout/album_thumbnail_4.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/album_thumbnail_5.xml b/res/layout/album_thumbnail_5.xml new file mode 100644 index 0000000000..2347e9b33b --- /dev/null +++ b/res/layout/album_thumbnail_5.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/album_thumbnail_many.xml b/res/layout/album_thumbnail_many.xml new file mode 100644 index 0000000000..004ee8323c --- /dev/null +++ b/res/layout/album_thumbnail_many.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/album_thumbnail_view.xml b/res/layout/album_thumbnail_view.xml new file mode 100644 index 0000000000..3bac5cb0e1 --- /dev/null +++ b/res/layout/album_thumbnail_view.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/res/layout/conversation_item_thumbnail.xml b/res/layout/conversation_item_thumbnail.xml index 3876fa7e08..55bbbefa16 100644 --- a/res/layout/conversation_item_thumbnail.xml +++ b/res/layout/conversation_item_thumbnail.xml @@ -1,7 +1,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + - + + + + + + android:layout_height="wrap_content" + android:paddingTop="32dp" + android:animateLayoutChanges="true" + app:scrollView_maxHeight="120dp"> + + + + + + + + + + diff --git a/res/layout/media_preview_album_rail_item.xml b/res/layout/media_preview_album_rail_item.xml new file mode 100644 index 0000000000..df2ca67c16 --- /dev/null +++ b/res/layout/media_preview_album_rail_item.xml @@ -0,0 +1,12 @@ + + + diff --git a/res/layout/media_preview_exoplayer_layout.xml b/res/layout/media_preview_exoplayer_layout.xml new file mode 100644 index 0000000000..d6b870c7c5 --- /dev/null +++ b/res/layout/media_preview_exoplayer_layout.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/media_view.xml b/res/layout/media_view.xml index dd6a39ad7e..60f83751d4 100644 --- a/res/layout/media_view.xml +++ b/res/layout/media_view.xml @@ -1,5 +1,6 @@ @@ -8,6 +9,7 @@ android:id="@+id/image" android:layout_width="match_parent" android:layout_height="match_parent" + android:clickable="false" android:contentDescription="@string/media_preview_activity__media_content_description" /> + + - + + + + + + + + diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 3b4d3ea751..290234bde1 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -86,6 +86,7 @@ + @@ -300,4 +301,8 @@ + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 087e22fb32..036e232750 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10,6 +10,9 @@ New message + + \+%d + Currently: %s You haven\'t set a passphrase yet! @@ -734,6 +737,12 @@ Signal New message + + + %d Item + %d Items + + Device no longer registered This is likely because you registered your phone number with Signal on a different device. Tap to re-register. diff --git a/res/values/themes.xml b/res/values/themes.xml index b17c51bd5d..7aa071c51c 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -204,6 +204,7 @@ @color/core_grey_90 @drawable/sticky_date_header_background_light @color/core_grey_60 + @color/transparent_black_30 @drawable/quick_camera_light @drawable/ic_mic_grey600_24dp @@ -311,6 +312,7 @@ @color/core_grey_05 @drawable/sticky_date_header_background_dark @color/core_grey_25 + @color/transparent_white_30 @drawable/contact_list_divider_dark diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index 2f9fa25d3f..0aa42ae246 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -42,7 +42,6 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; -import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.components.AlertView; import org.thoughtcrime.securesms.components.AudioView; @@ -53,7 +52,6 @@ import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.QuoteView; import org.thoughtcrime.securesms.components.SharedContactView; import org.thoughtcrime.securesms.contactshare.Contact; -import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; @@ -71,6 +69,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideClickListener; +import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientModifiedListener; import org.thoughtcrime.securesms.util.DateUtils; @@ -83,6 +82,7 @@ import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.views.Stub; import org.whispersystems.libsignal.util.guava.Optional; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; @@ -132,10 +132,11 @@ public class ConversationItem extends LinearLayout private int defaultBubbleColor; private int measureCalls; - private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); - private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener(); - private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener(); - private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener(); + private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener(); + private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener(); + private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener); + private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener(); + private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener(); private final Context context; @@ -427,7 +428,7 @@ public class ConversationItem extends LinearLayout //noinspection ConstantConditions audioViewStub.get().setAudio(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide(), showControls); - audioViewStub.get().setDownloadClickListener(downloadClickListener); + audioViewStub.get().setDownloadClickListener(singleDownloadClickListener); audioViewStub.get().setOnLongClickListener(passthroughClickListener); ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); @@ -442,7 +443,7 @@ public class ConversationItem extends LinearLayout //noinspection ConstantConditions documentViewStub.get().setDocument(((MediaMmsMessageRecord)messageRecord).getSlideDeck().getDocumentSlide(), showControls); documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener()); - documentViewStub.get().setDownloadClickListener(downloadClickListener); + documentViewStub.get().setDownloadClickListener(singleDownloadClickListener); documentViewStub.get().setOnLongClickListener(passthroughClickListener); ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); @@ -455,19 +456,18 @@ public class ConversationItem extends LinearLayout if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE); //noinspection ConstantConditions - Slide thumbnailSlide = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide(); - Attachment attachment = thumbnailSlide.asAttachment(); + List thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides(); mediaThumbnailStub.get().setImageResource(glideRequests, - thumbnailSlide, + thumbnailSlides, showControls, - false, - attachment.getWidth(), - attachment.getHeight()); + false); mediaThumbnailStub.get().setThumbnailClickListener(new ThumbnailClickListener()); mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener); mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener); mediaThumbnailStub.get().setOnClickListener(passthroughClickListener); mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody())); + mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? defaultBubbleColor + : messageRecord.getRecipient().getColor().toConversationColor(context)); setThumbnailOutlineCorners(messageRecord, previousRecord, nextRecord, isGroupThread); @@ -847,9 +847,9 @@ public class ConversationItem extends LinearLayout } } - private class AttachmentDownloadClickListener implements SlideClickListener { + private class AttachmentDownloadClickListener implements SlidesClickedListener { @Override - public void onClick(View v, final Slide slide) { + public void onClick(View v, final List slides) { Log.i(TAG, "onClick() for attachment download"); if (messageRecord.isMmsNotification()) { Log.i(TAG, "Scheduling MMS attachment download"); @@ -858,19 +858,32 @@ public class ConversationItem extends LinearLayout .add(new MmsDownloadJob(context, messageRecord.getId(), messageRecord.getThreadId(), false)); } else { - Log.i(TAG, "Scheduling push attachment download"); - DatabaseFactory.getAttachmentDatabase(context).setTransferState(messageRecord.getId(), - slide.asAttachment(), - AttachmentDatabase.TRANSFER_PROGRESS_STARTED); + Log.i(TAG, "Scheduling push attachment downloads for " + slides.size() + " items"); - ApplicationContext.getInstance(context) - .getJobManager() - .add(new AttachmentDownloadJob(context, messageRecord.getId(), - ((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true)); + for (Slide slide : slides) { + ApplicationContext.getInstance(context) + .getJobManager() + .add(new AttachmentDownloadJob(context, messageRecord.getId(), + ((DatabaseAttachment)slide.asAttachment()).getAttachmentId(), true)); + } } } } + private class SlideClickPassthroughListener implements SlideClickListener { + + private final SlidesClickedListener original; + + private SlideClickPassthroughListener(@NonNull SlidesClickedListener original) { + this.original = original; + } + + @Override + public void onClick(View v, Slide slide) { + original.onClick(v, Collections.singletonList(slide)); + } + } + private class ThumbnailClickListener implements SlideClickListener { public void onClick(final View v, final Slide slide) { if (shouldInterceptClicks(messageRecord) || !batchSelected.isEmpty()) { @@ -883,6 +896,7 @@ public class ConversationItem extends LinearLayout intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, messageRecord.isOutgoing()); intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getTimestamp()); intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize()); + intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull()); intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, false); context.startActivity(intent); diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java index 612f525eda..9af32fe3cf 100644 --- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -19,6 +19,7 @@ package org.thoughtcrime.securesms; import android.Manifest; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.arch.lifecycle.ViewModelProviders; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -36,15 +37,21 @@ import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.support.v7.app.AlertDialog; import org.thoughtcrime.securesms.logging.Log; + +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.GestureDetector; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; +import android.widget.TextView; import android.widget.Toast; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; @@ -53,6 +60,8 @@ import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedList import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; +import org.thoughtcrime.securesms.mediapreview.AlbumRailAdapter; +import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.permissions.Permissions; @@ -71,33 +80,49 @@ import java.util.WeakHashMap; /** * Activity for displaying media attachments in-app */ -public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, LoaderManager.LoaderCallbacks> { +public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener, + LoaderManager.LoaderCallbacks>, + AlbumRailAdapter.RailItemClickedListener +{ private final static String TAG = MediaPreviewActivity.class.getSimpleName(); public static final String ADDRESS_EXTRA = "address"; public static final String DATE_EXTRA = "date"; public static final String SIZE_EXTRA = "size"; + public static final String CAPTION_EXTRA = "caption"; public static final String OUTGOING_EXTRA = "outgoing"; public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent"; private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); - private ViewPager mediaPager; - private Uri initialMediaUri; - private String initialMediaType; - private long initialMediaSize; - private Recipient conversationRecipient; - private boolean leftIsRecent; + private ViewPager mediaPager; + private View detailsContainer; + private TextView caption; + private View captionContainer; + private RecyclerView albumRail; + private AlbumRailAdapter albumRailAdapter; + private ViewGroup playbackControlsContainer; + private Uri initialMediaUri; + private String initialMediaType; + private long initialMediaSize; + private String initialCaption; + private Recipient conversationRecipient; + private boolean leftIsRecent; + private GestureDetector clickDetector; + private MediaPreviewViewModel viewModel; private int restartItem = -1; + @SuppressWarnings("ConstantConditions") @Override protected void onCreate(Bundle bundle, boolean ready) { this.setTheme(R.style.TextSecure_DarkTheme); dynamicLanguage.onCreate(this); + viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class); + setFullscreenIfPossible(); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); @@ -107,6 +132,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im initializeViews(); initializeResources(); + initializeObservers(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + clickDetector.onTouchEvent(ev); + return super.dispatchTouchEvent(ev); } @Override @@ -126,6 +158,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im Util.runOnMain(this::initializeActionBar); } + @Override + public void onRailItemClicked(int distanceFromActive) { + mediaPager.setCurrentItem(mediaPager.getCurrentItem() + distanceFromActive); + } + @SuppressWarnings("ConstantConditions") private void initializeActionBar() { MediaItem mediaItem = getCurrentMediaItem(); @@ -172,6 +209,17 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im mediaPager = findViewById(R.id.media_pager); mediaPager.setOffscreenPageLimit(1); mediaPager.addOnPageChangeListener(new ViewPagerListener()); + + albumRail = findViewById(R.id.media_preview_album_rail); + albumRailAdapter = new AlbumRailAdapter(GlideApp.with(this), this); + + albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); + albumRail.setAdapter(albumRailAdapter); + + detailsContainer = findViewById(R.id.media_preview_details_container); + caption = findViewById(R.id.media_preview_caption); + captionContainer = findViewById(R.id.media_preview_caption_container); + playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container); } private void initializeResources() { @@ -180,6 +228,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im initialMediaUri = getIntent().getData(); initialMediaType = getIntent().getType(); initialMediaSize = getIntent().getLongExtra(SIZE_EXTRA, 0); + initialCaption = getIntent().getStringExtra(CAPTION_EXTRA); leftIsRecent = getIntent().getBooleanExtra(LEFT_IS_RECENT_EXTRA, false); restartItem = -1; @@ -190,6 +239,49 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im } } + private void initializeObservers() { + viewModel.getPreviewData().observe(this, previewData -> { + if (previewData == null) { + return; + } + + View playbackControls = ((MediaItemAdapter) mediaPager.getAdapter()).getPlaybackControls(mediaPager.getCurrentItem()); + + if (previewData.getAlbumThumbnails().isEmpty() && previewData.getCaption() == null && playbackControls == null) { + detailsContainer.setVisibility(View.GONE); + } else { + detailsContainer.setVisibility(View.VISIBLE); + } + + albumRail.setVisibility(previewData.getAlbumThumbnails().isEmpty() ? View.GONE : View.VISIBLE); + albumRailAdapter.setRecords(previewData.getAlbumThumbnails(), previewData.getActivePosition()); + albumRail.smoothScrollToPosition(previewData.getActivePosition()); + + captionContainer.setVisibility(previewData.getCaption() == null ? View.GONE : View.VISIBLE); + caption.setText(previewData.getCaption()); + + if (playbackControls != null) { + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + playbackControls.setLayoutParams(params); + + playbackControlsContainer.removeAllViews(); + playbackControlsContainer.addView(playbackControls); + } else { + playbackControlsContainer.removeAllViews(); + } + }); + + clickDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (e.getY() < detailsContainer.getTop()) { + detailsContainer.setVisibility(detailsContainer.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE); + } + return super.onSingleTapUp(e); + } + }); + } + private void initializeMedia() { if (!isContentTypeSupported(initialMediaType)) { Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing."); @@ -203,6 +295,12 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im getSupportLoaderManager().restartLoader(0, null, this); } else { mediaPager.setAdapter(new SingleItemPagerAdapter(this, GlideApp.with(this), getWindow(), initialMediaUri, initialMediaType, initialMediaSize)); + + if (initialCaption != null) { + detailsContainer.setVisibility(View.VISIBLE); + captionContainer.setVisibility(View.VISIBLE); + caption.setText(initialCaption); + } } } @@ -348,6 +446,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im mediaPager.setAdapter(adapter); adapter.setActive(true); + viewModel.setCursor(data.first, leftIsRecent); + if (restartItem < 0) mediaPager.setCurrentItem(data.second); else mediaPager.setCurrentItem(restartItem); } @@ -369,7 +469,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im if (adapter != null) { MediaItem item = adapter.getMediaItemFor(position); if (item.recipient != null) item.recipient.addListener(MediaPreviewActivity.this); - + viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position); initializeActionBar(); } } @@ -453,6 +553,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im public void pause(int position) { } + + @Override + public @Nullable View getPlaybackControls(int position) { + return null; + } } private static class CursorPagerAdapter extends PagerAdapter implements MediaItemAdapter { @@ -511,7 +616,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im try { //noinspection ConstantConditions - mediaView.set(glideRequests, window, mediaRecord.getAttachment().getDataUri(), mediaRecord.getAttachment().getContentType(), mediaRecord.getAttachment().getSize(), autoplay); + mediaView.set(glideRequests, window, mediaRecord.getAttachment().getDataUri(), + mediaRecord.getAttachment().getContentType(), mediaRecord.getAttachment().getSize(), autoplay); } catch (IOException e) { Log.w(TAG, e); } @@ -552,6 +658,13 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im if (mediaView != null) mediaView.pause(); } + @Override + public @Nullable View getPlaybackControls(int position) { + MediaView mediaView = mediaViews.get(position); + if (mediaView != null) return mediaView.getPlaybackControls(); + return null; + } + private int getCursorPosition(int position) { if (leftIsRecent) return position; else return cursor.getCount() - 1 - position; @@ -585,5 +698,6 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im interface MediaItemAdapter { MediaItem getMediaItemFor(int position); void pause(int position); + @Nullable View getPlaybackControls(int position); } } diff --git a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java index df5010dee9..40c2f2ba48 100644 --- a/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java +++ b/src/org/thoughtcrime/securesms/RecipientPreferenceActivity.java @@ -178,6 +178,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing()); intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate()); intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize()); + intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption()); intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR); intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType()); startActivity(intent); diff --git a/src/org/thoughtcrime/securesms/attachments/Attachment.java b/src/org/thoughtcrime/securesms/attachments/Attachment.java index 7cc795d853..c639811ad9 100644 --- a/src/org/thoughtcrime/securesms/attachments/Attachment.java +++ b/src/org/thoughtcrime/securesms/attachments/Attachment.java @@ -37,10 +37,13 @@ public abstract class Attachment { private final boolean quote; + @Nullable + private final String caption; + public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName, @Nullable String location, @Nullable String key, @Nullable String relay, @Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote, - int width, int height, boolean quote) + int width, int height, boolean quote, @Nullable String caption) { this.contentType = contentType; this.transferState = transferState; @@ -55,6 +58,7 @@ public abstract class Attachment { this.width = width; this.height = height; this.quote = quote; + this.caption = caption; } @Nullable @@ -126,4 +130,8 @@ public abstract class Attachment { public boolean isQuote() { return quote; } + + public @Nullable String getCaption() { + return caption; + } } diff --git a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java index ba57be3752..f293991d25 100644 --- a/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/DatabaseAttachment.java @@ -17,9 +17,9 @@ public class DatabaseAttachment extends Attachment { String contentType, int transferProgress, long size, String fileName, String location, String key, String relay, byte[] digest, String fastPreflightId, boolean voiceNote, - int width, int height, boolean quote) + int width, int height, boolean quote, @Nullable String caption) { - super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote); + super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, caption); this.attachmentId = attachmentId; this.hasData = hasData; this.hasThumbnail = hasThumbnail; diff --git a/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java b/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java index 0011af6c92..2983ce4886 100644 --- a/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/MmsNotificationAttachment.java @@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase; public class MmsNotificationAttachment extends Attachment { public MmsNotificationAttachment(int status, long size) { - super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false); + super("application/mms", getTransferStateFromStatus(status), size, null, null, null, null, null, null, false, 0, 0, false, null); } @Nullable diff --git a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java index 19427c5862..aba842f44b 100644 --- a/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/PointerAttachment.java @@ -19,9 +19,9 @@ public class PointerAttachment extends Attachment { @Nullable String fileName, @NonNull String location, @Nullable String key, @Nullable String relay, @Nullable byte[] digest, boolean voiceNote, - int width, int height) + int width, int height, @Nullable String caption) { - super(contentType, transferState, size, fileName, location, key, relay, digest, null, voiceNote, width, height, false); + super(contentType, transferState, size, fileName, location, key, relay, digest, null, voiceNote, width, height, false, caption); } @Nullable @@ -87,7 +87,8 @@ public class PointerAttachment extends Attachment { pointer.get().asPointer().getDigest().orNull(), pointer.get().asPointer().getVoiceNote(), pointer.get().asPointer().getWidth(), - pointer.get().asPointer().getHeight())); + pointer.get().asPointer().getHeight(), + pointer.get().asPointer().getCaption().orNull())); } @@ -104,6 +105,7 @@ public class PointerAttachment extends Attachment { thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null, false, thumbnail != null ? thumbnail.asPointer().getWidth() : 0, - thumbnail != null ? thumbnail.asPointer().getHeight() : 0)); + thumbnail != null ? thumbnail.asPointer().getHeight() : 0, + thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null)); } } diff --git a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java index 72c4f365ed..bea12e15ac 100644 --- a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -10,17 +10,17 @@ public class UriAttachment extends Attachment { private final @Nullable Uri thumbnailUri; public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size, - @Nullable String fileName, boolean voiceNote, boolean quote) + @Nullable String fileName, boolean voiceNote, boolean quote, @Nullable String caption) { - this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote); + this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, quote, caption); } public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri, @NonNull String contentType, int transferState, long size, int width, int height, @Nullable String fileName, @Nullable String fastPreflightId, - boolean voiceNote, boolean quote) + boolean voiceNote, boolean quote, @Nullable String caption) { - super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote); + super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, caption); this.dataUri = dataUri; this.thumbnailUri = thumbnailUri; } diff --git a/src/org/thoughtcrime/securesms/components/AlbumThumbnailView.java b/src/org/thoughtcrime/securesms/components/AlbumThumbnailView.java new file mode 100644 index 0000000000..c4286fe18f --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/AlbumThumbnailView.java @@ -0,0 +1,159 @@ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.support.annotation.ColorInt; +import android.support.annotation.IdRes; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.mms.Slide; +import org.thoughtcrime.securesms.mms.SlideClickListener; +import org.thoughtcrime.securesms.mms.SlidesClickedListener; +import org.thoughtcrime.securesms.util.views.Stub; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class AlbumThumbnailView extends FrameLayout { + + private @Nullable SlideClickListener thumbnailClickListener; + private @Nullable SlidesClickedListener downloadClickListener; + + private int currentSizeClass; + + private ViewGroup albumCellContainer; + private Stub transferControls; + + private final SlideClickListener defaultThumbnailClickListener = (v, slide) -> { + if (thumbnailClickListener != null) { + thumbnailClickListener.onClick(v, slide); + } + }; + + private final OnLongClickListener defaultLongClickListener = v -> this.performLongClick(); + + public AlbumThumbnailView(@NonNull @android.support.annotation.NonNull Context context) { + super(context); + initialize(); + } + + public AlbumThumbnailView(@NonNull @android.support.annotation.NonNull Context context, @Nullable @android.support.annotation.Nullable AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + private void initialize() { + inflate(getContext(), R.layout.album_thumbnail_view, this); + + albumCellContainer = findViewById(R.id.album_cell_container); + transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub)); + } + + public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List slides, boolean showControls) { + if (slides.size() < 2) { + throw new IllegalStateException("Provided less than two slides."); + } + + if (showControls) { + transferControls.get().setShowDownloadText(true); + transferControls.get().setSlides(slides); + transferControls.get().setDownloadClickListener(v -> { + if (downloadClickListener != null) { + downloadClickListener.onClick(v, slides); + } + }); + } else { + if (transferControls.resolved()) { + transferControls.get().setVisibility(GONE); + } + } + + int sizeClass = sizeClass(slides.size()); + + if (sizeClass != currentSizeClass) { + inflateLayout(sizeClass); + currentSizeClass = sizeClass; + } + + showSlides(glideRequests, slides); + } + + public void setCellBackgroundColor(@ColorInt int color) { + ViewGroup cellRoot = findViewById(R.id.album_thumbnail_root); + + if (cellRoot != null) { + for (int i = 0; i < cellRoot.getChildCount(); i++) { + cellRoot.getChildAt(i).setBackgroundColor(color); + } + } + } + + public void setThumbnailClickListener(@Nullable SlideClickListener listener) { + thumbnailClickListener = listener; + } + + public void setDownloadClickListener(@Nullable SlidesClickedListener listener) { + downloadClickListener = listener; + } + + private void inflateLayout(int sizeClass) { + albumCellContainer.removeAllViews(); + + switch (sizeClass) { + case 2: + inflate(getContext(), R.layout.album_thumbnail_2, albumCellContainer); + break; + case 3: + inflate(getContext(), R.layout.album_thumbnail_3, albumCellContainer); + break; + case 4: + inflate(getContext(), R.layout.album_thumbnail_4, albumCellContainer); + break; + case 5: + inflate(getContext(), R.layout.album_thumbnail_5, albumCellContainer); + break; + default: + inflate(getContext(), R.layout.album_thumbnail_many, albumCellContainer); + break; + } + } + + private void showSlides(@NonNull GlideRequests glideRequests, @NonNull List slides) { + setSlide(glideRequests, slides.get(0), R.id.album_cell_1); + setSlide(glideRequests, slides.get(1), R.id.album_cell_2); + + if (slides.size() >= 3) { + setSlide(glideRequests, slides.get(2), R.id.album_cell_3); + } + + if (slides.size() >= 4) { + setSlide(glideRequests, slides.get(3), R.id.album_cell_4); + } + + if (slides.size() >= 5) { + setSlide(glideRequests, slides.get(4), R.id.album_cell_5); + } + + if (slides.size() > 5) { + TextView text = findViewById(R.id.album_cell_overflow_text); + text.setText(getContext().getString(R.string.AlbumThumbnailView_plus, slides.size() - 5)); + } + } + + private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id) { + ThumbnailView cell = findViewById(id); + cell.setImageResource(glideRequests, slide, false, false); + cell.setThumbnailClickListener(defaultThumbnailClickListener); + cell.setOnLongClickListener(defaultLongClickListener); + } + + private int sizeClass(int size) { + return Math.min(size, 6); + } +} diff --git a/src/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java b/src/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java index 7b2973dd3a..048780897f 100644 --- a/src/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java +++ b/src/org/thoughtcrime/securesms/components/ConversationItemThumbnail.java @@ -3,11 +3,10 @@ package org.thoughtcrime.securesms.components; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; -import android.net.Uri; +import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; @@ -16,40 +15,36 @@ import android.widget.FrameLayout; import android.widget.ImageView; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideClickListener; +import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.util.ThemeUtil; +import java.util.List; + public class ConversationItemThumbnail extends FrameLayout { private static final String TAG = ConversationItemThumbnail.class.getSimpleName(); - private static final Paint LIGHT_THEME_OUTLINE_PAINT = new Paint(); - private static final Paint DARK_THEME_OUTLINE_PAINT = new Paint(); - - static { - LIGHT_THEME_OUTLINE_PAINT.setColor(Color.argb((int) (255 * 0.2), 0, 0, 0)); - LIGHT_THEME_OUTLINE_PAINT.setStyle(Paint.Style.STROKE); - LIGHT_THEME_OUTLINE_PAINT.setStrokeWidth(1f); - LIGHT_THEME_OUTLINE_PAINT.setAntiAlias(true); - - DARK_THEME_OUTLINE_PAINT.setColor(Color.argb((int) (255 * 0.2), 255, 255, 255)); - DARK_THEME_OUTLINE_PAINT.setStyle(Paint.Style.STROKE); - DARK_THEME_OUTLINE_PAINT.setStrokeWidth(1f); - DARK_THEME_OUTLINE_PAINT.setAntiAlias(true); - } - - private final float[] radii = new float[8]; - private final RectF bounds = new RectF(); - private final Path corners = new Path(); + private final float[] radii = new float[8]; + private final RectF bounds = new RectF(); + private final Path corners = new Path(); private ThumbnailView thumbnail; + private AlbumThumbnailView album; private ImageView shade; private ConversationItemFooter footer; - private Paint outlinePaint; private CornerMask cornerMask; + private final Paint outlinePaint = new Paint(); + { + outlinePaint.setStyle(Paint.Style.STROKE); + outlinePaint.setStrokeWidth(1f); + outlinePaint.setAntiAlias(true); + } + public ConversationItemThumbnail(Context context) { super(context); init(null); @@ -68,13 +63,13 @@ public class ConversationItemThumbnail extends FrameLayout { private void init(@Nullable AttributeSet attrs) { inflate(getContext(), R.layout.conversation_item_thumbnail, this); - this.thumbnail = findViewById(R.id.conversation_thumbnail_image); - this.shade = findViewById(R.id.conversation_thumbnail_shade); - this.footer = findViewById(R.id.conversation_thumbnail_footer); - this.outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT; - this.cornerMask = new CornerMask(this); + outlinePaint.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color)); - setTouchDelegate(thumbnail.getTouchDelegate()); + this.thumbnail = findViewById(R.id.conversation_thumbnail_image); + this.album = findViewById(R.id.conversation_thumbnail_album); + this.shade = findViewById(R.id.conversation_thumbnail_shade); + this.footer = findViewById(R.id.conversation_thumbnail_footer); + this.cornerMask = new CornerMask(this); if (attrs != null) { TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0); @@ -99,32 +94,37 @@ public class ConversationItemThumbnail extends FrameLayout { cornerMask.mask(canvas); } - final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2; + if (album.getVisibility() != VISIBLE) { + final float halfStrokeWidth = outlinePaint.getStrokeWidth() / 2; - bounds.left = halfStrokeWidth; - bounds.top = halfStrokeWidth; - bounds.right = canvas.getWidth() - halfStrokeWidth; - bounds.bottom = canvas.getHeight() - halfStrokeWidth; + bounds.left = halfStrokeWidth; + bounds.top = halfStrokeWidth; + bounds.right = canvas.getWidth() - halfStrokeWidth; + bounds.bottom = canvas.getHeight() - halfStrokeWidth; - corners.reset(); - corners.addRoundRect(bounds, radii, Path.Direction.CW); + corners.reset(); + corners.addRoundRect(bounds, radii, Path.Direction.CW); - canvas.drawPath(corners, outlinePaint); + canvas.drawPath(corners, outlinePaint); + } } @Override public void setFocusable(boolean focusable) { thumbnail.setFocusable(focusable); + album.setFocusable(focusable); } @Override public void setClickable(boolean clickable) { thumbnail.setClickable(clickable); + album.setClickable(clickable); } @Override public void setOnLongClickListener(@Nullable OnLongClickListener l) { thumbnail.setOnLongClickListener(l); + album.setOnLongClickListener(l); } public void showShade(boolean show) { @@ -146,37 +146,38 @@ public class ConversationItemThumbnail extends FrameLayout { } @UiThread - public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, + public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull List slides, boolean showControls, boolean isPreview) { - thumbnail.setImageResource(glideRequests, slide, showControls, isPreview); + if (slides.size() == 1) { + thumbnail.setVisibility(VISIBLE); + album.setVisibility(GONE); + + Attachment attachment = slides.get(0).asAttachment(); + thumbnail.setImageResource(glideRequests, slides.get(0), showControls, isPreview, attachment.getWidth(), attachment.getHeight()); + setTouchDelegate(thumbnail.getTouchDelegate()); + } else { + thumbnail.setVisibility(GONE); + album.setVisibility(VISIBLE); + + album.setSlides(glideRequests, slides, showControls); + setTouchDelegate(album.getTouchDelegate()); + } } - @UiThread - public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, - boolean showControls, boolean isPreview, int naturalWidth, - int naturalHeight) - { - thumbnail.setImageResource(glideRequests, slide, showControls, isPreview, naturalWidth, naturalHeight); - } - - public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) { - thumbnail.setImageResource(glideRequests, uri); + public void setConversationColor(@ColorInt int color) { + if (album.getVisibility() == VISIBLE) { + album.setCellBackgroundColor(color); + } } public void setThumbnailClickListener(SlideClickListener listener) { thumbnail.setThumbnailClickListener(listener); + album.setThumbnailClickListener(listener); } - public void setDownloadClickListener(SlideClickListener listener) { + public void setDownloadClickListener(SlidesClickedListener listener) { thumbnail.setDownloadClickListener(listener); - } - - public void clear(GlideRequests glideRequests) { - thumbnail.clear(glideRequests); - } - - public void showProgressSpinner() { - thumbnail.showProgressSpinner(); + album.setDownloadClickListener(listener); } } diff --git a/src/org/thoughtcrime/securesms/components/MaxHeightScrollView.java b/src/org/thoughtcrime/securesms/components/MaxHeightScrollView.java new file mode 100644 index 0000000000..7f129e7d0d --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/MaxHeightScrollView.java @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.widget.ScrollView; + +import org.thoughtcrime.securesms.R; + +public class MaxHeightScrollView extends ScrollView { + + private int maxHeight = -1; + + public MaxHeightScrollView(Context context) { + super(context); + initialize(null); + } + + public MaxHeightScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(attrs); + } + + private void initialize(@Nullable AttributeSet attrs) { + if (attrs != null) { + TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.MaxHeightScrollView, 0, 0); + + maxHeight = typedArray.getDimensionPixelOffset(R.styleable.MaxHeightScrollView_scrollView_maxHeight, -1); + + typedArray.recycle(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (maxHeight >= 0) { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST); + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/src/org/thoughtcrime/securesms/components/MediaView.java b/src/org/thoughtcrime/securesms/components/MediaView.java index d8d809ff53..a4f17c9a03 100644 --- a/src/org/thoughtcrime/securesms/components/MediaView.java +++ b/src/org/thoughtcrime/securesms/components/MediaView.java @@ -11,6 +11,7 @@ import android.util.AttributeSet; import android.view.View; import android.view.Window; import android.widget.FrameLayout; +import android.widget.TextView; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -81,6 +82,19 @@ public class MediaView extends FrameLayout { } } + public void hideControls() { + if (this.videoView.resolved()){ + this.videoView.get().hideControls(); + } + } + + public @Nullable View getPlaybackControls() { + if (this.videoView.resolved()){ + return this.videoView.get().getControlView(); + } + return null; + } + public void cleanup() { this.imageView.cleanup(); if (this.videoView.resolved()) { diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index ad8273cd8d..b7e61f3ae4 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -27,12 +27,14 @@ import org.thoughtcrime.securesms.mms.GlideRequest; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideClickListener; +import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.whispersystems.libsignal.util.guava.Optional; +import java.util.Collections; import java.util.Locale; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; @@ -49,6 +51,7 @@ public class ThumbnailView extends FrameLayout { private ImageView image; private View playOverlay; + private View captionIcon; private OnClickListener parentClickListener; private final int[] dimens = new int[2]; @@ -57,7 +60,7 @@ public class ThumbnailView extends FrameLayout { private Optional transferControls = Optional.absent(); private SlideClickListener thumbnailClickListener = null; - private SlideClickListener downloadClickListener = null; + private SlidesClickedListener downloadClickListener = null; private Slide slide = null; private int radius; @@ -77,6 +80,7 @@ public class ThumbnailView extends FrameLayout { this.image = findViewById(R.id.thumbnail_image); this.playOverlay = findViewById(R.id.play_overlay); + this.captionIcon = findViewById(R.id.thumbnail_caption_icon); super.setOnClickListener(new ThumbnailClickDispatcher()); if (attrs != null) { @@ -230,8 +234,8 @@ public class ThumbnailView extends FrameLayout { @UiThread public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, - boolean showControls, boolean isPreview, int naturalWidth, - int naturalHeight) + boolean showControls, boolean isPreview, + int naturalWidth, int naturalHeight) { if (showControls) { getTransferControls().setSlide(slide); @@ -267,6 +271,8 @@ public class ThumbnailView extends FrameLayout { this.slide = slide; + this.captionIcon.setVisibility(slide.getCaption().isPresent() ? VISIBLE : GONE); + dimens[WIDTH] = naturalWidth; dimens[HEIGHT] = naturalHeight; invalidate(); @@ -302,7 +308,7 @@ public class ThumbnailView extends FrameLayout { this.thumbnailClickListener = listener; } - public void setDownloadClickListener(SlideClickListener listener) { + public void setDownloadClickListener(SlidesClickedListener listener) { this.downloadClickListener = listener; } @@ -342,8 +348,14 @@ public class ThumbnailView extends FrameLayout { size[WIDTH] = getDefaultWidth(); size[HEIGHT] = getDefaultHeight(); } - return request.override(size[WIDTH], size[HEIGHT]) - .transforms(fitting, new RoundedCorners(radius)); + + request = request.override(size[WIDTH], size[HEIGHT]); + + if (radius > 0) { + return request.transforms(fitting, new RoundedCorners(radius)); + } else { + return request.transforms(fitting); + } } private int getDefaultWidth() { @@ -382,7 +394,7 @@ public class ThumbnailView extends FrameLayout { public void onClick(View view) { Log.i(TAG, "onClick() for download button"); if (downloadClickListener != null && slide != null) { - downloadClickListener.onClick(view, slide); + downloadClickListener.onClick(view, Collections.singletonList(slide)); } else { Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + String.valueOf(slide) + " downloadClickListener: " + String.valueOf(downloadClickListener)); } diff --git a/src/org/thoughtcrime/securesms/components/TransferControlView.java b/src/org/thoughtcrime/securesms/components/TransferControlView.java index af43c1e2eb..63d40beab2 100644 --- a/src/org/thoughtcrime/securesms/components/TransferControlView.java +++ b/src/org/thoughtcrime/securesms/components/TransferControlView.java @@ -15,32 +15,42 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.TextView; +import com.annimon.stream.Stream; import com.nineoldandroids.animation.Animator; import com.nineoldandroids.animation.ValueAnimator; -import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener; import com.pnikosis.materialishprogress.ProgressWheel; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.events.PartProgressEvent; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + public class TransferControlView extends FrameLayout { private static final int TRANSITION_MS = 300; - @Nullable private Slide slide; - @Nullable private View current; + @Nullable private List slides; + @Nullable private View current; private final ProgressWheel progressWheel; - private final TextView downloadDetails; + private final View downloadDetails; + private final TextView downloadDetailsText; private final int contractedWidth; private final int expandedWidth; + private final Map downloadProgress; + public TransferControlView(Context context) { this(context, null); } @@ -61,10 +71,12 @@ public class TransferControlView extends FrameLayout { ViewUtil.setBackground(this, background); setVisibility(GONE); - this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel); - this.downloadDetails = ViewUtil.findById(this, R.id.download_details); - this.contractedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_contracted_width); - this.expandedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_expanded_width); + this.downloadProgress = new HashMap<>(); + this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel); + this.downloadDetails = ViewUtil.findById(this, R.id.download_details); + this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text); + this.contractedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_contracted_width); + this.expandedWidth = getResources().getDimensionPixelSize(R.dimen.transfer_controls_expanded_width); } @Override @@ -91,20 +103,54 @@ public class TransferControlView extends FrameLayout { EventBus.getDefault().unregister(this); } - public void setSlide(final @NonNull Slide slide) { - this.slide = slide; - if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) { - showProgressSpinner(); - } else if (slide.isPendingDownload()) { - downloadDetails.setText(slide.getContentDescription()); - display(downloadDetails); + public void setSlide(final @NonNull Slide slides) { + setSlides(Collections.singletonList(slides)); + } + + public void setSlides(final @NonNull List slides) { + if (slides.isEmpty()) { + throw new IllegalArgumentException("Must provide at least one slide."); + } + + this.slides = slides; + + if (!isUpdateToExistingSet(slides)) { + downloadProgress.clear(); + Stream.of(slides).forEach(s -> downloadProgress.put(s.asAttachment(), 0f)); } else { - display(null); + for (Slide slide : slides) { + if (slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) { + downloadProgress.put(slide.asAttachment(), 1f); + } + } + } + + switch (getTransferState(slides)) { + case AttachmentDatabase.TRANSFER_PROGRESS_STARTED: + showProgressSpinner(calculateProgress(downloadProgress)); + break; + case AttachmentDatabase.TRANSFER_PROGRESS_PENDING: + case AttachmentDatabase.TRANSFER_PROGRESS_FAILED: + downloadDetailsText.setText(getDownloadText(this.slides)); + display(downloadDetails); + break; + default: + display(null); + break; } } public void showProgressSpinner() { - progressWheel.spin(); + showProgressSpinner(calculateProgress(downloadProgress)); + } + + public void showProgressSpinner(float progress) { + if (progress == 0) { + progressWheel.spin(); + } else { + progressWheel.setInstantProgress(progress); + } + display(progressWheel); } @@ -120,12 +166,51 @@ public class TransferControlView extends FrameLayout { current.setVisibility(GONE); } current = null; - slide = null; + slides = null; + } + + public void setShowDownloadText(boolean showDownloadText) { + downloadDetailsText.setVisibility(showDownloadText ? VISIBLE : GONE); + } + + private boolean isUpdateToExistingSet(@NonNull List slides) { + if (slides.size() != downloadProgress.size()) { + return false; + } + + for (Slide slide : slides) { + if (!downloadProgress.containsKey(slide.asAttachment())) { + return false; + } + } + + return true; + } + + private int getTransferState(@NonNull List slides) { + int transferState = AttachmentDatabase.TRANSFER_PROGRESS_DONE; + for (Slide slide : slides) { + if (slide.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING && transferState == AttachmentDatabase.TRANSFER_PROGRESS_DONE) { + transferState = slide.getTransferState(); + } else { + transferState = Math.max(transferState, slide.getTransferState()); + } + } + return transferState; + } + + private String getDownloadText(@NonNull List slides) { + if (slides.size() == 1) { + return slides.get(0).getContentDescription(); + } else { + int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE ? count + 1 : count); + return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount); + } } private void display(@Nullable final View view) { - final int sourceWidth = current == downloadDetails ? expandedWidth : contractedWidth; - final int targetWidth = view == downloadDetails ? expandedWidth : contractedWidth; + final int sourceWidth = (current == downloadDetails && downloadDetailsText.getVisibility() == VISIBLE) ? expandedWidth : contractedWidth; + final int targetWidth = (view == downloadDetails && downloadDetailsText.getVisibility() == VISIBLE) ? expandedWidth : contractedWidth; if (current == view || current == null) { ViewGroup.LayoutParams layoutParams = getLayoutParams(); @@ -149,28 +234,31 @@ public class TransferControlView extends FrameLayout { private Animator getWidthAnimator(final int from, final int to) { final ValueAnimator anim = ValueAnimator.ofInt(from, to); - anim.addUpdateListener(new AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - final int val = (Integer)animation.getAnimatedValue(); - final ViewGroup.LayoutParams layoutParams = getLayoutParams(); - layoutParams.width = val; - setLayoutParams(layoutParams); - } + anim.addUpdateListener(animation -> { + final int val = (Integer)animation.getAnimatedValue(); + final ViewGroup.LayoutParams layoutParams = getLayoutParams(); + layoutParams.width = val; + setLayoutParams(layoutParams); }); anim.setInterpolator(new FastOutSlowInInterpolator()); anim.setDuration(TRANSITION_MS); return anim; } + private float calculateProgress(@NonNull Map downloadProgress) { + float totalProgress = 0; + for (float progress : downloadProgress.values()) { + totalProgress += progress / downloadProgress.size(); + } + return totalProgress; + } + @Subscribe(sticky = true, threadMode = ThreadMode.ASYNC) public void onEventAsync(final PartProgressEvent event) { - if (this.slide != null && event.attachment.equals(this.slide.asAttachment())) { - Util.runOnMain(new Runnable() { - @Override - public void run() { - progressWheel.setInstantProgress(((float)event.progress) / event.total); - } + if (downloadProgress.containsKey(event.attachment)) { + Util.runOnMain(() -> { + downloadProgress.put(event.attachment, ((float)event.progress) / event.total); + progressWheel.setInstantProgress(calculateProgress(downloadProgress)); }); } } diff --git a/src/org/thoughtcrime/securesms/contactshare/Contact.java b/src/org/thoughtcrime/securesms/contactshare/Contact.java index ec9028ba9b..39d6f1cd39 100644 --- a/src/org/thoughtcrime/securesms/contactshare/Contact.java +++ b/src/org/thoughtcrime/securesms/contactshare/Contact.java @@ -642,7 +642,7 @@ public class Contact implements Parcelable { private static Attachment attachmentFromUri(@Nullable Uri uri) { if (uri == null) return null; - return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false); + return new UriAttachment(uri, MediaUtil.IMAGE_JPEG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, false, false, null); } @Override diff --git a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 86efcb027a..7fe79dc8f9 100644 --- a/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/src/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -98,6 +98,7 @@ public class AttachmentDatabase extends Database { private static final String THUMBNAIL_RANDOM = "thumbnail_random"; static final String WIDTH = "width"; static final String HEIGHT = "height"; + static final String CAPTION = "caption"; public static final String DIRECTORY = "parts"; @@ -113,7 +114,8 @@ public class AttachmentDatabase extends Database { CONTENT_LOCATION, DATA, THUMBNAIL, TRANSFER_STATE, SIZE, FILE_NAME, THUMBNAIL, THUMBNAIL_ASPECT_RATIO, UNIQUE_ID, DIGEST, FAST_PREFLIGHT_ID, VOICE_NOTE, - QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT}; + QUOTE, DATA_RANDOM, THUMBNAIL_RANDOM, WIDTH, HEIGHT, + CAPTION }; public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ROW_ID + " INTEGER PRIMARY KEY, " + MMS_ID + " INTEGER, " + "seq" + " INTEGER DEFAULT 0, " + @@ -125,7 +127,8 @@ public class AttachmentDatabase extends Database { FILE_NAME + " TEXT, " + THUMBNAIL + " TEXT, " + THUMBNAIL_ASPECT_RATIO + " REAL, " + UNIQUE_ID + " INTEGER NOT NULL, " + DIGEST + " BLOB, " + FAST_PREFLIGHT_ID + " TEXT, " + VOICE_NOTE + " INTEGER DEFAULT 0, " + DATA_RANDOM + " BLOB, " + THUMBNAIL_RANDOM + " BLOB, " + - QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0);"; + QUOTE + " INTEGER DEFAULT 0, " + WIDTH + " INTEGER DEFAULT 0, " + HEIGHT + " INTEGER DEFAULT 0, " + + CAPTION + " TEXT DEFAULT NULL);"; public static final String[] CREATE_INDEXS = { "CREATE INDEX IF NOT EXISTS part_mms_id_index ON " + TABLE_NAME + " (" + MMS_ID + ");", @@ -416,7 +419,8 @@ public class AttachmentDatabase extends Database { databaseAttachment.isVoiceNote(), mediaStream.getWidth(), mediaStream.getHeight(), - databaseAttachment.isQuote()); + databaseAttachment.isQuote(), + databaseAttachment.getCaption()); } @@ -589,7 +593,8 @@ public class AttachmentDatabase extends Database { object.getInt(VOICE_NOTE) == 1, object.getInt(WIDTH), object.getInt(HEIGHT), - object.getInt(QUOTE) == 1)); + object.getInt(QUOTE) == 1, + object.getString(CAPTION))); } } @@ -612,7 +617,8 @@ public class AttachmentDatabase extends Database { cursor.getInt(cursor.getColumnIndexOrThrow(VOICE_NOTE)) == 1, cursor.getInt(cursor.getColumnIndexOrThrow(WIDTH)), cursor.getInt(cursor.getColumnIndexOrThrow(HEIGHT)), - cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1)); + cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE)) == 1, + cursor.getString(cursor.getColumnIndexOrThrow(CAPTION)))); } } catch (JSONException e) { throw new AssertionError(e); @@ -650,6 +656,7 @@ public class AttachmentDatabase extends Database { contentValues.put(WIDTH, attachment.getWidth()); contentValues.put(HEIGHT, attachment.getHeight()); contentValues.put(QUOTE, quote); + contentValues.put(CAPTION, attachment.getCaption()); if (dataInfo != null) { contentValues.put(DATA, dataInfo.file.getAbsolutePath()); diff --git a/src/org/thoughtcrime/securesms/database/MediaDatabase.java b/src/org/thoughtcrime/securesms/database/MediaDatabase.java index 6feb9731e8..ff35cead9b 100644 --- a/src/org/thoughtcrime/securesms/database/MediaDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MediaDatabase.java @@ -35,6 +35,7 @@ public class MediaDatabase extends Database { + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.WIDTH + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.HEIGHT + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ", " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", " @@ -84,7 +85,9 @@ public class MediaDatabase extends Database { private final long date; private final boolean outgoing; - private MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) { + // TODO: Make private again + public MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) { +// private MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) { this.attachment = attachment; this.address = address; this.date = date; diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index fbd1e52dfb..961a1316f5 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -163,7 +163,8 @@ public class MmsDatabase extends MessagingDatabase { "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + "'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + - "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + + "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + + "'" + AttachmentDatabase.CAPTION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, }; @@ -715,7 +716,8 @@ public class MmsDatabase extends MessagingDatabase { databaseAttachment.isVoiceNote(), databaseAttachment.getWidth(), databaseAttachment.getHeight(), - databaseAttachment.isQuote())); + databaseAttachment.isQuote(), + databaseAttachment.getCaption())); } return insertMediaMessage(request.getBody(), diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 7f72a0551e..2ce3cd079b 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -227,7 +227,8 @@ public class MmsSmsDatabase extends Database { "'" + AttachmentDatabase.QUOTE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.QUOTE + ", " + "'" + AttachmentDatabase.CONTENT_DISPOSITION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_DISPOSITION + ", " + "'" + AttachmentDatabase.NAME + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.NAME + ", " + - "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + + "'" + AttachmentDatabase.TRANSFER_STATE + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.TRANSFER_STATE + ", " + + "'" + AttachmentDatabase.CAPTION + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CAPTION + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, @@ -326,6 +327,7 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(AttachmentDatabase.WIDTH); mmsColumnsPresent.add(AttachmentDatabase.HEIGHT); mmsColumnsPresent.add(AttachmentDatabase.QUOTE); + mmsColumnsPresent.add(AttachmentDatabase.CAPTION); mmsColumnsPresent.add(AttachmentDatabase.CONTENT_DISPOSITION); mmsColumnsPresent.add(AttachmentDatabase.NAME); mmsColumnsPresent.add(AttachmentDatabase.TRANSFER_STATE); diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 7acb168c17..447bdbe05e 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -57,8 +57,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int QUOTE_MISSING = 11; private static final int NOTIFICATION_CHANNELS = 12; private static final int SECRET_SENDER = 13; + private static final int ATTACHMENT_CAPTIONS = 14; - private static final int DATABASE_VERSION = 13; + private static final int DATABASE_VERSION = 14; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -294,6 +295,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL("ALTER TABLE sms ADD COLUMN unidentified INTEGER DEFAULT 0"); } + if (oldVersion < ATTACHMENT_CAPTIONS) { + db.execSQL("ALTER TABLE part ADD COLUMN caption TEXT DEFAULT NULL"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/src/org/thoughtcrime/securesms/groups/GroupManager.java b/src/org/thoughtcrime/securesms/groups/GroupManager.java index 9fee1eda13..355c3996a1 100644 --- a/src/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/src/org/thoughtcrime/securesms/groups/GroupManager.java @@ -112,7 +112,7 @@ public class GroupManager { if (avatar != null) { Uri avatarUri = MemoryBlobProvider.getInstance().createSingleUseUri(avatar); - avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false); + avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, null); } OutgoingGroupMediaMessage outgoingMessage = new OutgoingGroupMediaMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, null, Collections.emptyList()); diff --git a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java index fe377665d4..b57a6f2d8f 100644 --- a/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/AttachmentDownloadJob.java @@ -92,6 +92,16 @@ public class AttachmentDownloadJob extends ContextJob implements InjectableType @Override public void onAdded() { Log.i(TAG, "onAdded() messageId: " + messageId + " partRowId: " + partRowId + " partUniqueId: " + partUniqueId + " manual: " + manual); + + final AttachmentDatabase database = DatabaseFactory.getAttachmentDatabase(context); + final AttachmentId attachmentId = new AttachmentId(partRowId, partUniqueId); + final DatabaseAttachment attachment = database.getAttachment(attachmentId); + final boolean pending = attachment != null && attachment.getTransferState() != AttachmentDatabase.TRANSFER_PROGRESS_DONE; + + if (pending && (manual || AttachmentUtil.isAutoDownloadPermitted(context, attachment))) { + Log.i(TAG, "onAdded() Marking attachment progress as 'started'"); + database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED); + } } @Override diff --git a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java index 1ec9512220..12e7a2c0d0 100644 --- a/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java +++ b/src/org/thoughtcrime/securesms/jobs/MmsDownloadJob.java @@ -241,7 +241,7 @@ public class MmsDownloadJob extends ContextJob { attachments.add(new UriAttachment(uri, Util.toIsoString(part.getContentType()), AttachmentDatabase.TRANSFER_PROGRESS_DONE, - part.getData().length, name, false, false)); + part.getData().length, name, false, false, null)); } } } diff --git a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java index 8fa6c3e100..11c52595f2 100644 --- a/src/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/src/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.LinkedList; import java.util.List; +import java.util.UUID; import java.util.concurrent.TimeUnit; public abstract class PushSendJob extends SendJob { @@ -120,6 +121,7 @@ public abstract class PushSendJob extends SendJob { .withVoiceNote(attachment.isVoiceNote()) .withWidth(attachment.getWidth()) .withHeight(attachment.getHeight()) + .withCaption(attachment.getCaption()) .withListener((total, progress) -> EventBus.getDefault().postSticky(new PartProgressEvent(attachment, total, progress))) .build(); } catch (IOException ioe) { diff --git a/src/org/thoughtcrime/securesms/mediapreview/AlbumRailAdapter.java b/src/org/thoughtcrime/securesms/mediapreview/AlbumRailAdapter.java new file mode 100644 index 0000000000..9a82363931 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mediapreview/AlbumRailAdapter.java @@ -0,0 +1,100 @@ +package org.thoughtcrime.securesms.mediapreview; + +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.ThumbnailView; +import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; +import org.thoughtcrime.securesms.mms.GlideRequests; + +import java.util.ArrayList; +import java.util.List; + +public class AlbumRailAdapter extends RecyclerView.Adapter { + + private final GlideRequests glideRequests; + private final List records; + private final RailItemClickedListener listener; + + private int activePosition; + + public AlbumRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemClickedListener listener) { + this.glideRequests = glideRequests; + this.records = new ArrayList<>(); + this.listener = listener; + + setHasStableIds(true); + } + + @NonNull + @Override + public AlbumRailViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + return new AlbumRailViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_preview_album_rail_item, viewGroup, false)); + } + + @Override + public void onBindViewHolder(@NonNull AlbumRailViewHolder albumRailViewHolder, int i) { + albumRailViewHolder.bind(records.get(i), i == activePosition, glideRequests, listener, i - activePosition); + } + + @Override + public void onViewRecycled(@NonNull AlbumRailViewHolder holder) { + holder.recycle(); + } + + @Override + public long getItemId(int position) { + return records.get(position).getAttachment().getAttachmentId().getUniqueId(); + } + + @Override + public int getItemCount() { + return records.size(); + } + + public void setRecords(@NonNull List records, int activePosition) { + this.activePosition = activePosition; + + this.records.clear(); + this.records.addAll(records); + + notifyDataSetChanged(); + } + + static class AlbumRailViewHolder extends RecyclerView.ViewHolder { + + private final ThumbnailView image; + + AlbumRailViewHolder(@NonNull View itemView) { + super(itemView); + image = (ThumbnailView) itemView; + } + + void bind(@NonNull MediaRecord record, boolean isActive, @NonNull GlideRequests glideRequests, + @NonNull RailItemClickedListener railItemClickedListener, int distanceFromActive) + { + if (record.getAttachment().getThumbnailUri() != null) { + image.setImageResource(glideRequests, record.getAttachment().getThumbnailUri()); + } else if (record.getAttachment().getDataUri() != null) { + image.setImageResource(glideRequests, record.getAttachment().getDataUri()); + } else { + image.clear(glideRequests); + } + + image.setBackgroundResource(isActive ? R.drawable.album_rail_item_background : 0); + image.setOnClickListener(v -> railItemClickedListener.onRailItemClicked(distanceFromActive)); + } + + void recycle() { + image.setOnClickListener(null); + } + } + + public interface RailItemClickedListener { + void onRailItemClicked(int distanceFromActive); + } +} diff --git a/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java new file mode 100644 index 0000000000..20e3027535 --- /dev/null +++ b/src/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java @@ -0,0 +1,110 @@ +package org.thoughtcrime.securesms.mediapreview; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.MutableLiveData; +import android.arch.lifecycle.ViewModel; +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class MediaPreviewViewModel extends ViewModel { + + private final MutableLiveData previewData = new MutableLiveData<>(); + + private boolean leftIsRecent; + + private @Nullable Cursor cursor; + + public void setCursor(@Nullable Cursor cursor, boolean leftIsRecent) { + this.cursor = cursor; + this.leftIsRecent = leftIsRecent; + } + + public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) { + if (cursor == null) { + previewData.postValue(new PreviewData(Collections.emptyList(), null, 0)); + return; + } + + activePosition = getCursorPosition(activePosition); + + cursor.moveToPosition(activePosition); + + MediaRecord activeRecord = MediaRecord.from(context, cursor); + LinkedList rail = new LinkedList<>(); + + rail.add(activeRecord); + + while (cursor.moveToPrevious()) { + MediaRecord record = MediaRecord.from(context, cursor); + if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) { + rail.addFirst(record); + } else { + break; + } + } + + cursor.moveToPosition(activePosition); + + while (cursor.moveToNext()) { + MediaRecord record = MediaRecord.from(context, cursor); + if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) { + rail.addLast(record); + } else { + break; + } + } + + if (!leftIsRecent) { + Collections.reverse(rail); + } + + previewData.postValue(new PreviewData(rail.size() > 1 ? rail : Collections.emptyList(), + activeRecord.getAttachment().getCaption(), + rail.indexOf(activeRecord))); + } + + private int getCursorPosition(int position) { + if (cursor == null) { + return 0; + } + + if (leftIsRecent) return position; + else return cursor.getCount() - 1 - position; + } + + public LiveData getPreviewData() { + return previewData; + } + + public static class PreviewData { + private final List albumThumbnails; + private final String caption; + private final int activePosition; + + public PreviewData(@NonNull List albumThumbnails, @Nullable String caption, int activePosition) { + this.albumThumbnails = albumThumbnails; + this.caption = caption; + this.activePosition = activePosition; + } + + public @NonNull List getAlbumThumbnails() { + return albumThumbnails; + } + + public @Nullable String getCaption() { + return caption; + } + + public int getActivePosition() { + return activePosition; + } + } +} diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index 0d9ad9b730..8aa631d255 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -34,6 +34,7 @@ import android.provider.OpenableColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; + import org.thoughtcrime.securesms.logging.Log; import android.util.Pair; import android.view.View; @@ -493,6 +494,7 @@ public class AttachmentManager { Intent intent = new Intent(context, MediaPreviewActivity.class); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize()); + intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull()); intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, true); intent.setDataAndType(slide.getUri(), slide.getContentType()); diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java index b3b5b56150..ef65cb3fcf 100644 --- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -38,7 +38,7 @@ public class AudioSlide extends Slide { } public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) { - super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false)); + super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote, false, null)); } public AudioSlide(Context context, Attachment attachment) { diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index 22261ba505..3c258410b7 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -41,7 +41,6 @@ public abstract class Slide { public Slide(@NonNull Context context, @NonNull Attachment attachment) { this.context = context; this.attachment = attachment; - } public String getContentType() { @@ -63,6 +62,11 @@ public abstract class Slide { return Optional.absent(); } + @NonNull + public Optional getCaption() { + return Optional.fromNullable(attachment.getCaption()); + } + @NonNull public Optional getFileName() { return Optional.fromNullable(attachment.getFileName()); @@ -112,7 +116,7 @@ public abstract class Slide { getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_PENDING; } - public long getTransferState() { + public int getTransferState() { return attachment.getTransferState(); } @@ -152,7 +156,8 @@ public abstract class Slide { fileName, fastPreflightId, voiceNote, - quote); + quote, + null); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } diff --git a/src/org/thoughtcrime/securesms/mms/SlideDeck.java b/src/org/thoughtcrime/securesms/mms/SlideDeck.java index 46b6ac30dd..d81524a194 100644 --- a/src/org/thoughtcrime/securesms/mms/SlideDeck.java +++ b/src/org/thoughtcrime/securesms/mms/SlideDeck.java @@ -20,6 +20,8 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.annimon.stream.Stream; + import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.util.MediaUtil; import org.whispersystems.libsignal.util.guava.Optional; @@ -103,6 +105,10 @@ public class SlideDeck { return null; } + public @NonNull List getThumbnailSlides() { + return Stream.of(slides).filter(Slide::hasImage).toList(); + } + public @Nullable AudioSlide getAudioSlide() { for (Slide slide : slides) { if (slide.hasAudio()) { diff --git a/src/org/thoughtcrime/securesms/mms/SlidesClickedListener.java b/src/org/thoughtcrime/securesms/mms/SlidesClickedListener.java new file mode 100644 index 0000000000..9e6914d2ee --- /dev/null +++ b/src/org/thoughtcrime/securesms/mms/SlidesClickedListener.java @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.mms; + +import android.view.View; + +import java.util.List; + +public interface SlidesClickedListener { + void onClick(View v, List slides); +} diff --git a/src/org/thoughtcrime/securesms/video/VideoPlayer.java b/src/org/thoughtcrime/securesms/video/VideoPlayer.java index b309b3e2f2..bf6b59a7d5 100644 --- a/src/org/thoughtcrime/securesms/video/VideoPlayer.java +++ b/src/org/thoughtcrime/securesms/video/VideoPlayer.java @@ -21,6 +21,7 @@ import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; +import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; @@ -46,6 +47,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -70,6 +72,7 @@ public class VideoPlayer extends FrameLayout { @Nullable private final PlayerView exoView; @Nullable private SimpleExoPlayer exoPlayer; + @Nullable private PlayerControlView exoControls; @Nullable private AttachmentServer attachmentServer; @Nullable private Window window; @@ -89,6 +92,8 @@ public class VideoPlayer extends FrameLayout { if (Build.VERSION.SDK_INT >= 16) { this.exoView = ViewUtil.findById(this, R.id.video_view); this.videoView = null; + this.exoControls = new PlayerControlView(getContext()); + this.exoControls.setShowTimeoutMs(-1); } else { this.videoView = ViewUtil.findById(this, R.id.video_view); this.exoView = null; @@ -111,6 +116,19 @@ public class VideoPlayer extends FrameLayout { } } + public void hideControls() { + if (this.exoView != null) { + this.exoView.hideController(); + } + } + + public @Nullable View getControlView() { + if (this.exoControls != null) { + return this.exoControls; + } + return null; + } + public void cleanup() { if (this.attachmentServer != null) { this.attachmentServer.stop(); @@ -137,6 +155,8 @@ public class VideoPlayer extends FrameLayout { exoPlayer.addListener(new ExoPlayerListener(window)); //noinspection ConstantConditions exoView.setPlayer(exoPlayer); + //noinspection ConstantConditions + exoControls.setPlayer(exoPlayer); DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null); AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(getContext(), defaultDataSourceFactory, null);