From 69d0242574cc68b068afe3a00861e574b176f08b Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Sun, 16 Oct 2016 19:05:07 -0700 Subject: [PATCH] Giphy integration // FREEBIE --- AndroidManifest.xml | 5 + build.gradle | 2 + proguard-appcompat-v7.pro | 4 + res/drawable-hdpi/ic_dashboard_white_24dp.png | Bin 0 -> 130 bytes res/drawable-hdpi/ic_gif_white_36dp.png | Bin 0 -> 243 bytes .../ic_view_stream_white_24dp.png | Bin 0 -> 105 bytes res/drawable-hdpi/poweredby_giphy.png | Bin 0 -> 4124 bytes res/drawable-mdpi/ic_dashboard_white_24dp.png | Bin 0 -> 94 bytes res/drawable-mdpi/ic_gif_white_36dp.png | Bin 0 -> 196 bytes .../ic_view_stream_white_24dp.png | Bin 0 -> 82 bytes res/drawable-mdpi/poweredby_giphy.png | Bin 0 -> 2473 bytes .../ic_dashboard_white_24dp.png | Bin 0 -> 104 bytes res/drawable-xhdpi/ic_gif_white_36dp.png | Bin 0 -> 213 bytes .../ic_view_stream_white_24dp.png | Bin 0 -> 92 bytes res/drawable-xhdpi/poweredby_giphy.png | Bin 0 -> 5567 bytes .../ic_dashboard_white_24dp.png | Bin 0 -> 109 bytes res/drawable-xxhdpi/ic_gif_white_36dp.png | Bin 0 -> 312 bytes .../ic_view_stream_white_24dp.png | Bin 0 -> 94 bytes res/drawable-xxhdpi/poweredby_giphy.png | Bin 0 -> 8825 bytes .../ic_dashboard_white_24dp.png | Bin 0 -> 110 bytes res/drawable-xxxhdpi/ic_gif_white_36dp.png | Bin 0 -> 308 bytes .../ic_view_stream_white_24dp.png | Bin 0 -> 98 bytes res/drawable-xxxhdpi/poweredby_giphy.png | Bin 0 -> 6626 bytes res/layout/attachment_type_selector.xml | 15 ++ res/layout/giphy_activity.xml | 52 ++++++ res/layout/giphy_activity_toolbar.xml | 86 +++++++++ res/layout/giphy_fragment.xml | 27 +++ res/layout/giphy_thumbnail.xml | 22 +++ res/values/strings.xml | 15 ++ .../securesms/ConversationActivity.java | 9 +- .../components/AttachmentTypeSelector.java | 7 +- .../securesms/components/ThumbnailView.java | 21 ++- .../components/ZoomingImageView.java | 2 + .../contacts/avatars/ContactPhotoFactory.java | 2 + .../securesms/giph/model/GiphyImage.java | 62 +++++++ .../securesms/giph/model/GiphyResponse.java | 17 ++ .../securesms/giph/net/GiphyGifLoader.java | 23 +++ .../securesms/giph/net/GiphyLoader.java | 70 +++++++ .../giph/net/GiphyProxySelector.java | 73 ++++++++ .../giph/net/GiphyStickerLoader.java | 23 +++ .../giph/ui/AspectRatioImageView.java | 119 ++++++++++++ .../securesms/giph/ui/GiphyActivity.java | 163 ++++++++++++++++ .../giph/ui/GiphyActivityToolbar.java | 174 ++++++++++++++++++ .../securesms/giph/ui/GiphyAdapter.java | 155 ++++++++++++++++ .../securesms/giph/ui/GiphyFragment.java | 122 ++++++++++++ .../securesms/giph/ui/GiphyGifFragment.java | 19 ++ .../giph/ui/GiphyStickerFragment.java | 17 ++ .../giph/util/InfiniteScrollListener.java | 48 +++++ .../giph/util/RecyclerViewPositionHelper.java | 115 ++++++++++++ .../securesms/glide/OkHttpStreamFetcher.java | 80 ++++++++ .../securesms/glide/OkHttpUrlLoader.java | 75 ++++++++ .../securesms/mms/AttachmentManager.java | 6 + .../securesms/mms/TextSecureGlideModule.java | 5 +- .../SingleRecipientNotificationBuilder.java | 2 + .../securesms/util/JsonUtils.java | 7 +- src/org/thoughtcrime/securesms/util/Util.java | 8 + .../util/concurrent/ListenableFuture.java | 3 +- .../util/concurrent/SettableFuture.java | 4 +- 58 files changed, 1644 insertions(+), 15 deletions(-) create mode 100644 res/drawable-hdpi/ic_dashboard_white_24dp.png create mode 100644 res/drawable-hdpi/ic_gif_white_36dp.png create mode 100644 res/drawable-hdpi/ic_view_stream_white_24dp.png create mode 100644 res/drawable-hdpi/poweredby_giphy.png create mode 100644 res/drawable-mdpi/ic_dashboard_white_24dp.png create mode 100644 res/drawable-mdpi/ic_gif_white_36dp.png create mode 100644 res/drawable-mdpi/ic_view_stream_white_24dp.png create mode 100644 res/drawable-mdpi/poweredby_giphy.png create mode 100644 res/drawable-xhdpi/ic_dashboard_white_24dp.png create mode 100644 res/drawable-xhdpi/ic_gif_white_36dp.png create mode 100644 res/drawable-xhdpi/ic_view_stream_white_24dp.png create mode 100644 res/drawable-xhdpi/poweredby_giphy.png create mode 100644 res/drawable-xxhdpi/ic_dashboard_white_24dp.png create mode 100644 res/drawable-xxhdpi/ic_gif_white_36dp.png create mode 100644 res/drawable-xxhdpi/ic_view_stream_white_24dp.png create mode 100644 res/drawable-xxhdpi/poweredby_giphy.png create mode 100644 res/drawable-xxxhdpi/ic_dashboard_white_24dp.png create mode 100644 res/drawable-xxxhdpi/ic_gif_white_36dp.png create mode 100644 res/drawable-xxxhdpi/ic_view_stream_white_24dp.png create mode 100644 res/drawable-xxxhdpi/poweredby_giphy.png create mode 100644 res/layout/giphy_activity.xml create mode 100644 res/layout/giphy_activity_toolbar.xml create mode 100644 res/layout/giphy_fragment.xml create mode 100644 res/layout/giphy_thumbnail.xml create mode 100644 src/org/thoughtcrime/securesms/giph/model/GiphyImage.java create mode 100644 src/org/thoughtcrime/securesms/giph/model/GiphyResponse.java create mode 100644 src/org/thoughtcrime/securesms/giph/net/GiphyGifLoader.java create mode 100644 src/org/thoughtcrime/securesms/giph/net/GiphyLoader.java create mode 100644 src/org/thoughtcrime/securesms/giph/net/GiphyProxySelector.java create mode 100644 src/org/thoughtcrime/securesms/giph/net/GiphyStickerLoader.java create mode 100644 src/org/thoughtcrime/securesms/giph/ui/AspectRatioImageView.java create mode 100644 src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java create mode 100644 src/org/thoughtcrime/securesms/giph/ui/GiphyActivityToolbar.java create mode 100644 src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java create mode 100644 src/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java create mode 100644 src/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java create mode 100644 src/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java create mode 100644 src/org/thoughtcrime/securesms/giph/util/InfiniteScrollListener.java create mode 100644 src/org/thoughtcrime/securesms/giph/util/RecyclerViewPositionHelper.java create mode 100644 src/org/thoughtcrime/securesms/glide/OkHttpStreamFetcher.java create mode 100644 src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 44275e9d6a7..9ec840247d3 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -266,6 +266,11 @@ android:windowSoftInputMode="stateHidden" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> + + diff --git a/build.gradle b/build.gradle index e96c363d3c8..0e4ed3c78f7 100644 --- a/build.gradle +++ b/build.gradle @@ -172,6 +172,8 @@ android { buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L" buildConfigField "String", "TEXTSECURE_URL", "\"https://textsecure-service.whispersystems.org\"" + buildConfigField "String", "GIPHY_PROXY_HOST", "\"giphy-proxy-production.whispersystems.org\"" + buildConfigField "int", "GIPHY_PROXY_PORT", "80" buildConfigField "String", "USER_AGENT", "\"OWA\"" buildConfigField "String", "REDPHONE_MASTER_URL", "\"https://redphone-master.whispersystems.org\"" buildConfigField "String", "REDPHONE_RELAY_HOST", "\"relay.whispersystems.org\"" diff --git a/proguard-appcompat-v7.pro b/proguard-appcompat-v7.pro index 718eb9da93b..f0d673934f8 100644 --- a/proguard-appcompat-v7.pro +++ b/proguard-appcompat-v7.pro @@ -7,3 +7,7 @@ -keep public class * extends android.support.v4.view.ActionProvider { public (android.content.Context); } + +-keepattributes *Annotation* +-keep public class * extends android.support.design.widget.CoordinatorLayout.Behavior { *; } +-keep public class * extends android.support.design.widget.ViewOffsetBehavior { *; } diff --git a/res/drawable-hdpi/ic_dashboard_white_24dp.png b/res/drawable-hdpi/ic_dashboard_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3208779f85536c837f2d23cdfc7633ddb0e36d09 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;r>Bc!h{y5d1PNA`=Kp;Er8hXS zAKJ8n&GgT4J}HjVeNrY_0u$K+wH+r-a($6};ljD>AKeob#YDG!yp{1z-n4_SviC<) dipT>NhMfZUxAqHKn*$AH@O1TaS?83{1OW0GDY5_n literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_gif_white_36dp.png b/res/drawable-hdpi/ic_gif_white_36dp.png new file mode 100644 index 0000000000000000000000000000000000000000..17b6ed692e2905376aef70befeeef6f9f857bc94 GIT binary patch literal 243 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL0wmRZ7KH(+U7jwEArXg@6C_v{Cy4Yk1sZU< z9n_F8d0>C~|F8em|K%N;tpBw;K2=$u#Fj9Ry-SN@j>D|ft^YSNW^b6oH03$NGu4E^ z@&=RB-;%bjX)~K9%aFb6b3G#$&w*sgO}n^?bfOzV`BtY+Q4yG=%iQI(fWcO$xsk0) z_xFE>#0wpp&oFX{p4iGW@ymXJO+AyYcoZ#Lc4U*3PT1ey`L5EZBN~<}XF2ZqTd%RN isbb3~DI_N*ure6&%84p&ezSl92s~Z=T-G@yGywpEeOuB1 literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_view_stream_white_24dp.png b/res/drawable-hdpi/ic_view_stream_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..857becfc24689d8a8ac24f68e7a7163b513f7e5b GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;fv1aOh{y5d1PRu~3{3ybC-BH_ zkan@kkTA`-qA*L<<5lX!ixJYM83vLjTy6&)m>I6G+&Vo?q?#3|m%-E3&t;ucLK6Uo CM;d4V literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/poweredby_giphy.png b/res/drawable-hdpi/poweredby_giphy.png new file mode 100644 index 0000000000000000000000000000000000000000..df5e4f06e1391d0516d244db45509c3b8bef82db GIT binary patch literal 4124 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 a0RR6e{v@HJf!JmM0000UftDnm{r-UW|E=?OF literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_gif_white_36dp.png b/res/drawable-mdpi/ic_gif_white_36dp.png new file mode 100644 index 0000000000000000000000000000000000000000..0978a141a9fcab89a263ae1d917f5e523c201c8e GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8x2KC^NCo5DO9wfb3Y^nVJmz(Eey#Dmlh8vGZxtDPS8geByw{mrL z9#4Cu$MLX1k*n~Vl+lB(`>!4Dt$MHMzERy{qrXDDRO_MNf2C^~H>obXm^`21+Wi$m u<#GJJXChu7*!EeGeZj&yZq+FPPUa^*&JwSi|L_XXSqz@8elF{r5}E+N0ZJeM literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_view_stream_white_24dp.png b/res/drawable-mdpi/ic_view_stream_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..a0a663458ae751b08215eecd9538a835f7ffb8bd GIT binary patch literal 82 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1NlzEY5R22v2@)qC*gG1mN;HXJ e_ukN_!oa}va$3a%^Sl_KA_h-aKbLh*2~7aUvlN2> literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/poweredby_giphy.png b/res/drawable-mdpi/poweredby_giphy.png new file mode 100644 index 0000000000000000000000000000000000000000..0cee864a36e477d89f7122dc6a0a5e350bf8ad88 GIT binary patch literal 2473 zcmV;a30C%rP)000sQ1^@s6+|AWf000SaNklq#l@lEp2ci%Z@=FFKh zXJ*d#z0suyD(`#_QZXYx%L zSG->FD#c?I3lv?&J&N^;^A(>_Yz(~f70VS171K8Uy^8Cs|C5TxDRx_*NbiPkQCy`s zPw^LPb4Pri;+Z!8gkq)QE7I#U2Pi(TxKEJzCdH!_XDYT?+nD03in9a1Kbhw^#ZGHu zu2U7)OOt~V0q3w+(6Q$e6xUR}G-a3$ZMP~;P>dMgLdA}PZ)uRfx@t$;6lWOEs6AH} zV^&yuH|V{%M%EFE`HGv1_RWfejJIq2N7wNGOfeSbn1zv99yypgd)T)(X;0?q48b?7 zd_UcfRQyWuSV@s;+WNZ&w>OdK!HV}wO3h;YcNvfKPK7!CX>GfL+(=W#Mv9FTm8l}@9zo|r-SJQ@reN9pd-EF+I{X+3U#d{=e z(%&Sdrr#@`CwYS^$&Tvi6!P7PdE%_plN%(Z@=?>v9q6NqLkiyVAjCCy3i-h6NK*S3 zB&FZ3$$y>VX_B1n6vY$m_o#vOZZUa{wc(%|ko^Aw$#n;UE>^6u{zZxxD<gafb{~?i6R*I^F4Bs5ncqgAfUyt@wt_4jwKeM$#$C5zWbe!~bDPYkZiE zW!_IHPEvf#WLDPB9)Dk2T!xw)&{N6+`}+cWk~Utd*d!@5zfrMT(g`5~9%~AHRn5?!WxN2`zyX{eEi1oNFl2f@04`l;CZg~UtMFfR~kQhjGN7tn@*!)K5oKi z7|%SZ-}p8}p_ zE>YZ9^2OenBO?Y|XTNzKrg(%+%HJ1;v0s;zZn*hfB&+LtW8H}@9v zLzB|$KxO^qx93+YUQ+Zi|3t%_)7DS$__p!;S`L=%#KjbpvvHamCDj+>rdTXAY1?YQ8Fz%FQ^JN^X?%y9EH{wfwpK~rM8ByK zDR4S!9Gw47Np%freA0BhT=FT6(Wwf+Af76GPCvCo!1Ha)xammUd8v)ROi~oQsKHJy zKmI)y{vNO4LGMxNy@a$Ok792#lzL3r+#I%xt-bEnJPgIuOp5)0q&v435S zE*KL>rN-eBvsCg6Tz9x@8rI>4#~?WCv8J!j=|;r~)(1YKHWB(#`Ogx zlAG<1f-L4B%AG9v7R)=T6}C9nlba_|j5NRvkd3ESJ5_Qd#z@Pp4<{mZVw3|+&)#t*_eTu0`*LGn!++9~Tv9dfbpke1e38=Fg%StPk+DD@dce%r*>7f5~$LpHfl z@@uBLROpU4Ci#7zUmzx>`4(J9US~S$J`WD0K{ep*k~gAB@LEF?mG9j!)|#S?l(D*j z|CXAztHSRq8~E;!cJJA?_mSbWRdQWhk{9hnejNE_lIyx-Ojpt!2NcI!VtpJiE)c$P z!=_*k=6N;!F|7_tr|zV&hW$^!W(~#P=ARJ;z0aw;F%6PYC$moRcp97doA}!5)^=P! zJ9s#>3t}U=BX|c zDHEq8GV)63vLK&2tZQSs(iQ1(NL;N$L-(Ri>rOU?rnmKt=k4bjgN*QW^>bP0l+XkK_VXR( literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_gif_white_36dp.png b/res/drawable-xhdpi/ic_gif_white_36dp.png new file mode 100644 index 0000000000000000000000000000000000000000..acdb6d0b90461fae61ee213450f04e62b4a1cdb4 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw=6Jd|hGg7(d(E5A!9c_zPrS%TmEJ{NOhDBP3@;{nu|*z9T{5}Ie$~CQ-PJLfj%(`^ zGK*MdI$W5dtf7&9Aw&ORPN3tx&6k>*4!1OIxxeW@>q_5gQ4_8`e#752Q=!RD=F64n zvOflW`5e3UmSnK2>aV-ycyhn|FZ+zYPh)50X3bRO09!R@?bI*%@y1gwJAt^Ku6{1- HoD!M<$B+uf mNfp-s literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/poweredby_giphy.png b/res/drawable-xhdpi/poweredby_giphy.png new file mode 100644 index 0000000000000000000000000000000000000000..09b38f190f610b91bcc2a74f006b7218b708c9ed GIT binary patch literal 5567 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 N002ovPDHLkV1g;o`uYF> literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_dashboard_white_24dp.png b/res/drawable-xxhdpi/ic_dashboard_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..471aa0db16efabb7bf066845a16dc9ee93c7de65 GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJd1{_6jv*C{ z$qN)${QtlI7kD`1B2~fxeq}~|Eoaec)I$ztaD0e F0stJcA>RN1 literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_gif_white_36dp.png b/res/drawable-xxhdpi/ic_gif_white_36dp.png new file mode 100644 index 0000000000000000000000000000000000000000..ae117123cf458a9ecfc6db60201356c3c7ce92fd GIT binary patch literal 312 zcmeAS@N?(olHy`uVBq!ia0vp^IUvlz0wh)Q=eq%^|DG<6Ar-gY-f;A5b`WWJXzBEr zvx=wJ@S)JUtI|7GdLLeS%OuaHDg5G&;*T+x*{l>&x$5LkCvyC65{vT!>IVazkUK}c zudF?uZFBVM#d)QV=d9ZJ?MC6&Yq81hYai+Q{qFkRTpOCaH*d{e_j$kOB6J1&n6bd;2Fu|&A#ORwo7wvB zd$q1jnm_TYWlh4YQ=PYOl=SV8|7Y~b59aPa+WXbLU+JoSK79}*`{} Bg_!^V literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/ic_view_stream_white_24dp.png b/res/drawable-xxhdpi/ic_view_stream_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..b8656197f4b776c283d019de5abd994c150b9006 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJdE%Zfjv*C{ p$r4K%{@Zi>k+xcs_+s%w4u*aCHx(}hrAUA@db;|#taD0e0s!WQ8B+iN literal 0 HcmV?d00001 diff --git a/res/drawable-xxhdpi/poweredby_giphy.png b/res/drawable-xxhdpi/poweredby_giphy.png new file mode 100644 index 0000000000000000000000000000000000000000..aca5bf661457502c351994b782ed094e53c55ced GIT binary patch literal 8825 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 k+DxTOsimjaUTboFyt=akR{09!e9&j0`b literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/ic_view_stream_white_24dp.png b/res/drawable-xxxhdpi/ic_view_stream_white_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..1e679697e97cadf58f3f39e6954399712d6abcca GIT binary patch literal 98 zcmeAS@N?(olHy`uVBq!ia0vp^2_Vb}Bp6OT_L>T$m`Z~Df*BafCZDwc@}xao978G? tlNlNVKge_037nOD%JXZ39PjHu28K6JR!@-c{5J`t+0)g}Wt~$(697u88xa5i literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/poweredby_giphy.png b/res/drawable-xxxhdpi/poweredby_giphy.png new file mode 100644 index 0000000000000000000000000000000000000000..f266ba6527424708e48110268e744f8e5e8f9297 GIT binary patch literal 6626 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 V(TeM7{#U!fgMqbGzbM;;{SR_fy8{3K literal 0 HcmV?d00001 diff --git a/res/layout/attachment_type_selector.xml b/res/layout/attachment_type_selector.xml index 9b912d01226..58723e99601 100644 --- a/res/layout/attachment_type_selector.xml +++ b/res/layout/attachment_type_selector.xml @@ -169,6 +169,21 @@ android:gravity="center" android:orientation="vertical"> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/giphy_activity_toolbar.xml b/res/layout/giphy_activity_toolbar.xml new file mode 100644 index 00000000000..1e5d1de533a --- /dev/null +++ b/res/layout/giphy_activity_toolbar.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/giphy_fragment.xml b/res/layout/giphy_fragment.xml new file mode 100644 index 00000000000..9c364a25ada --- /dev/null +++ b/res/layout/giphy_fragment.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/giphy_thumbnail.xml b/res/layout/giphy_thumbnail.xml new file mode 100644 index 00000000000..eed7e09e1ad --- /dev/null +++ b/res/layout/giphy_thumbnail.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index d25ae0b6fd0..995f4eb1be4 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -269,6 +269,14 @@ Permanent Signal communication failure! Signal was unable to register with Google Play Services. Signal messages and calls have been disabled, please try re-registering in Settings > Advanced. + + + Error while retrieving full resolution GiF... + + + GIFs + Stickers + New group Update group @@ -751,6 +759,13 @@ %dw + + Search GIFs and stickers + + + No results found. + + Could not read the log on your device. You can still use ADB to get a debug log instead. Thanks for your help! diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 1a8e487d253..3bb9dcba675 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -92,14 +92,11 @@ import org.thoughtcrime.securesms.database.DraftDatabase.Draft; import org.thoughtcrime.securesms.database.DraftDatabase.Drafts; import org.thoughtcrime.securesms.database.GroupDatabase; -import org.thoughtcrime.securesms.database.MessagingDatabase; import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo; -import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId; import org.thoughtcrime.securesms.database.MmsSmsColumns.Types; import org.thoughtcrime.securesms.database.RecipientPreferenceDatabase.RecipientsPreferences; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; -import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob; import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter; @@ -187,6 +184,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private static final int TAKE_PHOTO = 6; private static final int ADD_CONTACT = 7; private static final int PICK_LOCATION = 8; + private static final int PICK_GIF = 9; private MasterSecret masterSecret; protected ComposeText composeText; @@ -371,6 +369,9 @@ public void onActivityResult(final int reqCode, int resultCode, Intent data) { SignalPlace place = new SignalPlace(PlacePicker.getPlace(data, this)); attachmentManager.setLocation(masterSecret, place, getCurrentMediaConstraints()); break; + case PICK_GIF: + setMedia(data.getData(), MediaType.GIF); + break; } } @@ -1118,6 +1119,8 @@ private void addAttachment(int type) { AttachmentManager.selectLocation(this, PICK_LOCATION); break; case AttachmentTypeSelectorAdapter.TAKE_PHOTO: attachmentManager.capturePhoto(this, TAKE_PHOTO); break; + case AttachmentTypeSelector.ADD_GIF: + AttachmentManager.selectGif(this, PICK_GIF); break; } } diff --git a/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java b/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java index 0898f51e623..1d6c017c9ce 100644 --- a/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java +++ b/src/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java @@ -34,6 +34,7 @@ public class AttachmentTypeSelector extends PopupWindow { public static final int ADD_CONTACT_INFO = 4; public static final int TAKE_PHOTO = 5; public static final int ADD_LOCATION = 6; + public static final int ADD_GIF = 7; private static final int ANIMATION_DURATION = 300; @@ -45,6 +46,7 @@ public class AttachmentTypeSelector extends PopupWindow { private final @NonNull ImageView contactButton; private final @NonNull ImageView cameraButton; private final @NonNull ImageView locationButton; + private final @NonNull ImageView gifButton; private final @NonNull ImageView closeButton; private @Nullable View currentAnchor; @@ -62,8 +64,9 @@ public AttachmentTypeSelector(@NonNull Context context, @Nullable AttachmentClic this.videoButton = ViewUtil.findById(layout, R.id.video_button); this.contactButton = ViewUtil.findById(layout, R.id.contact_button); this.cameraButton = ViewUtil.findById(layout, R.id.camera_button); - this.closeButton = ViewUtil.findById(layout, R.id.close_button); this.locationButton = ViewUtil.findById(layout, R.id.location_button); + this.gifButton = ViewUtil.findById(layout, R.id.giphy_button); + this.closeButton = ViewUtil.findById(layout, R.id.close_button); this.imageButton.setOnClickListener(new PropagatingClickListener(ADD_IMAGE)); this.audioButton.setOnClickListener(new PropagatingClickListener(ADD_SOUND)); @@ -71,6 +74,7 @@ public AttachmentTypeSelector(@NonNull Context context, @Nullable AttachmentClic this.contactButton.setOnClickListener(new PropagatingClickListener(ADD_CONTACT_INFO)); this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO)); this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION)); + this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF)); this.closeButton.setOnClickListener(new CloseClickListener()); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { @@ -112,6 +116,7 @@ public void onGlobalLayout() { animateButtonIn(audioButton, ANIMATION_DURATION / 3); animateButtonIn(locationButton, ANIMATION_DURATION / 3); animateButtonIn(videoButton, ANIMATION_DURATION / 4); + animateButtonIn(gifButton, ANIMATION_DURATION / 4); animateButtonIn(contactButton, 0); animateButtonIn(closeButton, 0); } diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index e75a8b8d34d..29799b548e6 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -18,6 +18,7 @@ import com.bumptech.glide.DrawableRequestBuilder; import com.bumptech.glide.GenericRequestBuilder; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; @@ -127,7 +128,9 @@ public void setImageResource(@NonNull MasterSecret masterSecret, @NonNull Slide public void setImageResource(@NonNull MasterSecret masterSecret, @NonNull Uri uri) { if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); - Glide.with(getContext()).load(new DecryptableUri(masterSecret, uri)) + Glide.with(getContext()) + .load(new DecryptableUri(masterSecret, uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) .crossFade() .transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint)) .into(image); @@ -161,18 +164,22 @@ private boolean isContextValid() { private GenericRequestBuilder buildThumbnailGlideRequest(@NonNull Slide slide, @NonNull MasterSecret masterSecret) { @SuppressWarnings("ConstantConditions") - DrawableRequestBuilder builder = Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri())) - .crossFade() - .transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint)); + DrawableRequestBuilder builder = Glide.with(getContext()) + .load(new DecryptableUri(masterSecret, slide.getThumbnailUri())) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .crossFade() + .transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint)); if (slide.isInProgress()) return builder; else return builder.error(R.drawable.ic_missing_thumbnail_picture); } private GenericRequestBuilder buildPlaceholderGlideRequest(Slide slide) { - return Glide.with(getContext()).load(slide.getPlaceholderRes(getContext().getTheme())) - .asBitmap() - .fitCenter(); + return Glide.with(getContext()) + .load(slide.getPlaceholderRes(getContext().getTheme())) + .asBitmap() + .diskCacheStrategy(DiskCacheStrategy.NONE) + .fitCenter(); } private class ThumbnailClickDispatcher implements View.OnClickListener { diff --git a/src/org/thoughtcrime/securesms/components/ZoomingImageView.java b/src/org/thoughtcrime/securesms/components/ZoomingImageView.java index 1caf6d4bf34..03947b42e0b 100644 --- a/src/org/thoughtcrime/securesms/components/ZoomingImageView.java +++ b/src/org/thoughtcrime/securesms/components/ZoomingImageView.java @@ -7,6 +7,7 @@ import android.widget.ImageView; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.drawable.GlideDrawable; import com.bumptech.glide.request.target.BitmapImageViewTarget; import com.bumptech.glide.request.target.GlideDrawableImageViewTarget; @@ -34,6 +35,7 @@ public ZoomingImageView(Context context, AttributeSet attrs, int defStyleAttr) { public void setImageUri(MasterSecret masterSecret, Uri uri) { Glide.with(getContext()) .load(new DecryptableUri(masterSecret, uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) .dontTransform() .dontAnimate() .into(new GlideDrawableImageViewTarget(this) { diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java index 833fa4ed0f6..ccb54d433e4 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java @@ -10,6 +10,7 @@ import android.text.TextUtils; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.mms.ContactPhotoUriLoader.ContactPhotoUri; @@ -52,6 +53,7 @@ public static ContactPhoto getContactPhoto(@NonNull Context context, try { Bitmap bitmap = Glide.with(context) .load(new ContactPhotoUri(uri)).asBitmap() + .diskCacheStrategy(DiskCacheStrategy.NONE) .centerCrop().into(targetSize, targetSize).get(); return new BitmapContactPhoto(bitmap); } catch (ExecutionException e) { diff --git a/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java b/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java new file mode 100644 index 00000000000..b804e975044 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.giph.model; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GiphyImage { + + @JsonProperty + private ImageTypes images; + + public String getGifUrl() { + return images.downsized_medium.url; + } + + public float getGifAspectRatio() { + return (float)images.downsized_medium.width / (float)images.downsized_medium.height; + } + + public String getStillUrl() { + return images.fixed_width_still.url; + } + + public static class ImageTypes { + @JsonProperty + private ImageData fixed_height; + @JsonProperty + private ImageData fixed_height_still; + @JsonProperty + private ImageData fixed_height_downsampled; + @JsonProperty + private ImageData fixed_width; + @JsonProperty + private ImageData fixed_width_still; + @JsonProperty + private ImageData fixed_width_downsampled; + @JsonProperty + private ImageData fixed_width_small; + @JsonProperty + private ImageData downsized_medium; + } + + public static class ImageData { + @JsonProperty + private String url; + + @JsonProperty + private int width; + + @JsonProperty + private int height; + + @JsonProperty + private int size; + + @JsonProperty + private String mp4; + + @JsonProperty + private String webp; + } + +} diff --git a/src/org/thoughtcrime/securesms/giph/model/GiphyResponse.java b/src/org/thoughtcrime/securesms/giph/model/GiphyResponse.java new file mode 100644 index 00000000000..4ab61b57155 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/model/GiphyResponse.java @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms.giph.model; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class GiphyResponse { + + @JsonProperty + private List data; + + public List getData() { + return data; + } + +} diff --git a/src/org/thoughtcrime/securesms/giph/net/GiphyGifLoader.java b/src/org/thoughtcrime/securesms/giph/net/GiphyGifLoader.java new file mode 100644 index 00000000000..e832d23bf49 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/net/GiphyGifLoader.java @@ -0,0 +1,23 @@ +package org.thoughtcrime.securesms.giph.net; + + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public class GiphyGifLoader extends GiphyLoader { + + public GiphyGifLoader(@NonNull Context context, @Nullable String searchString) { + super(context, searchString); + } + + @Override + protected String getTrendingUrl() { + return "https://api.giphy.com/v1/gifs/trending?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE; + } + + @Override + protected String getSearchUrl() { + return "https://api.giphy.com/v1/gifs/search?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE + "&q=%s"; + } +} diff --git a/src/org/thoughtcrime/securesms/giph/net/GiphyLoader.java b/src/org/thoughtcrime/securesms/giph/net/GiphyLoader.java new file mode 100644 index 00000000000..3f6d02fe156 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/net/GiphyLoader.java @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.giph.net; + + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.util.Log; + +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.giph.model.GiphyResponse; +import org.thoughtcrime.securesms.util.AsyncLoader; +import org.thoughtcrime.securesms.util.JsonUtils; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +public abstract class GiphyLoader extends AsyncLoader> { + + private static final String TAG = GiphyLoader.class.getName(); + + public static int PAGE_SIZE = 100; + + @Nullable private String searchString; + + private final OkHttpClient client = new OkHttpClient(); + + protected GiphyLoader(@NonNull Context context, @Nullable String searchString) { + super(context); + this.searchString = searchString; + this.client.setProxySelector(new GiphyProxySelector()); + } + + @Override + public List loadInBackground() { + return loadPage(0); + } + + public List loadPage(int offset) { + try { + String url; + + if (TextUtils.isEmpty(searchString)) url = String.format(getTrendingUrl(), offset); + else url = String.format(getSearchUrl(), offset, Uri.encode(searchString)); + + Request request = new Request.Builder().url(url).build(); + Response response = client.newCall(request).execute(); + + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + + GiphyResponse giphyResponse = JsonUtils.fromJson(response.body().byteStream(), GiphyResponse.class); + + return giphyResponse.getData(); + } catch (IOException e) { + Log.w(TAG, e); + return new LinkedList<>(); + } + } + + protected abstract String getTrendingUrl(); + protected abstract String getSearchUrl(); +} diff --git a/src/org/thoughtcrime/securesms/giph/net/GiphyProxySelector.java b/src/org/thoughtcrime/securesms/giph/net/GiphyProxySelector.java new file mode 100644 index 00000000000..e7c64bd487d --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/net/GiphyProxySelector.java @@ -0,0 +1,73 @@ +package org.thoughtcrime.securesms.giph.net; + + +import android.os.AsyncTask; +import android.util.Log; + +import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.util.Util; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +public class GiphyProxySelector extends ProxySelector { + + private static final String TAG = GiphyProxySelector.class.getSimpleName(); + + private final List EMPTY = new ArrayList<>(1); + private volatile List GIPHY = null; + + public GiphyProxySelector() { + EMPTY.add(Proxy.NO_PROXY); + + if (Util.isMainThread()) { + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + synchronized (GiphyProxySelector.this) { + initializeGiphyProxy(); + GiphyProxySelector.this.notifyAll(); + } + return null; + } + }.execute(); + } else { + initializeGiphyProxy(); + } + } + + @Override + public List select(URI uri) { + if (uri.getHost().endsWith("giphy.com")) return getOrCreateGiphyProxy(); + else return EMPTY; + } + + @Override + public void connectFailed(URI uri, SocketAddress address, IOException failure) { + Log.w(TAG, failure); + } + + private void initializeGiphyProxy() { + GIPHY = new ArrayList(1) {{ + add(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(BuildConfig.GIPHY_PROXY_HOST, + BuildConfig.GIPHY_PROXY_PORT))); + }}; + } + + private List getOrCreateGiphyProxy() { + if (GIPHY == null) { + synchronized (this) { + while (GIPHY == null) Util.wait(this, 0); + } + } + + return GIPHY; + } + +} diff --git a/src/org/thoughtcrime/securesms/giph/net/GiphyStickerLoader.java b/src/org/thoughtcrime/securesms/giph/net/GiphyStickerLoader.java new file mode 100644 index 00000000000..9290fc2dabe --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/net/GiphyStickerLoader.java @@ -0,0 +1,23 @@ +package org.thoughtcrime.securesms.giph.net; + + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public class GiphyStickerLoader extends GiphyLoader { + + public GiphyStickerLoader(@NonNull Context context, @Nullable String searchString) { + super(context, searchString); + } + + @Override + protected String getTrendingUrl() { + return "https://api.giphy.com/v1/stickers/trending?api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE; + } + + @Override + protected String getSearchUrl() { + return "https://api.giphy.com/v1/stickers/search?q=cat&api_key=3o6ZsYH6U6Eri53TXy&offset=%d&limit=" + PAGE_SIZE + "&q=%s"; + } +} diff --git a/src/org/thoughtcrime/securesms/giph/ui/AspectRatioImageView.java b/src/org/thoughtcrime/securesms/giph/ui/AspectRatioImageView.java new file mode 100644 index 00000000000..a1ce36e23d0 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/ui/AspectRatioImageView.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2015 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.thoughtcrime.securesms.giph.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + + +/** + * AspectRatioImageView maintains an aspect ratio by adjusting the width or height dimension. The + * aspect ratio (width to height ratio) and adjustment dimension can be configured. + */ +public class AspectRatioImageView extends ImageView { + + private static final float DEFAULT_ASPECT_RATIO = 1.0f; + private static final int DEFAULT_ADJUST_DIMENSION = 0; + // defined by attrs.xml enum + static final int ADJUST_DIMENSION_HEIGHT = 0; + static final int ADJUST_DIMENSION_WIDTH = 1; + + private double aspectRatio; // width to height ratio + private int dimensionToAdjust; // ADJUST_DIMENSION_HEIGHT or ADJUST_DIMENSION_WIDTH + + public AspectRatioImageView(Context context) { + this(context, null); + } + + public AspectRatioImageView(Context context, AttributeSet attrs) { + super(context, attrs); +// final TypedArray a = context.obtainStyledAttributes(attrs, +// R.styleable.tw__AspectRatioImageView); +// try { +// aspectRatio = a.getFloat(R.styleable.tw__AspectRatioImageView_tw__image_aspect_ratio, +// DEFAULT_ASPECT_RATIO); +// dimensionToAdjust +// = a.getInt(R.styleable.tw__AspectRatioImageView_tw__image_dimension_to_adjust, +// DEFAULT_ADJUST_DIMENSION); +// } finally { +// a.recycle(); +// } + } + + public double getAspectRatio() { + return aspectRatio; + } + + public int getDimensionToAdjust() { + return dimensionToAdjust; + } + + /** + * Sets the aspect ratio that should be respected during measurement. + * + * @param aspectRatio desired width to height ratio + */ + public void setAspectRatio(final double aspectRatio) { + this.aspectRatio = aspectRatio; + } + + /** + * Resets the size to 0. + */ + public void resetSize() { + if (getMeasuredWidth() == 0 && getMeasuredHeight() == 0) { + return; + } + measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY)); + layout(0, 0, 0, 0); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + if (dimensionToAdjust == ADJUST_DIMENSION_HEIGHT) { + height = calculateHeight(width, aspectRatio); + } else { + width = calculateWidth(height, aspectRatio); + } + setMeasuredDimension(width, height); + } + + /** + * Returns the height that will satisfy the width to height aspect ratio, keeping the given + * width fixed. + */ + int calculateHeight(int width, double ratio) { + if (ratio == 0) { + return 0; + } + return (int) Math.round(width / ratio); + } + + /** + * Returns the width that will satisfy the width to height aspect ratio, keeping the given + * height fixed. + */ + int calculateWidth(int height, double ratio) { + return (int) Math.round(height * ratio); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java new file mode 100644 index 00000000000..11ae145fb71 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -0,0 +1,163 @@ +package org.thoughtcrime.securesms.giph.ui; + + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.util.DynamicLanguage; +import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; +import org.thoughtcrime.securesms.util.DynamicTheme; +import org.thoughtcrime.securesms.util.ViewUtil; + +import java.util.concurrent.ExecutionException; + +public class GiphyActivity extends PassphraseRequiredActionBarActivity + implements GiphyActivityToolbar.OnLayoutChangedListener, + GiphyActivityToolbar.OnFilterChangedListener, + GiphyAdapter.OnItemClickListener +{ + + private static final String TAG = GiphyActivity.class.getSimpleName(); + + private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); + private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); + + private GiphyGifFragment gifFragment; + private GiphyStickerFragment stickerFragment; + + private GiphyAdapter.GiphyViewHolder finishingImage; + + @Override + public void onPreCreate() { + dynamicTheme.onCreate(this); + dynamicLanguage.onCreate(this); + } + + @Override + public void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) { + setContentView(R.layout.giphy_activity); + + initializeToolbar(); + initializeResources(); + } + + private void initializeToolbar() { + GiphyActivityToolbar toolbar = ViewUtil.findById(this, R.id.giphy_toolbar); + toolbar.setOnFilterChangedListener(this); + toolbar.setOnLayoutChangedListener(this); + + setSupportActionBar(toolbar); + + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + getSupportActionBar().setDisplayShowTitleEnabled(false); + } + + private void initializeResources() { + ViewPager viewPager = ViewUtil.findById(this, R.id.giphy_pager); + TabLayout tabLayout = ViewUtil.findById(this, R.id.tab_layout); + + this.gifFragment = new GiphyGifFragment(); + this.stickerFragment = new GiphyStickerFragment(); + + gifFragment.setClickListener(this); + stickerFragment.setClickListener(this); + + viewPager.setAdapter(new GiphyFragmentPagerAdapter(this, getSupportFragmentManager(), + gifFragment, stickerFragment)); + tabLayout.setupWithViewPager(viewPager); + } + + @Override + public void onFilterChanged(String filter) { + this.gifFragment.setSearchString(filter); + this.stickerFragment.setSearchString(filter); + } + + @Override + public void onLayoutChanged(int type) { + this.gifFragment.setLayoutManager(type); + this.stickerFragment.setLayoutManager(type); + } + + @Override + public void onClick(final GiphyAdapter.GiphyViewHolder viewHolder) { + if (finishingImage != null) finishingImage.gifProgress.setVisibility(View.GONE); + finishingImage = viewHolder; + finishingImage.gifProgress.setVisibility(View.VISIBLE); + + new AsyncTask() { + @Override + protected Uri doInBackground(Void... params) { + try { + return Uri.fromFile(viewHolder.getFile()); + } catch (InterruptedException | ExecutionException e) { + Log.w(TAG, e); + return null; + } + } + + protected void onPostExecute(@Nullable Uri uri) { + if (uri == null) { + Toast.makeText(GiphyActivity.this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show(); + } else if (viewHolder == finishingImage) { + setResult(RESULT_OK, new Intent().setData(uri)); + finish(); + } else { + Log.w(TAG, "Resolved Uri is no longer the selected element..."); + } + } + }.execute(); + } + + private static class GiphyFragmentPagerAdapter extends FragmentPagerAdapter { + + private final Context context; + private final GiphyGifFragment gifFragment; + private final GiphyStickerFragment stickerFragment; + + private GiphyFragmentPagerAdapter(@NonNull Context context, + @NonNull FragmentManager fragmentManager, + @NonNull GiphyGifFragment gifFragment, + @NonNull GiphyStickerFragment stickerFragment) + { + super(fragmentManager); + this.context = context.getApplicationContext(); + this.gifFragment = gifFragment; + this.stickerFragment = stickerFragment; + } + + @Override + public Fragment getItem(int position) { + if (position == 0) return gifFragment; + else return stickerFragment; + } + + @Override + public int getCount() { + return 2; + } + + @Override + public CharSequence getPageTitle(int position) { + if (position == 0) return context.getString(R.string.GiphyFragmentPagerAdapter_gifs); + else return context.getString(R.string.GiphyFragmentPagerAdapter_stickers); + } + } + +} diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyActivityToolbar.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyActivityToolbar.java new file mode 100644 index 00000000000..c940937b689 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyActivityToolbar.java @@ -0,0 +1,174 @@ +package org.thoughtcrime.securesms.giph.ui; + + +import android.content.Context; +import android.graphics.Rect; +import android.support.annotation.Nullable; +import android.support.v7.widget.Toolbar; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.TouchDelegate; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.AnimatingToggle; +import org.thoughtcrime.securesms.util.ViewUtil; + +public class GiphyActivityToolbar extends Toolbar { + + @Nullable private OnFilterChangedListener filterListener; + @Nullable private OnLayoutChangedListener layoutListener; + + private EditText searchText; + private AnimatingToggle toggle; + private ImageView action; + private ImageView listToggle; + private ImageView gridToggle; + private ImageView clearToggle; + private LinearLayout toggleContainer; + + public GiphyActivityToolbar(Context context) { + this(context, null); + } + + public GiphyActivityToolbar(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.toolbarStyle); + } + + public GiphyActivityToolbar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + inflate(context, R.layout.giphy_activity_toolbar, this); + + this.action = ViewUtil.findById(this, R.id.action_icon); + this.searchText = ViewUtil.findById(this, R.id.search_view); + this.toggle = ViewUtil.findById(this, R.id.button_toggle); + this.listToggle = ViewUtil.findById(this, R.id.view_stream); + this.gridToggle = ViewUtil.findById(this, R.id.view_grid); + this.clearToggle = ViewUtil.findById(this, R.id.search_clear); + this.toggleContainer = ViewUtil.findById(this, R.id.toggle_container); + + this.listToggle.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + displayTogglingView(gridToggle); + if (layoutListener != null) layoutListener.onLayoutChanged(OnLayoutChangedListener.LAYOUT_LIST); + } + }); + + this.gridToggle.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + displayTogglingView(listToggle); + if (layoutListener != null) layoutListener.onLayoutChanged(OnLayoutChangedListener.LAYOUT_GRID); + } + }); + + this.clearToggle.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + searchText.setText(""); + clearToggle.setVisibility(View.INVISIBLE); + } + }); + + this.searchText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + if (SearchUtil.isEmpty(searchText)) clearToggle.setVisibility(View.INVISIBLE); + else clearToggle.setVisibility(View.VISIBLE); + + notifyListener(); + } + }); + + expandTapArea(this, action); + expandTapArea(toggleContainer, gridToggle); + } + + @Override + public void setNavigationIcon(int resId) { + action.setImageResource(resId); + } + + public void clear() { + searchText.setText(""); + notifyListener(); + } + + public void setOnLayoutChangedListener(@Nullable OnLayoutChangedListener layoutListener) { + this.layoutListener = layoutListener; + } + + public void setOnFilterChangedListener(@Nullable OnFilterChangedListener filterListener) { + this.filterListener = filterListener; + } + + private void notifyListener() { + if (filterListener != null) filterListener.onFilterChanged(searchText.getText().toString()); + } + + private void displayTogglingView(View view) { + toggle.display(view); + expandTapArea(toggleContainer, view); + } + + private void expandTapArea(final View container, final View child) { + final int padding = getResources().getDimensionPixelSize(R.dimen.contact_selection_actions_tap_area); + + container.post(new Runnable() { + @Override + public void run() { + Rect rect = new Rect(); + child.getHitRect(rect); + + rect.top -= padding; + rect.left -= padding; + rect.right += padding; + rect.bottom += padding; + + container.setTouchDelegate(new TouchDelegate(rect, child)); + } + }); + } + + private static class SearchUtil { + public static boolean isTextInput(EditText editText) { + return (editText.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT; + } + + public static boolean isPhoneInput(EditText editText) { + return (editText.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_PHONE; + } + + public static boolean isEmpty(EditText editText) { + return editText.getText().length() <= 0; + } + } + + public interface OnFilterChangedListener { + void onFilterChanged(String filter); + } + + public interface OnLayoutChangedListener { + public static final int LAYOUT_GRID = 1; + public static final int LAYOUT_LIST = 2; + void onLayoutChanged(int type); + } + + +} diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java new file mode 100644 index 00000000000..56cc21d2880 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java @@ -0,0 +1,155 @@ +package org.thoughtcrime.securesms.giph.ui; + + +import android.content.Context; +import android.graphics.drawable.ColorDrawable; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; + +import com.bumptech.glide.DrawableRequestBuilder; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.color.MaterialColor; +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.ViewUtil; + +import java.io.File; +import java.util.List; +import java.util.concurrent.ExecutionException; + + +public class GiphyAdapter extends RecyclerView.Adapter { + + private static final String TAG = GiphyAdapter.class.getSimpleName(); + + private List images; + private Context context; + private OnItemClickListener listener; + + class GiphyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, RequestListener { + + public AspectRatioImageView thumbnail; + public GiphyImage image; + public ProgressBar gifProgress; + public volatile boolean modelReady; + + GiphyViewHolder(View view) { + super(view); + thumbnail = ViewUtil.findById(view, R.id.thumbnail); + gifProgress = ViewUtil.findById(view, R.id.gif_progress); + thumbnail.setOnClickListener(this); + gifProgress.setVisibility(View.GONE); + } + + @Override + public void onClick(View v) { + if (listener != null) listener.onClick(this); + } + + @Override + public boolean onException(Exception e, String model, Target target, boolean isFirstResource) { + Log.w(TAG, e); + + synchronized (this) { + if (image.getGifUrl().equals(model)) { + this.modelReady = true; + notifyAll(); + } + } + + return false; + } + + @Override + public boolean onResourceReady(GlideDrawable resource, String model, Target target, boolean isFromMemoryCache, boolean isFirstResource) { + synchronized (this) { + if (image.getGifUrl().equals(model)) { + this.modelReady = true; + notifyAll(); + } + } + + return false; + } + + public File getFile() throws ExecutionException, InterruptedException { + synchronized (this) { + while (!modelReady) { + Util.wait(this, 0); + } + } + + return Glide.with(context) + .load(image.getGifUrl()) + .downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .get(); + } + } + + GiphyAdapter(Context context, List images) { + this.context = context; + this.images = images; + } + + public void setImages(List images) { + this.images = images; + notifyDataSetChanged(); + } + + public void addImages(List images) { + this.images.addAll(images); + notifyDataSetChanged(); + } + + @Override + public GiphyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.giphy_thumbnail, parent, false); + + return new GiphyViewHolder(itemView); + } + + @Override + public void onBindViewHolder(GiphyViewHolder holder, int position) { + GiphyImage image = images.get(position); + + holder.modelReady = false; + holder.image = image; + holder.thumbnail.setAspectRatio(image.getGifAspectRatio()); + holder.gifProgress.setVisibility(View.GONE); + + DrawableRequestBuilder thumbnailRequest = Glide.with(context) + .load(image.getStillUrl()); + + Glide.with(context) + .load(image.getGifUrl()) + .thumbnail(thumbnailRequest) + .placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context))) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .listener(holder) + .into(holder.thumbnail); + } + + @Override + public int getItemCount() { + return images.size(); + } + + public void setListener(OnItemClickListener listener) { + this.listener = listener; + } + + public interface OnItemClickListener { + void onClick(GiphyViewHolder viewHolder); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java new file mode 100644 index 00000000000..d90065ced01 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java @@ -0,0 +1,122 @@ +package org.thoughtcrime.securesms.giph.ui; + +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.support.v7.widget.DefaultItemAnimator; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.giph.net.GiphyLoader; +import org.thoughtcrime.securesms.giph.util.InfiniteScrollListener; +import org.thoughtcrime.securesms.util.ViewUtil; + +import java.util.LinkedList; +import java.util.List; + +public abstract class GiphyFragment extends Fragment implements LoaderManager.LoaderCallbacks>, GiphyAdapter.OnItemClickListener { + + private static final String TAG = GiphyFragment.class.getSimpleName(); + + private GiphyAdapter giphyAdapter; + private RecyclerView recyclerView; + private ProgressBar loadingProgress; + private TextView noResultsView; + private GiphyAdapter.OnItemClickListener listener; + + protected String searchString; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { + ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.giphy_fragment); + this.recyclerView = ViewUtil.findById(container, R.id.giphy_list); + this.loadingProgress = ViewUtil.findById(container, R.id.loading_progress); + this.noResultsView = ViewUtil.findById(container, R.id.no_results); + + return container; + } + + @Override + public void onActivityCreated(Bundle bundle) { + super.onActivityCreated(bundle); + + this.giphyAdapter = new GiphyAdapter(getActivity(), new LinkedList()); + this.giphyAdapter.setListener(this); + + this.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + this.recyclerView.setItemAnimator(new DefaultItemAnimator()); + this.recyclerView.setAdapter(giphyAdapter); + this.recyclerView.addOnScrollListener(new GiphyScrollListener()); + + getLoaderManager().initLoader(0, null, this); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + this.loadingProgress.setVisibility(View.GONE); + + if (data.isEmpty()) noResultsView.setVisibility(View.VISIBLE); + else noResultsView.setVisibility(View.GONE); + + this.giphyAdapter.setImages(data); + } + + @Override + public void onLoaderReset(Loader> loader) { + noResultsView.setVisibility(View.GONE); + this.giphyAdapter.setImages(new LinkedList()); + } + + public void setLayoutManager(int type) { + if (type == GiphyActivityToolbar.OnLayoutChangedListener.LAYOUT_GRID) { + this.recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)); + } else { + this.recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + } + } + + public void setClickListener(GiphyAdapter.OnItemClickListener listener) { + this.listener = listener; + } + + public void setSearchString(@Nullable String searchString) { + this.searchString = searchString; + this.noResultsView.setVisibility(View.GONE); + this.getLoaderManager().restartLoader(0, null, this); + } + + @Override + public void onClick(GiphyAdapter.GiphyViewHolder viewHolder) { + if (listener != null) listener.onClick(viewHolder); + } + + private class GiphyScrollListener extends InfiniteScrollListener { + @Override + public void onLoadMore(final int currentPage) { + final Loader> loader = getLoaderManager().getLoader(0); + if (loader == null) return; + + new AsyncTask>() { + @Override + protected List doInBackground(Void... params) { + return ((GiphyLoader)loader).loadPage(currentPage * GiphyLoader.PAGE_SIZE); + } + + protected void onPostExecute(List images) { + giphyAdapter.addImages(images); + } + }.execute(); + } + } +} diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java new file mode 100644 index 00000000000..ea6b8459726 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java @@ -0,0 +1,19 @@ +package org.thoughtcrime.securesms.giph.ui; + + +import android.os.Bundle; +import android.support.v4.content.Loader; + +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.giph.net.GiphyGifLoader; + +import java.util.List; + +public class GiphyGifFragment extends GiphyFragment { + + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new GiphyGifLoader(getActivity(), searchString); + } + +} diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java new file mode 100644 index 00000000000..27a03b326bb --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyStickerFragment.java @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms.giph.ui; + + +import android.os.Bundle; +import android.support.v4.content.Loader; + +import org.thoughtcrime.securesms.giph.model.GiphyImage; +import org.thoughtcrime.securesms.giph.net.GiphyStickerLoader; + +import java.util.List; + +public class GiphyStickerFragment extends GiphyFragment { + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new GiphyStickerLoader(getActivity(), searchString); + } +} diff --git a/src/org/thoughtcrime/securesms/giph/util/InfiniteScrollListener.java b/src/org/thoughtcrime/securesms/giph/util/InfiniteScrollListener.java new file mode 100644 index 00000000000..ba82f7e0da8 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/util/InfiniteScrollListener.java @@ -0,0 +1,48 @@ +// From https://gist.github.com/mipreamble/b6d4b3d65b0b4775a22e#file-recyclerviewpositionhelper-java + +package org.thoughtcrime.securesms.giph.util; + +import android.support.v7.widget.RecyclerView; + +public abstract class InfiniteScrollListener extends RecyclerView.OnScrollListener { + + public static String TAG = InfiniteScrollListener.class.getSimpleName(); + + private int previousTotal = 0; // The total number of items in the dataset after the last load + private boolean loading = true; // True if we are still waiting for the last set of data to load. + private int visibleThreshold = 5; // The minimum amount of items to have below your current scroll position before loading more. + + int firstVisibleItem, visibleItemCount, totalItemCount; + + private int currentPage = 1; + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + RecyclerViewPositionHelper recyclerViewPositionHelper = RecyclerViewPositionHelper.createHelper(recyclerView); + + visibleItemCount = recyclerView.getChildCount(); + totalItemCount = recyclerViewPositionHelper.getItemCount(); + firstVisibleItem = recyclerViewPositionHelper.findFirstVisibleItemPosition(); + + if (loading) { + if (totalItemCount > previousTotal) { + loading = false; + previousTotal = totalItemCount; + } + } + if (!loading && (totalItemCount - visibleItemCount) + <= (firstVisibleItem + visibleThreshold)) { + // End has been reached + // Do something + currentPage++; + + onLoadMore(currentPage); + + loading = true; + } + } + + public abstract void onLoadMore(int currentPage); +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/giph/util/RecyclerViewPositionHelper.java b/src/org/thoughtcrime/securesms/giph/util/RecyclerViewPositionHelper.java new file mode 100644 index 00000000000..e2a62ec17d3 --- /dev/null +++ b/src/org/thoughtcrime/securesms/giph/util/RecyclerViewPositionHelper.java @@ -0,0 +1,115 @@ +// From https://gist.github.com/mipreamble/b6d4b3d65b0b4775a22e#file-recyclerviewpositionhelper-java + +package org.thoughtcrime.securesms.giph.util; + + +import android.support.v7.widget.OrientationHelper; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class RecyclerViewPositionHelper { + + final RecyclerView recyclerView; + final RecyclerView.LayoutManager layoutManager; + + RecyclerViewPositionHelper(RecyclerView recyclerView) { + this.recyclerView = recyclerView; + this.layoutManager = recyclerView.getLayoutManager(); + } + + public static RecyclerViewPositionHelper createHelper(RecyclerView recyclerView) { + if (recyclerView == null) { + throw new NullPointerException("Recycler View is null"); + } + return new RecyclerViewPositionHelper(recyclerView); + } + + /** + * Returns the adapter item count. + * + * @return The total number on items in a layout manager + */ + public int getItemCount() { + return layoutManager == null ? 0 : layoutManager.getItemCount(); + } + + /** + * Returns the adapter position of the first visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + * + * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if + * there aren't any visible items. + */ + public int findFirstVisibleItemPosition() { + final View child = findOneVisibleChild(0, layoutManager.getChildCount(), false, true); + return child == null ? RecyclerView.NO_POSITION : recyclerView.getChildAdapterPosition(child); + } + + /** + * Returns the adapter position of the first fully visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + * + * @return The adapter position of the first fully visible item or + * {@link RecyclerView#NO_POSITION} if there aren't any visible items. + */ + public int findFirstCompletelyVisibleItemPosition() { + final View child = findOneVisibleChild(0, layoutManager.getChildCount(), true, false); + return child == null ? RecyclerView.NO_POSITION : recyclerView.getChildAdapterPosition(child); + } + + /** + * Returns the adapter position of the last visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + * + * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if + * there aren't any visible items + */ + public int findLastVisibleItemPosition() { + final View child = findOneVisibleChild(layoutManager.getChildCount() - 1, -1, false, true); + return child == null ? RecyclerView.NO_POSITION : recyclerView.getChildAdapterPosition(child); + } + + /** + * Returns the adapter position of the last fully visible view. This position does not include + * adapter changes that were dispatched after the last layout pass. + * + * @return The adapter position of the last fully visible view or + * {@link RecyclerView#NO_POSITION} if there aren't any visible items. + */ + public int findLastCompletelyVisibleItemPosition() { + final View child = findOneVisibleChild(layoutManager.getChildCount() - 1, -1, true, false); + return child == null ? RecyclerView.NO_POSITION : recyclerView.getChildAdapterPosition(child); + } + + View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, + boolean acceptPartiallyVisible) { + OrientationHelper helper; + if (layoutManager.canScrollVertically()) { + helper = OrientationHelper.createVerticalHelper(layoutManager); + } else { + helper = OrientationHelper.createHorizontalHelper(layoutManager); + } + + final int start = helper.getStartAfterPadding(); + final int end = helper.getEndAfterPadding(); + final int next = toIndex > fromIndex ? 1 : -1; + View partiallyVisible = null; + for (int i = fromIndex; i != toIndex; i += next) { + final View child = layoutManager.getChildAt(i); + final int childStart = helper.getDecoratedStart(child); + final int childEnd = helper.getDecoratedEnd(child); + if (childStart < end && childEnd > start) { + if (completelyVisible) { + if (childStart >= start && childEnd <= end) { + return child; + } else if (acceptPartiallyVisible && partiallyVisible == null) { + partiallyVisible = child; + } + } else { + return child; + } + } + } + return partiallyVisible; + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/glide/OkHttpStreamFetcher.java b/src/org/thoughtcrime/securesms/glide/OkHttpStreamFetcher.java new file mode 100644 index 00000000000..fd4fbae5d62 --- /dev/null +++ b/src/org/thoughtcrime/securesms/glide/OkHttpStreamFetcher.java @@ -0,0 +1,80 @@ +package org.thoughtcrime.securesms.glide; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.util.ContentLengthInputStream; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.ResponseBody; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +/** + * Fetches an {@link InputStream} using the okhttp library. + */ +public class OkHttpStreamFetcher implements DataFetcher { + private final OkHttpClient client; + private final GlideUrl url; + private InputStream stream; + private ResponseBody responseBody; + + public OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) { + this.client = client; + this.url = url; + } + + @Override + public InputStream loadData(Priority priority) throws Exception { + Request.Builder requestBuilder = new Request.Builder() + .url(url.toStringUrl()); + + for (Map.Entry headerEntry : url.getHeaders().entrySet()) { + String key = headerEntry.getKey(); + requestBuilder.addHeader(key, headerEntry.getValue()); + } + + Request request = requestBuilder.build(); + + Response response = client.newCall(request).execute(); + responseBody = response.body(); + if (!response.isSuccessful()) { + throw new IOException("Request failed with code: " + response.code()); + } + + long contentLength = responseBody.contentLength(); + stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength); + return stream; + } + + @Override + public void cleanup() { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + // Ignored + } + } + if (responseBody != null) { + try { + responseBody.close(); + } catch (IOException e) { + // Ignored. + } + } + } + + @Override + public String getId() { + return url.getCacheKey(); + } + + @Override + public void cancel() { + // TODO: call cancel on the client when this method is called on a background thread. See #257 + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java b/src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java new file mode 100644 index 00000000000..948b765f7b8 --- /dev/null +++ b/src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java @@ -0,0 +1,75 @@ +package org.thoughtcrime.securesms.glide; + +import android.content.Context; + +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.squareup.okhttp.OkHttpClient; + +import org.thoughtcrime.securesms.giph.net.GiphyProxySelector; + +import java.io.InputStream; + +/** + * A simple model loader for fetching media over http/https using OkHttp. + */ +public class OkHttpUrlLoader implements ModelLoader { + + /** + * The default factory for {@link OkHttpUrlLoader}s. + */ + public static class Factory implements ModelLoaderFactory { + private static volatile OkHttpClient internalClient; + private OkHttpClient client; + + private static OkHttpClient getInternalClient() { + if (internalClient == null) { + synchronized (Factory.class) { + if (internalClient == null) { + internalClient = new OkHttpClient(); + internalClient.setProxySelector(new GiphyProxySelector()); + } + } + } + return internalClient; + } + + /** + * Constructor for a new Factory that runs requests using a static singleton client. + */ + public Factory() { + this(getInternalClient()); + } + + /** + * Constructor for a new Factory that runs requests using given client. + */ + private Factory(OkHttpClient client) { + this.client = client; + } + + @Override + public ModelLoader build(Context context, GenericLoaderFactory factories) { + return new OkHttpUrlLoader(client); + } + + @Override + public void teardown() { + // Do nothing, this instance doesn't own the client. + } + } + + private final OkHttpClient client; + + private OkHttpUrlLoader(OkHttpClient client) { + this.client = client; + } + + @Override + public DataFetcher getResourceFetcher(GlideUrl model, int width, int height) { + return new OkHttpStreamFetcher(client, model); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index e5e0c401f6e..e6e9f9b19d8 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.components.location.SignalMapView; import org.thoughtcrime.securesms.components.location.SignalPlace; import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.MediaUtil; @@ -261,6 +262,11 @@ public static void selectLocation(Activity activity, int requestCode) { } } + public static void selectGif(Activity activity, int requestCode) { + Intent intent = new Intent(activity, GiphyActivity.class); + activity.startActivityForResult(intent, requestCode); + } + private @Nullable Uri getSlideUri() { return slide.isPresent() ? slide.get().getUri() : null; } diff --git a/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java b/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java index d7f81ba33f4..acb59a9e01c 100644 --- a/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java +++ b/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java @@ -6,8 +6,10 @@ import com.bumptech.glide.GlideBuilder; import com.bumptech.glide.load.engine.cache.DiskCache; import com.bumptech.glide.load.engine.cache.DiskCacheAdapter; +import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.module.GlideModule; +import org.thoughtcrime.securesms.glide.OkHttpUrlLoader; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; import org.thoughtcrime.securesms.mms.ContactPhotoUriLoader.ContactPhotoUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; @@ -17,7 +19,7 @@ public class TextSecureGlideModule implements GlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { - builder.setDiskCache(new NoopDiskCacheFactory()); +// builder.setDiskCache(new NoopDiskCacheFactory()); } @Override @@ -25,6 +27,7 @@ public void registerComponents(Context context, Glide glide) { glide.register(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory()); glide.register(ContactPhotoUri.class, InputStream.class, new ContactPhotoUriLoader.Factory()); glide.register(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory()); + glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); } public static class NoopDiskCacheFactory implements DiskCache.Factory { diff --git a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java index 093269eb0a8..3987c8dd145 100644 --- a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -16,6 +16,7 @@ import android.text.SpannableStringBuilder; import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; @@ -200,6 +201,7 @@ private Bitmap getBigPicture(@NonNull MasterSecret masterSecret, return Glide.with(context) .load(new DecryptableStreamUriLoader.DecryptableUri(masterSecret, uri)) .asBitmap() + .diskCacheStrategy(DiskCacheStrategy.NONE) .into(500, 500) .get(); } catch (InterruptedException | ExecutionException e) { diff --git a/src/org/thoughtcrime/securesms/util/JsonUtils.java b/src/org/thoughtcrime/securesms/util/JsonUtils.java index 284d0a9e1b0..4d4deef19b7 100644 --- a/src/org/thoughtcrime/securesms/util/JsonUtils.java +++ b/src/org/thoughtcrime/securesms/util/JsonUtils.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.Reader; public class JsonUtils { @@ -23,7 +24,11 @@ public static T fromJson(String serialized, Class clazz) throws IOExcepti return objectMapper.readValue(serialized, clazz); } - public static T fromJson(InputStreamReader serialized, Class clazz) throws IOException { + public static T fromJson(InputStream serialized, Class clazz) throws IOException { + return objectMapper.readValue(serialized, clazz); + } + + public static T fromJson(Reader serialized, Class clazz) throws IOException { return objectMapper.readValue(serialized, clazz); } diff --git a/src/org/thoughtcrime/securesms/util/Util.java b/src/org/thoughtcrime/securesms/util/Util.java index f826de5179f..6a776ba3f4f 100644 --- a/src/org/thoughtcrime/securesms/util/Util.java +++ b/src/org/thoughtcrime/securesms/util/Util.java @@ -384,6 +384,14 @@ public static void runOnMainSync(final @NonNull Runnable runnable) { } } + public static T getRandomElement(T[] elements) { + try { + return elements[SecureRandom.getInstance("SHA1PRNG").nextInt(elements.length)]; + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + public static boolean equals(@Nullable Object a, @Nullable Object b) { return a == b || (a != null && a.equals(b)); } diff --git a/src/org/thoughtcrime/securesms/util/concurrent/ListenableFuture.java b/src/org/thoughtcrime/securesms/util/concurrent/ListenableFuture.java index 59f3a54acbb..b943a26fb95 100644 --- a/src/org/thoughtcrime/securesms/util/concurrent/ListenableFuture.java +++ b/src/org/thoughtcrime/securesms/util/concurrent/ListenableFuture.java @@ -1,8 +1,9 @@ package org.thoughtcrime.securesms.util.concurrent; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; -public interface ListenableFuture { +public interface ListenableFuture extends Future { void addListener(Listener listener); public interface Listener { diff --git a/src/org/thoughtcrime/securesms/util/concurrent/SettableFuture.java b/src/org/thoughtcrime/securesms/util/concurrent/SettableFuture.java index 447faa9812a..818c4f5f17b 100644 --- a/src/org/thoughtcrime/securesms/util/concurrent/SettableFuture.java +++ b/src/org/thoughtcrime/securesms/util/concurrent/SettableFuture.java @@ -7,7 +7,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -public class SettableFuture implements Future, ListenableFuture { +public class SettableFuture implements ListenableFuture { private final List> listeners = new LinkedList<>(); @@ -42,6 +42,7 @@ public boolean set(T result) { this.result = result; this.completed = true; + notifyAll(); } notifyAllListeners(); @@ -54,6 +55,7 @@ public boolean setException(Throwable throwable) { this.exception = throwable; this.completed = true; + notifyAll(); } notifyAllListeners();