From 10f224ede5015a4a1c66b6e41f18621c503c2edd Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 11 Oct 2017 17:12:46 -0700 Subject: [PATCH] Update to glide 4.x // FREEBIE --- AndroidManifest.xml | 3 - build.gradle | 69 ++++---- gradle/wrapper/gradle-wrapper.jar | Bin 52271 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 74 +++++---- gradlew.bat | 14 +- proguard-glide.pro | 2 + res/layout/zooming_image_view.xml | 8 +- .../securesms/GroupCreateActivity.java | 42 ++--- .../securesms/LogSubmitActivity.java | 1 - .../components/RecentPhotoViewRail.java | 13 +- .../securesms/components/ThumbnailView.java | 52 +++--- .../components/ZoomingImageView.java | 44 ++--- .../contacts/avatars/ContactPhotoFactory.java | 30 ++-- .../securesms/giph/ui/GiphyAdapter.java | 49 +++--- .../securesms/glide/OkHttpStreamFetcher.java | 67 +++++--- .../securesms/glide/OkHttpUrlLoader.java | 45 +++--- .../mms/AttachmentStreamLocalUriFetcher.java | 46 ++++-- .../mms/AttachmentStreamUriLoader.java | 57 +++---- .../mms/ContactPhotoLocalUriFetcher.java | 7 +- .../securesms/mms/ContactPhotoUriLoader.java | 68 +++++--- .../mms/DecryptableStreamLocalUriFetcher.java | 6 +- .../mms/DecryptableStreamUriLoader.java | 60 ++++--- .../securesms/mms/LegacyMmsConnection.java | 7 +- .../thoughtcrime/securesms/mms/MmsRadio.java | 32 +++- .../securesms/mms/MmsRadioException.java | 4 + .../securesms/mms/RoundedCorners.java | 105 ------------ ...lideModule.java => SignalGlideModule.java} | 27 +++- .../SingleRecipientNotificationBuilder.java | 14 +- .../profiles/AvatarPhotoUriFetcher.java | 35 ++-- .../profiles/AvatarPhotoUriLoader.java | 62 +++++-- .../scribbles/StickerSelectFragment.java | 33 ++-- .../scribbles/widget/ScribbleView.java | 26 +-- .../securesms/util/BitmapUtil.java | 151 +++++++----------- .../securesms/util/MediaUtil.java | 15 +- 35 files changed, 639 insertions(+), 633 deletions(-) delete mode 100644 src/org/thoughtcrime/securesms/mms/RoundedCorners.java rename src/org/thoughtcrime/securesms/mms/{TextSecureGlideModule.java => SignalGlideModule.java} (54%) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c45c59817..ec0cedd42 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -105,9 +105,6 @@ - - diff --git a/build.gradle b/build.gradle index 02feaea42..75d26497f 100644 --- a/build.gradle +++ b/build.gradle @@ -45,14 +45,14 @@ repositories { } dependencies { - compile 'com.android.support:appcompat-v7:25.4.0' - compile 'com.android.support:recyclerview-v7:25.4.0' - compile 'com.android.support:design:25.4.0' - compile 'com.android.support:support-v13:25.4.0' - compile 'com.android.support:cardview-v7:25.4.0' - compile 'com.android.support:preference-v7:25.4.0' - compile 'com.android.support:preference-v14:25.4.0' - compile 'com.android.support:gridlayout-v7:25.4.0' + compile 'com.android.support:appcompat-v7:26.1.0' + compile 'com.android.support:recyclerview-v7:26.1.0' + compile 'com.android.support:design:26.1.0' + compile 'com.android.support:support-v13:26.1.0' + compile 'com.android.support:cardview-v7:26.1.0' + compile 'com.android.support:preference-v7:26.1.0' + compile 'com.android.support:preference-v14:26.1.0' + compile 'com.android.support:gridlayout-v7:26.1.0' compile 'com.android.support:multidex:1.0.2' compile 'com.google.android.gms:play-services-gcm:9.6.1' @@ -70,8 +70,9 @@ dependencies { compile 'se.emilsjolander:stickylistheaders:2.7.0' compile 'com.jpardogo.materialtabstrip:library:1.0.9' compile 'org.apache.httpcomponents:httpclient-android:4.3.5' - compile 'com.github.chrisbanes.photoview:library:1.3.1' - compile 'com.github.bumptech.glide:glide:3.7.0' + compile 'com.github.chrisbanes:PhotoView:2.1.3' + compile 'com.github.bumptech.glide:glide:4.2.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.2.0' compile 'com.makeramen:roundedimageview:2.1.0' compile 'com.pnikosis:materialish-progress:1.5' compile 'org.greenrobot:eventbus:3.0.0' @@ -131,14 +132,14 @@ dependencies { dependencyVerification { verify = [ - 'com.android.support:appcompat-v7:70551e62660db15b790c5275f56b9de4dd9407d1494d07c8f3dd5698f3638677', - 'com.android.support:recyclerview-v7:a2fe121f9d01ed8980e97095b4a3fe9700a0aa0a7d4b0f8c594f765ad8455a0d', - 'com.android.support:design:3f409bf2019967ffc344cfaf11e52131fac982468a1707aaeb25bf3c52838966', - 'com.android.support:support-v13:f2dcf3eb3fe0271038dc78f6cff9cc3256a591f5c5198277b3887480e5b95e79', - 'com.android.support:cardview-v7:f3fbbe1fcfdbec7333c6a2c516c5fd511a909d1975271818e268d6fe297d8c70', - 'com.android.support:preference-v7:69bfa8e5527585dc51c02393b882c6b7e1e682995d5fbe45f57b7cb4e6970a7b', - 'com.android.support:preference-v14:aac6e6cb89b70e27859d88cf75410a5feab1c7a9364d420de42ddb7c154a866b', - 'com.android.support:gridlayout-v7:4c805b95e5b0a39c7244a0d1a14449cc54a9ab242b806f7379b38846f0539ce9', + 'com.android.support:appcompat-v7:9d44e7bf343dfd19a55e3e6f4c4e733b68d32509e0b0af5b32f2981f4f1dedd8', + 'com.android.support:recyclerview-v7:389cb47a7dabca4fb8c23657ff7c85ebc651428580d3a5ea0349eeb43ddea94b', + 'com.android.support:design:76f5fbb365bf2d622af5df8a4205904409250305685e38670bf654ac90c2494d', + 'com.android.support:support-v13:fc7ba35b0502a6168b350342779c6943100ace19cd6dd573707bddfa8e9e78a2', + 'com.android.support:cardview-v7:7ea56ed5560b629ee1c0f24af6693e32974fbc8b91b544052cd2c14b176c85e0', + 'com.android.support:preference-v7:42672e51c06c6e26a40798d3379ede97ee42076c84592d670c4e5c96630c50f1', + 'com.android.support:preference-v14:f340c88589184fd53ad46aebbba1ae5b88b5919f92ed085bf0f687a58d0e0e17', + 'com.android.support:gridlayout-v7:6fe57dd164f2e1d99ad650a56f686ddecd02bfbfabbfbd451e81a23eada5e564', 'com.android.support:multidex:7cd48755c7cfdb6dd2d21cbb02236ec390f6ac91cde87eb62f475b259ab5301d', 'com.google.android.gms:play-services-gcm:312e61253a236f2d9b750b9c04fc92fd190d23b0b2755c99de6ce4a28b259dae', 'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e', @@ -152,8 +153,8 @@ dependencyVerification { 'se.emilsjolander:stickylistheaders:a08ca948aa6b220f09d82f16bbbac395f6b78897e9eeac6a9f0b0ba755928eeb', 'com.jpardogo.materialtabstrip:library:c6ef812fba4f74be7dc4a905faa4c2908cba261a94c13d4f96d5e67e4aad4aaa', 'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1', - 'com.github.chrisbanes.photoview:library:f152dd0a87aca891aa182e42863fa05e0e8b2842e3b9fc512d7a3a6243c38ac4', - 'com.github.bumptech.glide:glide:76ef123957b5fbaebb05fcbe6606dd58c3bc3fcdadb257f99811d0ac9ea9b88b', + 'com.github.chrisbanes:PhotoView:ed06775308da260e1fd86d1d3288988fcd3d80db24ce0d7c9fcfedc39e622292', + 'com.github.bumptech.glide:glide:555350c4b9d163f1d3772a64a92119086073ed88340eb284391b1acc1bb5dd6c', 'com.makeramen:roundedimageview:1f5a1865796b308c6cdd114acc6e78408b110f0a62fc63553278fbeacd489cd1', 'com.pnikosis:materialish-progress:d71d80e00717a096784482aee21001a9d299fec3833e4ebd87739ed36cf77c54', 'org.greenrobot:eventbus:180d4212467df06f2fbc9c8d8a2984533ac79c87769ad883bc421612f0b4e17c', @@ -173,22 +174,26 @@ dependencyVerification { 'com.takisoft.fix:colorpicker:f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1', 'com.codewaves.stickyheadergrid:stickyheadergrid:5b4aa6a52a957cfd55f60f4220c11c0c371385a3cb9786cae03c260dcdef5794', 'com.github.dmytrodanylyk.circular-progress-button:library:635882453475181d737719b2adef658d80f9f85c9bdaf022f06cd22f387cdb16', - 'com.android.support:support-annotations:a774272036941b4e912eb426d70c848bde7f06a3bf5fb491f75a427dc6595270', - 'com.android.support:support-v4:ee44c481a1f4d6978568e223e8125379b52b2ececdd53450e09ebae144bd377d', - 'com.android.support:support-vector-drawable:077009d13882ee96f061e4bc2dbe7cce7ae1762d8297592a787ff741afbfb1f2', - 'com.android.support:animated-vector-drawable:628ab1d56a6ee4cbedf32617af8b2a1fe02964ed0628e8f898cc09ddba6e1835', - 'com.android.support:support-compat:54019c63614ce08b02d7b9605490cd2b29ba5b2505f394a9517450b5f72b30ca', - 'com.android.support:support-core-ui:e72ae29b823889686cff6fcb948d6745c2baf6d4c2af4fdffa1ec1e42e3833a3', - 'com.android.support:transition:848270144fb180efd2bf928a00ed176dbbc5290badfd638390ffba90088df8b3', + 'com.android.support:support-annotations:99d6199ad5a09a0e5e8a49a4cc08f818483ddcfd7eedea2f9923412daf982309', + 'com.android.support:support-v4:36d8385de1be7791231acb933b757198f97cb53bc7d046e8c4bc403d214caaca', + 'com.android.support:support-vector-drawable:1151b7f0ea29c9a9a8fee042a1dbe82f196632d801c438d08b279e131c767118', + 'com.android.support:animated-vector-drawable:d5905aee3c8a4ac75e069a73b914c0a41b9b36b0e6b04126719fca22659d3cc8', + 'com.android.support:support-compat:7d6da01cf9766b1705c6c80cfc12274a895b406c4c287900b07a56145ca6c030', + 'com.android.support:support-core-ui:82f538051599335ea881ec264407547cab52be750f16ce099cfb27754fc755ff', + 'com.android.support:transition:c5d3d1204997f80af1f4a3a315a54b1a23543c554963cef831da726aac34b56f', 'com.google.android.gms:play-services-base:0ca636a8fc9a5af45e607cdcd61783bf5d561cbbb0f862021ce69606eee5ad49', 'com.google.android.gms:play-services-basement:95dd882c5ffba15b9a99de3fefb05d3a01946623af67454ca00055d222f85a8d', 'com.google.android.gms:play-services-iid:54e919f9957b8b7820da7ee9b83471d00d0cac1cf08ddea8b5b41aea80bb1a70', 'org.whispersystems:signal-protocol-android:5b8acded7f2a40178eb90ab8e8cbfec89d170d91b3ff5e78487d1098df6185a1', 'org.whispersystems:signal-service-java:308d9e61b753760d0f3828eb3181db58469e75c763bdce5a8335df6c4af47695', + 'com.github.bumptech.glide:gifdecoder:217da4520c568a93aea9c7ce3b3cac2c61fabed5113b07ae38698054f6d2d8b6', + 'com.github.bumptech.glide:disklrucache:795c13245498c0cd806c3af71ee57b3f179cbd1609440a3021c211c364ef74d3', + 'com.github.bumptech.glide:annotations:057927a236f3229e72cfbac8bed0e9fb398473daf7d933390f59ea4cb79c137b', 'com.nineoldandroids:library:68025a14e3e7673d6ad2f95e4b46d78d7d068343aa99256b686fe59de1b3163a', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'com.klinkerapps:logger:177e325259a8b111ad6745ec10db5861723c99f402222b80629f576f49408541', 'com.google.android:flexbox:a9989fd13ae2ee42765dfc515fe362edf4f326e74925d02a10369df8092a4935', + 'android.arch.lifecycle:runtime:e4e34e5d02bd102e8d39ddbc29f9ead8a15a61e367993d02238196ac48509ad8', 'com.google.android.gms:play-services-tasks:69ec265168e601d0203d04cd42e34bb019b2f029aa1e16fabd38a5153eea2086', 'org.whispersystems:curve25519-android:82595394422b957d4a5b5f1b27b75ba25cf6dc4db4d312418ca38cd6fff279ca', 'org.whispersystems:signal-protocol-java:5152c2b01a25147967d6bf82e540f947901bdfa79260be3eb3e96b03f787d6b5', @@ -197,19 +202,21 @@ dependencyVerification { 'com.fasterxml.jackson.core:jackson-databind:835097bcdd11f5bc8a08378c70d4c8054dfa4b911691cc2752063c75534d198d', 'com.squareup.okhttp3:okhttp:c1d57f913f74f61d424d4250a92723ba9a61affc12a0ab194d84cc179b472841', 'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a', + 'android.arch.lifecycle:common:86bf301a20ad0cd0a391e22a52e6fbf90575c096ff83233fa9fd0d52b3219121', + 'android.arch.core:common:5192934cd73df32e2c15722ed7fc488dde90baaec9ae030010dd1a80fb4e74e1', 'org.whispersystems:curve25519-java:7dd659d8822c06c3aea1a47f18fac9e5761e29cab8100030b877db445005f03e', 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94', 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0', 'com.squareup.okio:okio:734269c3ebc5090e3b23566db558f421f0b4027277c79ad5d176b8ec168bb850', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', - 'com.android.support:support-media-compat:566a161d9cb0083ef62a53e46b71ce5b3d455b8635b1a0a4ae28d96d4b583de8', - 'com.android.support:support-core-utils:34b8437dfa95ff28d29cf57ffa3b1354a9fa9bfe4059f0fd5ce2f5e4326a1748', - 'com.android.support:support-fragment:316d35d4d2d2902057efad104a73e4bdb50bee260a7075678185b8cd71170945', + 'com.android.support:support-media-compat:9d8cee7cd40eff22ebdeb90c8e70f5ee96c5bd25cb2c3e3b3940e27285a3e98a', + 'com.android.support:support-core-utils:4fda6d4eb430971e3b1dad7456988333f374b0f4ba15f99839ca1a0ab5155c8a', + 'com.android.support:support-fragment:a0ab3369ef40fe199160692f0463a5f63f1277ebfb64dd587c76fdb128d76b32', ] } android { - compileSdkVersion 25 + compileSdkVersion 26 buildToolsVersion '25.0.2' useLibrary 'org.apache.http.legacy' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 30d399d8d2bf522ff5de94bf434a7cc43a9a74b5..fbd0c46ecebf82e02951b4dc3528ecd4269a8e3b 100644 GIT binary patch delta 25555 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 X)LUSC7jg($F9yc-4kfSUy6yh}_q3+h delta 23706 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%0A2n*MWuozM^9|(MV=+bPjjGJc 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ee02693a8..bd7127d9e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Mar 09 11:46:40 PST 2017 +#Wed Oct 11 12:30:44 PDT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-all.zip diff --git a/gradlew b/gradlew index 91a7e269e..4453ccea3 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730b..e95643d6a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/proguard-glide.pro b/proguard-glide.pro index 0588677f3..a5a3efcc1 100644 --- a/proguard-glide.pro +++ b/proguard-glide.pro @@ -1,3 +1,5 @@ +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.AppGlideModule -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { **[] $VALUES; public *; diff --git a/res/layout/zooming_image_view.xml b/res/layout/zooming_image_view.xml index 6ad6c4dc5..da1c60519 100644 --- a/res/layout/zooming_image_view.xml +++ b/res/layout/zooming_image_view.xml @@ -3,10 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" tools:context="org.thoughtcrime.securesms.components.ZoomingImageView"> - + () { - @Override - public void onResourceReady(Bitmap resource, GlideAnimation glideAnimation) { - setAvatar(Crop.getOutput(data), resource); - } - }); + GlideApp.with(this) + .asBitmap() + .load(Crop.getOutput(data)) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .override(AVATAR_SIZE, AVATAR_SIZE) + .into(new SimpleTarget() { + @Override + public void onResourceReady(Bitmap resource, Transition transition) { + setAvatar(Crop.getOutput(data), resource); + } + }); } } @@ -573,12 +577,12 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity private void setAvatar(T model, Bitmap bitmap) { avatarBmp = bitmap; - Glide.with(this) - .load(model) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .transform(new RoundedCorners(this, avatar.getWidth() / 2)) - .into(avatar); + GlideApp.with(this) + .load(model) + .circleCrop() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(avatar); } private static class GroupData { diff --git a/src/org/thoughtcrime/securesms/LogSubmitActivity.java b/src/org/thoughtcrime/securesms/LogSubmitActivity.java index 1455a274d..e7d1a4d7d 100644 --- a/src/org/thoughtcrime/securesms/LogSubmitActivity.java +++ b/src/org/thoughtcrime/securesms/LogSubmitActivity.java @@ -4,7 +4,6 @@ import android.content.ActivityNotFoundException; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.view.MenuItem; import android.widget.Toast; diff --git a/src/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java b/src/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java index 7cfb44fd8..cca2462ec 100644 --- a/src/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java +++ b/src/org/thoughtcrime/securesms/components/RecentPhotoViewRail.java @@ -20,7 +20,6 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; -import com.bumptech.glide.Glide; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.signature.MediaStoreSignature; @@ -28,6 +27,7 @@ import com.bumptech.glide.signature.MediaStoreSignature; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.util.ViewUtil; public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks { @@ -111,12 +111,11 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo Key signature = new MediaStoreSignature(mimeType, dateModified, orientation); - Glide.with(getContext()) - .fromMediaStore() - .load(uri) - .signature(signature) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(viewHolder.imageView); + GlideApp.with(getContext()) + .load(uri) + .signature(signature) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(viewHolder.imageView); viewHolder.imageView.setOnClickListener(new OnClickListener() { @Override diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index 54c926162..dacf71af4 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -15,22 +15,25 @@ import android.view.View; import android.widget.FrameLayout; import android.widget.ImageView; -import com.bumptech.glide.DrawableRequestBuilder; -import com.bumptech.glide.GenericRequestBuilder; import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; -import org.thoughtcrime.securesms.mms.RoundedCorners; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.whispersystems.libsignal.util.guava.Optional; +import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; + public class ThumbnailView extends FrameLayout { private static final String TAG = ThumbnailView.class.getSimpleName(); @@ -141,18 +144,18 @@ public class ThumbnailView extends FrameLayout { if (slide.getThumbnailUri() != null) buildThumbnailGlideRequest(slide, masterSecret).into(image); else if (slide.hasPlaceholder()) buildPlaceholderGlideRequest(slide).into(image); - else Glide.clear(image); + else Glide.with(getContext()).clear(image); } public void setImageResource(@NonNull MasterSecret masterSecret, @NonNull Uri uri) { if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); - Glide.with(getContext()) - .load(new DecryptableUri(masterSecret, uri)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .crossFade() - .transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint)) - .into(image); + GlideApp.with(getContext()) + .load(new DecryptableUri(masterSecret, uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transform(new RoundedCorners(radius)) + .transition(withCrossFade()) + .into(image); } public void setThumbnailClickListener(SlideClickListener listener) { @@ -164,7 +167,7 @@ public class ThumbnailView extends FrameLayout { } public void clear() { - if (isContextValid()) Glide.clear(image); + if (isContextValid()) Glide.with(getContext()).clear(image); if (transferControls.isPresent()) getTransferControls().clear(); slide = null; @@ -181,24 +184,23 @@ public class ThumbnailView extends FrameLayout { !((Activity)getContext()).isDestroyed(); } - private GenericRequestBuilder buildThumbnailGlideRequest(@NonNull Slide slide, @NonNull MasterSecret masterSecret) { - @SuppressWarnings("ConstantConditions") - DrawableRequestBuilder builder = Glide.with(getContext()) - .load(new DecryptableUri(masterSecret, slide.getThumbnailUri())) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .crossFade() - .transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint)); + private RequestBuilder buildThumbnailGlideRequest(@NonNull Slide slide, @NonNull MasterSecret masterSecret) { + RequestBuilder builder = GlideApp.with(getContext()) + .load(new DecryptableUri(masterSecret, slide.getThumbnailUri())) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transform(new RoundedCorners(radius)) + .transition(withCrossFade()); if (slide.isInProgress()) return builder; - else return builder.error(R.drawable.ic_missing_thumbnail_picture); + else return builder.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)); } - private GenericRequestBuilder buildPlaceholderGlideRequest(Slide slide) { - return Glide.with(getContext()) - .load(slide.getPlaceholderRes(getContext().getTheme())) - .asBitmap() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .fitCenter(); + private RequestBuilder buildPlaceholderGlideRequest(Slide slide) { + return GlideApp.with(getContext()) + .asBitmap() + .load(slide.getPlaceholderRes(getContext().getTheme())) + .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 e2b12f4d0..3ff0fc02f 100644 --- a/src/org/thoughtcrime/securesms/components/ZoomingImageView.java +++ b/src/org/thoughtcrime/securesms/components/ZoomingImageView.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.components; import android.content.Context; -import android.graphics.Canvas; import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.Nullable; @@ -10,37 +9,33 @@ import android.util.Log; import android.util.Pair; import android.view.View; import android.widget.FrameLayout; -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.RequestListener; -import com.bumptech.glide.request.target.GlideDrawableImageViewTarget; import com.bumptech.glide.request.target.Target; import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; +import com.github.chrisbanes.photoview.PhotoView; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder; import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.util.MediaUtil; import java.io.IOException; import java.io.InputStream; -import uk.co.senab.photoview.PhotoViewAttacher; public class ZoomingImageView extends FrameLayout { private static final String TAG = ZoomingImageView.class.getName(); - private final ImageView imageView; - private final PhotoViewAttacher imageViewAttacher; + private final PhotoView photoView; private final SubsamplingScaleImageView subsamplingImageView; public ZoomingImageView(Context context) { @@ -56,9 +51,8 @@ public class ZoomingImageView extends FrameLayout { inflate(context, R.layout.zooming_image_view, this); - this.imageView = (ImageView) findViewById(R.id.image_view); - this.subsamplingImageView = (SubsamplingScaleImageView) findViewById(R.id.subsampling_image_view); - this.imageViewAttacher = new PhotoViewAttacher(imageView); + this.photoView = findViewById(R.id.image_view); + this.subsamplingImageView = findViewById(R.id.subsampling_image_view); this.subsamplingImageView.setBitmapDecoderClass(AttachmentBitmapDecoder.class); this.subsamplingImageView.setRegionDecoderClass(AttachmentRegionDecoder.class); @@ -74,7 +68,7 @@ public class ZoomingImageView extends FrameLayout { new AsyncTask>() { @Override protected @Nullable Pair doInBackground(Void... params) { - if (contentType.equals("image/gif")) return null; + if (MediaUtil.isGif(contentType)) return null; try { InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, uri); @@ -100,32 +94,26 @@ public class ZoomingImageView extends FrameLayout { } private void setImageViewUri(MasterSecret masterSecret, Uri uri) { + photoView.setVisibility(View.VISIBLE); subsamplingImageView.setVisibility(View.GONE); - imageView.setVisibility(View.VISIBLE); - Glide.with(getContext()) - .load(new DecryptableUri(masterSecret, uri)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .dontTransform() - .dontAnimate() - .into(new GlideDrawableImageViewTarget(imageView) { - @Override protected void setResource(GlideDrawable resource) { - super.setResource(resource); - imageViewAttacher.update(); - } - }); + GlideApp.with(getContext()) + .load(new DecryptableUri(masterSecret, uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontTransform() + .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + .into(photoView); } private void setSubsamplingImageViewUri(Uri uri) { subsamplingImageView.setVisibility(View.VISIBLE); - imageView.setVisibility(View.GONE); + photoView.setVisibility(View.GONE); subsamplingImageView.setImage(ImageSource.uri(uri)); } - public void cleanup() { - imageView.setImageDrawable(null); + photoView.setImageDrawable(null); subsamplingImageView.recycle(); } } diff --git a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java index cd622b4ca..990436177 100644 --- a/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java +++ b/src/org/thoughtcrime/securesms/contacts/avatars/ContactPhotoFactory.java @@ -11,13 +11,12 @@ import android.support.annotation.WorkerThread; import android.text.TextUtils; import android.util.Log; -import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.mms.ContactPhotoUriLoader.ContactPhotoUri; -import org.thoughtcrime.securesms.profiles.AvatarPhotoUriLoader; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.profiles.AvatarPhotoUriLoader.AvatarPhotoUri; import java.util.concurrent.ExecutionException; @@ -58,10 +57,13 @@ public class ContactPhotoFactory { if (uri == null) return getSignalAvatarContactPhoto(context, address, name, targetSize); try { - Bitmap bitmap = Glide.with(context) - .load(new ContactPhotoUri(uri)).asBitmap() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop().into(targetSize, targetSize).get(); + Bitmap bitmap = GlideApp.with(context) + .asBitmap() + .load(new ContactPhotoUri(uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .submit(targetSize, targetSize) + .get(); return new BitmapContactPhoto(bitmap); } catch (ExecutionException e) { return getSignalAvatarContactPhoto(context, address, name, targetSize); @@ -83,14 +85,14 @@ public class ContactPhotoFactory { int targetSize) { try { - Bitmap bitmap = Glide.with(context) - .load(new AvatarPhotoUri(address)) - .asBitmap() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) - .centerCrop() - .into(targetSize, targetSize) - .get(); + Bitmap bitmap = GlideApp.with(context) + .asBitmap() + .load(new AvatarPhotoUri(address)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .centerCrop() + .submit(targetSize, targetSize) + .get(); return new BitmapContactPhoto(bitmap); } catch (IllegalArgumentException e) { diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java index 99a210902..5ebffa4ba 100644 --- a/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java @@ -3,7 +3,9 @@ package org.thoughtcrime.securesms.giph.ui; import android.content.Context; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; @@ -11,16 +13,18 @@ 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.RequestBuilder; +import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.load.resource.drawable.GlideDrawable; +import com.bumptech.glide.load.engine.GlideException; 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.mms.GlideApp; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; @@ -37,7 +41,7 @@ public class GiphyAdapter extends RecyclerView.Adapter { + class GiphyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, RequestListener { public AspectRatioImageView thumbnail; public GiphyImage image; @@ -58,7 +62,7 @@ public class GiphyAdapter extends RecyclerView.Adapter target, boolean isFirstResource) { + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { Log.w(TAG, e); synchronized (this) { @@ -69,10 +73,11 @@ public class GiphyAdapter extends RecyclerView.Adapter target, boolean isFromMemoryCache, boolean isFirstResource) { + public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { synchronized (this) { if (image.getGifUrl().equals(model)) { this.modelReady = true; @@ -83,6 +88,7 @@ public class GiphyAdapter extends RecyclerView.Adapter images) { - this.context = context; + this.context = context.getApplicationContext(); this.images = images; } @@ -134,32 +140,33 @@ public class GiphyAdapter extends RecyclerView.Adapter thumbnailRequest = Glide.with(context) - .load(image.getStillUrl()); + RequestBuilder thumbnailRequest = GlideApp.with(context) + .load(image.getStillUrl()) + .diskCacheStrategy(DiskCacheStrategy.ALL); if (Util.isLowMemory(context)) { - Glide.with(context) - .load(image.getStillUrl()) - .placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context))) - .diskCacheStrategy(DiskCacheStrategy.ALL) - .into(holder.thumbnail); + GlideApp.with(context) + .load(image.getStillUrl()) + .placeholder(new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(context))) + .diskCacheStrategy(DiskCacheStrategy.ALL) + .into(holder.thumbnail); holder.setModelReady(); } else { - 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); + GlideApp.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 void onViewRecycled(GiphyViewHolder holder) { super.onViewRecycled(holder); - Glide.clear(holder.thumbnail); + Glide.with(context).clear(holder.thumbnail); } @Override diff --git a/src/org/thoughtcrime/securesms/glide/OkHttpStreamFetcher.java b/src/org/thoughtcrime/securesms/glide/OkHttpStreamFetcher.java index 380f13023..5aeba7652 100644 --- a/src/org/thoughtcrime/securesms/glide/OkHttpStreamFetcher.java +++ b/src/org/thoughtcrime/securesms/glide/OkHttpStreamFetcher.java @@ -1,6 +1,9 @@ package org.thoughtcrime.securesms.glide; +import android.support.annotation.NonNull; + import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.util.ContentLengthInputStream; @@ -17,7 +20,7 @@ import okhttp3.ResponseBody; /** * Fetches an {@link InputStream} using the okhttp library. */ -public class OkHttpStreamFetcher implements DataFetcher { +class OkHttpStreamFetcher implements DataFetcher { private static final String TAG = OkHttpStreamFetcher.class.getName(); @@ -26,33 +29,38 @@ public class OkHttpStreamFetcher implements DataFetcher { private InputStream stream; private ResponseBody responseBody; - public OkHttpStreamFetcher(OkHttpClient client, GlideUrl url) { + 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()); + public void loadData(Priority priority, DataCallback callback) { + try { + 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()); + 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); + + callback.onDataReady(stream); + } catch (IOException e) { + callback.onLoadFailed(e); } - - 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 @@ -69,13 +77,20 @@ public class OkHttpStreamFetcher implements DataFetcher { } } - @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 } + + @NonNull + @Override + public Class getDataClass() { + return InputStream.class; + } + + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.REMOTE; + } } \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java b/src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java index 7ee19060f..a2aecc0fa 100644 --- a/src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java +++ b/src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java @@ -1,12 +1,12 @@ package org.thoughtcrime.securesms.glide; -import android.content.Context; +import android.support.annotation.Nullable; -import com.bumptech.glide.load.data.DataFetcher; -import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; import org.thoughtcrime.securesms.giph.net.GiphyProxySelector; @@ -19,9 +19,23 @@ import okhttp3.OkHttpClient; */ public class OkHttpUrlLoader implements ModelLoader { - /** - * The default factory for {@link OkHttpUrlLoader}s. - */ + private final OkHttpClient client; + + private OkHttpUrlLoader(OkHttpClient client) { + this.client = client; + } + + @Nullable + @Override + public LoadData buildLoadData(GlideUrl glideUrl, int width, int height, Options options) { + return new LoadData<>(glideUrl, new OkHttpStreamFetcher(client, glideUrl)); + } + + @Override + public boolean handles(GlideUrl glideUrl) { + return true; + } + public static class Factory implements ModelLoaderFactory { private static volatile OkHttpClient internalClient; private OkHttpClient client; @@ -39,22 +53,16 @@ public class OkHttpUrlLoader implements ModelLoader { 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) { + public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new OkHttpUrlLoader(client); } @@ -63,15 +71,4 @@ public class OkHttpUrlLoader implements ModelLoader { // 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/AttachmentStreamLocalUriFetcher.java b/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java index b9144d45a..a0462632f 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentStreamLocalUriFetcher.java @@ -1,40 +1,45 @@ package org.thoughtcrime.securesms.mms; -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; +import android.support.annotation.NonNull; import android.util.Log; import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; -import com.bumptech.glide.load.data.StreamLocalUriFetcher; -import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -public class AttachmentStreamLocalUriFetcher implements DataFetcher { +class AttachmentStreamLocalUriFetcher implements DataFetcher { + private static final String TAG = AttachmentStreamLocalUriFetcher.class.getSimpleName(); + private File attachment; private byte[] key; private InputStream is; - public AttachmentStreamLocalUriFetcher(File attachment, byte[] key) { + AttachmentStreamLocalUriFetcher(File attachment, byte[] key) { this.attachment = attachment; this.key = key; } - @Override public InputStream loadData(Priority priority) throws Exception { - is = new AttachmentCipherInputStream(attachment, key, Optional.absent()); - return is; + @Override + public void loadData(Priority priority, DataCallback callback) { + try { + is = new AttachmentCipherInputStream(attachment, key, Optional.absent()); + callback.onDataReady(is); + } catch (IOException | InvalidMessageException e) { + callback.onLoadFailed(e); + } } - @Override public void cleanup() { + @Override + public void cleanup() { try { if (is != null) is.close(); is = null; @@ -43,11 +48,20 @@ public class AttachmentStreamLocalUriFetcher implements DataFetcher } } - @Override public String getId() { - return AttachmentStreamLocalUriFetcher.class.getCanonicalName() + "::" + attachment.getAbsolutePath(); + @Override + public void cancel() {} + + @NonNull + @Override + public Class getDataClass() { + return InputStream.class; } - @Override public void cancel() { - + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.LOCAL; } + + } diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java b/src/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java index 2db01844a..253724ccb 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentStreamUriLoader.java @@ -1,39 +1,38 @@ package org.thoughtcrime.securesms.mms; -import android.content.Context; -import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; -import com.bumptech.glide.load.data.DataFetcher; -import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.Key; +import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; -import com.bumptech.glide.load.model.stream.StreamModelLoader; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; -import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; -import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; -import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import java.io.File; import java.io.InputStream; +import java.security.MessageDigest; -/** - * A {@link ModelLoader} for translating uri models into {@link InputStream} data. Capable of handling 'http', - * 'https', 'android.resource', 'content', and 'file' schemes. Unsupported schemes will throw an exception in - * {@link #getResourceFetcher(Uri, int, int)}. - */ -public class AttachmentStreamUriLoader implements StreamModelLoader { - private final Context context; +public class AttachmentStreamUriLoader implements ModelLoader { - /** - * THe default factory for {@link com.bumptech.glide.load.model.stream.StreamUriLoader}s. - */ - public static class Factory implements ModelLoaderFactory { + @Nullable + @Override + public LoadData buildLoadData(AttachmentModel attachmentModel, int width, int height, Options options) { + return new LoadData<>(attachmentModel, new AttachmentStreamLocalUriFetcher(attachmentModel.attachment, attachmentModel.key)); + } + + @Override + public boolean handles(AttachmentModel attachmentModel) { + return true; + } + + static class Factory implements ModelLoaderFactory { @Override - public StreamModelLoader build(Context context, GenericLoaderFactory factories) { - return new AttachmentStreamUriLoader(context); + public ModelLoader build(MultiModelLoaderFactory multiFactory) { + return new AttachmentStreamUriLoader(); } @Override @@ -42,16 +41,7 @@ public class AttachmentStreamUriLoader implements StreamModelLoader getResourceFetcher(AttachmentModel model, int width, int height) { - return new AttachmentStreamLocalUriFetcher(model.attachment, model.key); - } - - public static class AttachmentModel { + public static class AttachmentModel implements Key { public @NonNull File attachment; public @NonNull byte[] key; @@ -60,6 +50,11 @@ public class AttachmentStreamUriLoader implements StreamModelLoader { -public class ContactPhotoUriLoader implements StreamModelLoader { private final Context context; - /** - * THe default factory for {@link com.bumptech.glide.load.model.stream.StreamUriLoader}s. - */ - public static class Factory implements ModelLoaderFactory { + private ContactPhotoUriLoader(Context context) { + this.context = context; + } + + @Nullable + @Override + public LoadData buildLoadData(ContactPhotoUri contactPhotoUri, int width, int height, Options options) { + return new LoadData<>(contactPhotoUri, new StreamLocalUriFetcher(context.getContentResolver(), contactPhotoUri.uri)); + } + + @Override + public boolean handles(ContactPhotoUri contactPhotoUri) { + return true; + } + + static class Factory implements ModelLoaderFactory { + + private final Context context; + + Factory(Context context) { + this.context = context.getApplicationContext(); + } @Override - public StreamModelLoader build(Context context, GenericLoaderFactory factories) { + public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new ContactPhotoUriLoader(context); } @@ -32,21 +55,28 @@ public class ContactPhotoUriLoader implements StreamModelLoader } } - public ContactPhotoUriLoader(Context context) { - this.context = context; - } - - @Override - public DataFetcher getResourceFetcher(ContactPhotoUri model, int width, int height) { - return new ContactPhotoLocalUriFetcher(context, model.uri); - } - - public static class ContactPhotoUri { + public static class ContactPhotoUri implements Key { public @NonNull Uri uri; public ContactPhotoUri(@NonNull Uri uri) { this.uri = uri; } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) { + messageDigest.update(uri.toString().getBytes()); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof ContactPhotoUri)) return false; + + return this.uri.equals(((ContactPhotoUri)other).uri); + } + + public int hashCode() { + return uri.hashCode(); + } } } diff --git a/src/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java b/src/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java index f87f12232..e49d075ff 100644 --- a/src/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java +++ b/src/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java @@ -17,15 +17,15 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -public class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher { +class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher { private static final String TAG = DecryptableStreamLocalUriFetcher.class.getSimpleName(); private Context context; private MasterSecret masterSecret; - public DecryptableStreamLocalUriFetcher(Context context, MasterSecret masterSecret, Uri uri) { - super(context, uri); + DecryptableStreamLocalUriFetcher(Context context, MasterSecret masterSecret, Uri uri) { + super(context.getContentResolver(), uri); this.context = context; this.masterSecret = masterSecret; } diff --git a/src/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java b/src/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java index b5fcdf314..2f67e3653 100644 --- a/src/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java +++ b/src/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java @@ -3,33 +3,49 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; -import com.bumptech.glide.load.data.DataFetcher; -import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.Key; +import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; -import com.bumptech.glide.load.model.stream.StreamModelLoader; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import java.io.InputStream; +import java.security.MessageDigest; + +public class DecryptableStreamUriLoader implements ModelLoader { -/** - * A {@link ModelLoader} for translating uri models into {@link InputStream} data. Capable of handling 'http', - * 'https', 'android.resource', 'content', and 'file' schemes. Unsupported schemes will throw an exception in - * {@link #getResourceFetcher(Uri, int, int)}. - */ -public class DecryptableStreamUriLoader implements StreamModelLoader { private final Context context; - /** - * THe default factory for {@link com.bumptech.glide.load.model.stream.StreamUriLoader}s. - */ - public static class Factory implements ModelLoaderFactory { + private DecryptableStreamUriLoader(Context context) { + this.context = context; + } + + @Nullable + @Override + public LoadData buildLoadData(DecryptableUri decryptableUri, int width, int height, Options options) { + return new LoadData<>(decryptableUri, new DecryptableStreamLocalUriFetcher(context, decryptableUri.masterSecret, decryptableUri.uri)); + } + + @Override + public boolean handles(DecryptableUri decryptableUri) { + return true; + } + + static class Factory implements ModelLoaderFactory { + + private final Context context; + + Factory(Context context) { + this.context = context.getApplicationContext(); + } @Override - public StreamModelLoader build(Context context, GenericLoaderFactory factories) { + public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new DecryptableStreamUriLoader(context); } @@ -39,16 +55,7 @@ public class DecryptableStreamUriLoader implements StreamModelLoader getResourceFetcher(DecryptableUri model, int width, int height) { - return new DecryptableStreamLocalUriFetcher(context, model.masterSecret, model.uri); - } - - public static class DecryptableUri { + public static class DecryptableUri implements Key { public @NonNull MasterSecret masterSecret; public @NonNull Uri uri; @@ -57,6 +64,11 @@ public class DecryptableStreamUriLoader implements StreamModelLoader " + routeToHostObtained); - return routeToHostObtained; + return false; } protected static byte[] parseResponse(InputStream is) throws IOException { diff --git a/src/org/thoughtcrime/securesms/mms/MmsRadio.java b/src/org/thoughtcrime/securesms/mms/MmsRadio.java index 7a3bcc63e..5f61ad822 100644 --- a/src/org/thoughtcrime/securesms/mms/MmsRadio.java +++ b/src/org/thoughtcrime/securesms/mms/MmsRadio.java @@ -11,8 +11,13 @@ import android.util.Log; import org.thoughtcrime.securesms.util.Util; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + public class MmsRadio { + private static final String TAG = MmsRadio.class.getSimpleName(); + private static MmsRadio instance; public static synchronized MmsRadio getInstance(Context context) { @@ -52,8 +57,17 @@ public class MmsRadio { if (connectedCounter == 0) { Log.w("MmsRadio", "Turning off MMS radio..."); - connectivityManager.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS); - + try { + final Method stopUsingNetworkFeatureMethod = connectivityManager.getClass().getMethod("stopUsingNetworkFeature", Integer.TYPE, String.class); + stopUsingNetworkFeatureMethod.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS); + } catch (NoSuchMethodException nsme) { + Log.w(TAG, nsme); + } catch (IllegalAccessException iae) { + Log.w(TAG, iae); + } catch (InvocationTargetException ite) { + Log.w(TAG, ite); + } + if (connectivityListener != null) { Log.w("MmsRadio", "Unregistering receiver..."); context.unregisterReceiver(connectivityListener); @@ -63,8 +77,18 @@ public class MmsRadio { } public synchronized void connect() throws MmsRadioException { - int status = connectivityManager.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE, - FEATURE_ENABLE_MMS); + int status; + + try { + final Method startUsingNetworkFeatureMethod = connectivityManager.getClass().getMethod("startUsingNetworkFeature", Integer.TYPE, String.class); + status = (int)startUsingNetworkFeatureMethod.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS); + } catch (NoSuchMethodException nsme) { + throw new MmsRadioException(nsme); + } catch (IllegalAccessException iae) { + throw new MmsRadioException(iae); + } catch (InvocationTargetException ite) { + throw new MmsRadioException(ite); + } Log.w("MmsRadio", "startUsingNetworkFeature status: " + status); diff --git a/src/org/thoughtcrime/securesms/mms/MmsRadioException.java b/src/org/thoughtcrime/securesms/mms/MmsRadioException.java index 9919541b8..dfac78dee 100644 --- a/src/org/thoughtcrime/securesms/mms/MmsRadioException.java +++ b/src/org/thoughtcrime/securesms/mms/MmsRadioException.java @@ -4,4 +4,8 @@ public class MmsRadioException extends Throwable { public MmsRadioException(String s) { super(s); } + + public MmsRadioException(Exception e) { + super(e); + } } diff --git a/src/org/thoughtcrime/securesms/mms/RoundedCorners.java b/src/org/thoughtcrime/securesms/mms/RoundedCorners.java deleted file mode 100644 index 4b1117010..000000000 --- a/src/org/thoughtcrime/securesms/mms/RoundedCorners.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.thoughtcrime.securesms.mms; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.RectF; -import android.graphics.Shader.TileMode; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; -import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; -import com.bumptech.glide.load.resource.bitmap.TransformationUtils; - -import org.thoughtcrime.securesms.util.ResUtil; - -public class RoundedCorners extends BitmapTransformation { - private final boolean crop; - private final int radius; - private final int colorHint; - - public RoundedCorners(@NonNull Context context, boolean crop, int radius, int colorHint) { - super(context); - this.crop = crop; - this.radius = radius; - this.colorHint = colorHint; - } - - public RoundedCorners(@NonNull Context context, int radius) { - this(context, true, radius, ResUtil.getColor(context, android.R.attr.windowBackground)); - } - - @Override protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, - int outHeight) - { - final Bitmap toRound = crop ? centerCrop(pool, toTransform, outWidth, outHeight) - : fitCenter(pool, toTransform, outWidth, outHeight); - - final Bitmap rounded = round(pool, toRound); - - if (toRound != null && toRound != rounded && toRound != toTransform && !pool.put(toRound)) { - toRound.recycle(); - } - - return rounded; - } - - private Bitmap centerCrop(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { - final Bitmap toReuse = pool.get(outWidth, outHeight, getSafeConfig(toTransform)); - final Bitmap transformed = TransformationUtils.centerCrop(toReuse, toTransform, outWidth, outHeight); - - if (toReuse != null && toReuse != transformed && !pool.put(toReuse)) { - toReuse.recycle(); - } - - return transformed; - } - - private Bitmap fitCenter(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { - return TransformationUtils.fitCenter(toTransform, pool, outWidth, outHeight); - } - - private Bitmap round(@NonNull BitmapPool pool, @Nullable Bitmap toRound) { - if (toRound == null) { - return null; - } - - Bitmap result = pool.get(toRound.getWidth(), toRound.getHeight(), getSafeConfig(toRound)); - - if (result == null) { - result = Bitmap.createBitmap(toRound.getWidth(), toRound.getHeight(), getSafeConfig(toRound)); - } - - Canvas canvas = new Canvas(result); - - if (Config.RGB_565.equals(result.getConfig())) { - Paint cornerPaint = new Paint(); - cornerPaint.setColor(colorHint); - - canvas.drawRect(0, 0, radius, radius, cornerPaint); - canvas.drawRect(0, toRound.getHeight() - radius, radius, toRound.getHeight(), cornerPaint); - canvas.drawRect(toRound.getWidth() - radius, 0, toRound.getWidth(), radius, cornerPaint); - canvas.drawRect(toRound.getWidth() - radius, toRound.getHeight() - radius, toRound.getWidth(), toRound.getHeight(), cornerPaint); - } - - Paint shaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - shaderPaint.setShader(new BitmapShader(toRound, TileMode.CLAMP, TileMode.CLAMP)); - - canvas.drawRoundRect(new RectF(0, 0, toRound.getWidth(), toRound.getHeight()), radius, radius, shaderPaint); - - return result; - } - - private static Bitmap.Config getSafeConfig(Bitmap bitmap) { - return bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888; - } - - @Override - public String getId() { - return RoundedCorners.class.getCanonicalName(); - } -} diff --git a/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java b/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java similarity index 54% rename from src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java rename to src/org/thoughtcrime/securesms/mms/SignalGlideModule.java index 9ce5b075d..495d659ae 100644 --- a/src/org/thoughtcrime/securesms/mms/TextSecureGlideModule.java +++ b/src/org/thoughtcrime/securesms/mms/SignalGlideModule.java @@ -1,13 +1,16 @@ package org.thoughtcrime.securesms.mms; import android.content.Context; +import android.util.Log; import com.bumptech.glide.Glide; import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; 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 com.bumptech.glide.module.AppGlideModule; import org.thoughtcrime.securesms.glide.OkHttpUrlLoader; import org.thoughtcrime.securesms.mms.AttachmentStreamUriLoader.AttachmentModel; @@ -18,19 +21,27 @@ import org.thoughtcrime.securesms.profiles.AvatarPhotoUriLoader.AvatarPhotoUri; import java.io.InputStream; -public class TextSecureGlideModule implements GlideModule { +@GlideModule +public class SignalGlideModule extends AppGlideModule { + + @Override + public boolean isManifestParsingEnabled() { + return false; + } + @Override public void applyOptions(Context context, GlideBuilder builder) { + builder.setLogLevel(Log.ERROR); // builder.setDiskCache(new NoopDiskCacheFactory()); } @Override - 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()); - glide.register(AvatarPhotoUri.class, InputStream.class, new AvatarPhotoUriLoader.Factory()); + public void registerComponents(Context context, Glide glide, Registry registry) { + registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); + registry.append(ContactPhotoUri.class, InputStream.class, new ContactPhotoUriLoader.Factory(context)); + registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory()); + registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory()); + registry.append(AvatarPhotoUri.class, InputStream.class, new AvatarPhotoUriLoader.Factory(context)); } 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 bd33f778a..653b2f837 100644 --- a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java @@ -14,7 +14,6 @@ import android.support.v4.app.NotificationCompat.Action; import android.support.v4.app.RemoteInput; import android.text.SpannableStringBuilder; -import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.R; @@ -22,6 +21,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColors; import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoFactory; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; @@ -225,12 +225,12 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil @SuppressWarnings("ConstantConditions") Uri uri = slideDeck.getThumbnailSlide().getThumbnailUri(); - return Glide.with(context) - .load(new DecryptableStreamUriLoader.DecryptableUri(masterSecret, uri)) - .asBitmap() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(500, 500) - .get(); + return GlideApp.with(context) + .asBitmap() + .load(new DecryptableStreamUriLoader.DecryptableUri(masterSecret, uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .submit(500, 500) + .get(); } catch (InterruptedException | ExecutionException e) { throw new AssertionError(e); } diff --git a/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriFetcher.java b/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriFetcher.java index b0818e7c5..b028e3e77 100644 --- a/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriFetcher.java +++ b/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriFetcher.java @@ -5,32 +5,34 @@ import android.content.Context; import android.support.annotation.NonNull; import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import org.thoughtcrime.securesms.database.Address; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -public class AvatarPhotoUriFetcher implements DataFetcher { +class AvatarPhotoUriFetcher implements DataFetcher { private final Context context; private final Address address; private InputStream inputStream; - public AvatarPhotoUriFetcher(@NonNull Context context, @NonNull Address address) { + AvatarPhotoUriFetcher(@NonNull Context context, @NonNull Address address) { this.context = context.getApplicationContext(); this.address = address; } @Override - public InputStream loadData(Priority priority) throws IOException { - inputStream = AvatarHelper.getInputStreamFor(context, address); - return inputStream; + public void loadData(Priority priority, DataCallback callback) { + try { + inputStream = AvatarHelper.getInputStreamFor(context, address); + callback.onDataReady(inputStream); + } catch (IOException e) { + callback.onLoadFailed(e); + } } @Override @@ -40,13 +42,20 @@ public class AvatarPhotoUriFetcher implements DataFetcher { } catch (IOException e) {} } - @Override - public String getId() { - return address.serialize(); - } - @Override public void cancel() { } + + @NonNull + @Override + public Class getDataClass() { + return InputStream.class; + } + + @NonNull + @Override + public DataSource getDataSource() { + return DataSource.LOCAL; + } } diff --git a/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriLoader.java b/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriLoader.java index 3b301ddff..9209f80c3 100644 --- a/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriLoader.java +++ b/src/org/thoughtcrime/securesms/profiles/AvatarPhotoUriLoader.java @@ -3,24 +3,48 @@ package org.thoughtcrime.securesms.profiles; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; -import com.bumptech.glide.load.data.DataFetcher; -import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.Key; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; -import com.bumptech.glide.load.model.stream.StreamModelLoader; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; import org.thoughtcrime.securesms.database.Address; import java.io.InputStream; +import java.security.MessageDigest; -public class AvatarPhotoUriLoader implements StreamModelLoader { +public class AvatarPhotoUriLoader implements ModelLoader { private final Context context; + private AvatarPhotoUriLoader(Context context) { + this.context = context; + } + + @Nullable + @Override + public LoadData buildLoadData(AvatarPhotoUri avatarPhotoUri, int width, int height, Options options) { + return new LoadData<>(avatarPhotoUri, new AvatarPhotoUriFetcher(context, avatarPhotoUri.address)); + } + + @Override + public boolean handles(AvatarPhotoUri avatarPhotoUri) { + return true; + } + public static class Factory implements ModelLoaderFactory { + private final Context context; + + public Factory(Context context) { + this.context = context.getApplicationContext(); + } + @Override - public StreamModelLoader build(Context context, GenericLoaderFactory factories) { + public ModelLoader build(MultiModelLoaderFactory multiFactory) { return new AvatarPhotoUriLoader(context); } @@ -28,21 +52,29 @@ public class AvatarPhotoUriLoader implements StreamModelLoader getResourceFetcher(AvatarPhotoUri model, int width, int height) { - return new AvatarPhotoUriFetcher(context, model.address); - } - - public static class AvatarPhotoUri { + public static class AvatarPhotoUri implements Key { public @NonNull Address address; public AvatarPhotoUri(@NonNull Address address) { this.address = address; } + + @Override + public void updateDiskCacheKey(MessageDigest messageDigest) { + messageDigest.update(address.serialize().getBytes()); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof AvatarPhotoUri)) return false; + + return this.address.equals(((AvatarPhotoUri)other).address); + } + + @Override + public int hashCode() { + return address.hashCode(); + } } } diff --git a/src/org/thoughtcrime/securesms/scribbles/StickerSelectFragment.java b/src/org/thoughtcrime/securesms/scribbles/StickerSelectFragment.java index e297cdcdd..a02975755 100644 --- a/src/org/thoughtcrime/securesms/scribbles/StickerSelectFragment.java +++ b/src/org/thoughtcrime/securesms/scribbles/StickerSelectFragment.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2016 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify @@ -31,10 +31,10 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.mms.GlideApp; public class StickerSelectFragment extends Fragment implements LoaderManager.LoaderCallbacks { @@ -58,7 +58,7 @@ public class StickerSelectFragment extends Fragment implements LoaderManager.Loa @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.scribble_select_sticker_fragment, container, false); - this.recyclerView = (RecyclerView)view.findViewById(R.id.stickers_recycler_view); + this.recyclerView = view.findViewById(R.id.stickers_recycler_view); return view; } @@ -113,10 +113,10 @@ public class StickerSelectFragment extends Fragment implements LoaderManager.Loa public void onBindViewHolder(StickerViewHolder holder, int position) { holder.fileName = stickerFiles[position]; - Glide.with(context) - .load(Uri.parse("file:///android_asset/" + holder.fileName)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .into(holder.image); + GlideApp.with(context) + .load(Uri.parse("file:///android_asset/" + holder.fileName)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .into(holder.image); } @Override @@ -127,7 +127,7 @@ public class StickerSelectFragment extends Fragment implements LoaderManager.Loa @Override public void onViewRecycled(StickerViewHolder holder) { super.onViewRecycled(holder); - Glide.clear(holder.image); + GlideApp.with(context).clear(holder.image); } private void onStickerSelected(String fileName) { @@ -141,22 +141,19 @@ public class StickerSelectFragment extends Fragment implements LoaderManager.Loa StickerViewHolder(View itemView) { super(itemView); - image = (ImageView) itemView.findViewById(R.id.sticker_image); - itemView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - int pos = getAdapterPosition(); - if (pos >= 0) { - onStickerSelected(fileName); - } + image = itemView.findViewById(R.id.sticker_image); + itemView.setOnClickListener(view -> { + int pos = getAdapterPosition(); + if (pos >= 0) { + onStickerSelected(fileName); } }); } } } - public interface StickerSelectionListener { - public void onStickerSelected(String name); + interface StickerSelectionListener { + void onStickerSelected(String name); } diff --git a/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java b/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java index 960f91ac7..1363c8305 100644 --- a/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java +++ b/src/org/thoughtcrime/securesms/scribbles/widget/ScribbleView.java @@ -30,13 +30,13 @@ import android.util.Log; import android.widget.FrameLayout; import android.widget.ImageView; -import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.Target; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity; import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity; import org.thoughtcrime.securesms.util.Util; @@ -81,11 +81,11 @@ public class ScribbleView extends FrameLayout { this.imageUri = uri; this.masterSecret = masterSecret; - Glide.with(getContext()) - .load(new DecryptableUri(masterSecret, uri)) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .fitCenter() - .into(imageView); + GlideApp.with(getContext()) + .load(new DecryptableUri(masterSecret, uri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .fitCenter() + .into(imageView); } public @NonNull ListenableFuture getRenderedImage() { @@ -110,13 +110,13 @@ public class ScribbleView extends FrameLayout { height = 768; } - return Glide.with(context) - .load(new DecryptableUri(masterSecret, imageUri)) - .asBitmap() - .diskCacheStrategy(DiskCacheStrategy.NONE) - .skipMemoryCache(true) - .into(width, height) - .get(); + return GlideApp.with(context) + .asBitmap() + .load(new DecryptableUri(masterSecret, imageUri)) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .into(width, height) + .get(); } catch (InterruptedException | ExecutionException e) { Log.w(TAG, e); return null; diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index 62f8a5491..9319b38cd 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -15,14 +15,9 @@ import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; -import com.bumptech.glide.Glide; -import com.bumptech.glide.Priority; -import com.bumptech.glide.load.DecodeFormat; -import com.bumptech.glide.load.engine.Resource; -import com.bumptech.glide.load.resource.bitmap.BitmapResource; -import com.bumptech.glide.load.resource.bitmap.Downsampler; -import com.bumptech.glide.load.resource.bitmap.FitCenter; +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.MediaConstraints; import java.io.BufferedInputStream; @@ -30,6 +25,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import javax.microedition.khronos.egl.EGL10; @@ -49,90 +45,80 @@ public class BitmapUtil { public static byte[] createScaledBytes(Context context, T model, MediaConstraints constraints) throws BitmapDecodingException { - int quality = MAX_COMPRESSION_QUALITY; - int attempts = 0; - byte[] bytes; - - Bitmap scaledBitmap = Downsampler.AT_MOST.decode(getInputStreamForModel(context, model), - Glide.get(context).getBitmapPool(), - constraints.getImageMaxWidth(context), - constraints.getImageMaxHeight(context), - DecodeFormat.PREFER_RGB_565); - - if (scaledBitmap == null) { - throw new BitmapDecodingException("Unable to decode image"); - } - try { - do { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - scaledBitmap.compress(CompressFormat.JPEG, quality, baos); - bytes = baos.toByteArray(); + int quality = MAX_COMPRESSION_QUALITY; + int attempts = 0; + byte[] bytes; - Log.w(TAG, "iteration with quality " + quality + " size " + (bytes.length / 1024) + "kb"); - if (quality == MIN_COMPRESSION_QUALITY) break; + Bitmap scaledBitmap = GlideApp.with(context) + .asBitmap() + .load(model) + .downsample(DownsampleStrategy.AT_MOST) + .submit(constraints.getImageMaxWidth(context), + constraints.getImageMaxWidth(context)) + .get(); + + if (scaledBitmap == null) { + throw new BitmapDecodingException("Unable to decode image"); + } - int nextQuality = (int)Math.floor(quality * Math.sqrt((double)constraints.getImageMaxSize(context) / bytes.length)); - if (quality - nextQuality < MIN_COMPRESSION_QUALITY_DECREASE) { - nextQuality = quality - MIN_COMPRESSION_QUALITY_DECREASE; + try { + do { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + scaledBitmap.compress(CompressFormat.JPEG, quality, baos); + bytes = baos.toByteArray(); + + Log.w(TAG, "iteration with quality " + quality + " size " + (bytes.length / 1024) + "kb"); + if (quality == MIN_COMPRESSION_QUALITY) break; + + int nextQuality = (int)Math.floor(quality * Math.sqrt((double)constraints.getImageMaxSize(context) / bytes.length)); + if (quality - nextQuality < MIN_COMPRESSION_QUALITY_DECREASE) { + nextQuality = quality - MIN_COMPRESSION_QUALITY_DECREASE; + } + quality = Math.max(nextQuality, MIN_COMPRESSION_QUALITY); } - quality = Math.max(nextQuality, MIN_COMPRESSION_QUALITY); + while (bytes.length > constraints.getImageMaxSize(context) && attempts++ < MAX_COMPRESSION_ATTEMPTS); + if (bytes.length > constraints.getImageMaxSize(context)) { + throw new BitmapDecodingException("Unable to scale image below: " + bytes.length); + } + Log.w(TAG, "createScaledBytes(" + model.toString() + ") -> quality " + Math.min(quality, MAX_COMPRESSION_QUALITY) + ", " + attempts + " attempt(s)"); + return bytes; + } finally { + if (scaledBitmap != null) scaledBitmap.recycle(); } - while (bytes.length > constraints.getImageMaxSize(context) && attempts++ < MAX_COMPRESSION_ATTEMPTS); - if (bytes.length > constraints.getImageMaxSize(context)) { - throw new BitmapDecodingException("Unable to scale image below: " + bytes.length); - } - Log.w(TAG, "createScaledBytes(" + model.toString() + ") -> quality " + Math.min(quality, MAX_COMPRESSION_QUALITY) + ", " + attempts + " attempt(s)"); - return bytes; - } finally { - if (scaledBitmap != null) scaledBitmap.recycle(); + } catch (InterruptedException | ExecutionException e) { + throw new BitmapDecodingException(e); } } public static Bitmap createScaledBitmap(Context context, T model, int maxWidth, int maxHeight) throws BitmapDecodingException - { - final Pair dimensions = getDimensions(getInputStreamForModel(context, model)); - final Pair clamped = clampDimensions(dimensions.first, dimensions.second, - maxWidth, maxHeight); - return createScaledBitmapInto(context, model, clamped.first, clamped.second); - } - - private static InputStream getInputStreamForModel(Context context, T model) - throws BitmapDecodingException { try { - return Glide.buildStreamModelLoader(model, context) - .getResourceFetcher(model, -1, -1) - .loadData(Priority.NORMAL); - } catch (Exception e) { + return GlideApp.with(context) + .asBitmap() + .load(model) + .downsample(DownsampleStrategy.AT_MOST) + .submit(maxWidth, maxHeight) + .get(); + } catch (InterruptedException | ExecutionException e) { throw new BitmapDecodingException(e); } } - private static Bitmap createScaledBitmapInto(Context context, T model, int width, int height) - throws BitmapDecodingException - { - final Bitmap rough = Downsampler.AT_LEAST.decode(getInputStreamForModel(context, model), - Glide.get(context).getBitmapPool(), - width, height, - DecodeFormat.PREFER_RGB_565); - - final Resource resource = BitmapResource.obtain(rough, Glide.get(context).getBitmapPool()); - final Resource result = new FitCenter(context).transform(resource, width, height); - - if (result == null) { - throw new BitmapDecodingException("unable to transform Bitmap"); - } - return result.get(); - } - public static Bitmap createScaledBitmap(Context context, T model, float scale) throws BitmapDecodingException { - Pair dimens = getDimensions(getInputStreamForModel(context, model)); - return createScaledBitmapInto(context, model, - (int)(dimens.first * scale), (int)(dimens.second * scale)); + try { + return GlideApp.with(context) + .asBitmap() + .load(model) + .sizeMultiplier(scale) + .submit() + .get(); + } catch (InterruptedException | ExecutionException e) { + throw new BitmapDecodingException(e); + } } private static BitmapFactory.Options getImageDimensions(InputStream inputStream) @@ -253,27 +239,6 @@ public class BitmapUtil { return output; } - private static Pair clampDimensions(int inWidth, int inHeight, int maxWidth, int maxHeight) { - if (inWidth > maxWidth || inHeight > maxHeight) { - final float aspectWidth, aspectHeight; - - if (inWidth == 0 || inHeight == 0) { - aspectWidth = maxWidth; - aspectHeight = maxHeight; - } else if (inWidth >= inHeight) { - aspectWidth = maxWidth; - aspectHeight = (aspectWidth / inWidth) * inHeight; - } else { - aspectHeight = maxHeight; - aspectWidth = (aspectHeight / inHeight) * inWidth; - } - - return new Pair<>(Math.round(aspectWidth), Math.round(aspectHeight)); - } else { - return new Pair<>(inWidth, inHeight); - } - } - public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) { final AtomicBoolean created = new AtomicBoolean(false); final Bitmap[] result = new Bitmap[1]; diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java index 6a6da5de9..72ff87647 100644 --- a/src/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java @@ -11,8 +11,6 @@ import android.text.TextUtils; import android.util.Log; import android.webkit.MimeTypeMap; -import com.bumptech.glide.Glide; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.crypto.MasterSecret; @@ -20,6 +18,7 @@ import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DocumentSlide; import org.thoughtcrime.securesms.mms.GifSlide; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.MmsSlide; import org.thoughtcrime.securesms.mms.PartAuthority; @@ -67,12 +66,12 @@ public class MediaUtil { { try { int maxSize = context.getResources().getDimensionPixelSize(R.dimen.media_bubble_height); - return Glide.with(context) - .load(new DecryptableUri(masterSecret, uri)) - .asBitmap() - .centerCrop() - .into(maxSize, maxSize) - .get(); + return GlideApp.with(context) + .asBitmap() + .load(new DecryptableUri(masterSecret, uri)) + .centerCrop() + .into(maxSize, maxSize) + .get(); } catch (InterruptedException | ExecutionException e) { Log.w(TAG, e); throw new BitmapDecodingException(e);