diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 44275e9d6a..9ec840247d 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 e96c363d3c..0e4ed3c78f 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 718eb9da93..f0d673934f 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 0000000000..3208779f85
Binary files /dev/null and b/res/drawable-hdpi/ic_dashboard_white_24dp.png differ
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 0000000000..17b6ed692e
Binary files /dev/null and b/res/drawable-hdpi/ic_gif_white_36dp.png differ
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 0000000000..857becfc24
Binary files /dev/null and b/res/drawable-hdpi/ic_view_stream_white_24dp.png differ
diff --git a/res/drawable-hdpi/poweredby_giphy.png b/res/drawable-hdpi/poweredby_giphy.png
new file mode 100644
index 0000000000..df5e4f06e1
Binary files /dev/null and b/res/drawable-hdpi/poweredby_giphy.png differ
diff --git a/res/drawable-mdpi/ic_dashboard_white_24dp.png b/res/drawable-mdpi/ic_dashboard_white_24dp.png
new file mode 100644
index 0000000000..1614347a82
Binary files /dev/null and b/res/drawable-mdpi/ic_dashboard_white_24dp.png differ
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 0000000000..0978a141a9
Binary files /dev/null and b/res/drawable-mdpi/ic_gif_white_36dp.png differ
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 0000000000..a0a663458a
Binary files /dev/null and b/res/drawable-mdpi/ic_view_stream_white_24dp.png differ
diff --git a/res/drawable-mdpi/poweredby_giphy.png b/res/drawable-mdpi/poweredby_giphy.png
new file mode 100644
index 0000000000..0cee864a36
Binary files /dev/null and b/res/drawable-mdpi/poweredby_giphy.png differ
diff --git a/res/drawable-xhdpi/ic_dashboard_white_24dp.png b/res/drawable-xhdpi/ic_dashboard_white_24dp.png
new file mode 100644
index 0000000000..da1a5852c9
Binary files /dev/null and b/res/drawable-xhdpi/ic_dashboard_white_24dp.png differ
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 0000000000..acdb6d0b90
Binary files /dev/null and b/res/drawable-xhdpi/ic_gif_white_36dp.png differ
diff --git a/res/drawable-xhdpi/ic_view_stream_white_24dp.png b/res/drawable-xhdpi/ic_view_stream_white_24dp.png
new file mode 100644
index 0000000000..9e640ae559
Binary files /dev/null and b/res/drawable-xhdpi/ic_view_stream_white_24dp.png differ
diff --git a/res/drawable-xhdpi/poweredby_giphy.png b/res/drawable-xhdpi/poweredby_giphy.png
new file mode 100644
index 0000000000..09b38f190f
Binary files /dev/null and b/res/drawable-xhdpi/poweredby_giphy.png differ
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 0000000000..471aa0db16
Binary files /dev/null and b/res/drawable-xxhdpi/ic_dashboard_white_24dp.png differ
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 0000000000..ae117123cf
Binary files /dev/null and b/res/drawable-xxhdpi/ic_gif_white_36dp.png differ
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 0000000000..b8656197f4
Binary files /dev/null and b/res/drawable-xxhdpi/ic_view_stream_white_24dp.png differ
diff --git a/res/drawable-xxhdpi/poweredby_giphy.png b/res/drawable-xxhdpi/poweredby_giphy.png
new file mode 100644
index 0000000000..aca5bf6614
Binary files /dev/null and b/res/drawable-xxhdpi/poweredby_giphy.png differ
diff --git a/res/drawable-xxxhdpi/ic_dashboard_white_24dp.png b/res/drawable-xxxhdpi/ic_dashboard_white_24dp.png
new file mode 100644
index 0000000000..817f274cfa
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_dashboard_white_24dp.png differ
diff --git a/res/drawable-xxxhdpi/ic_gif_white_36dp.png b/res/drawable-xxxhdpi/ic_gif_white_36dp.png
new file mode 100644
index 0000000000..0ffbdb3554
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_gif_white_36dp.png differ
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 0000000000..1e679697e9
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_view_stream_white_24dp.png differ
diff --git a/res/drawable-xxxhdpi/poweredby_giphy.png b/res/drawable-xxxhdpi/poweredby_giphy.png
new file mode 100644
index 0000000000..f266ba6527
Binary files /dev/null and b/res/drawable-xxxhdpi/poweredby_giphy.png differ
diff --git a/res/layout/attachment_type_selector.xml b/res/layout/attachment_type_selector.xml
index 9b912d0122..58723e9960 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 0000000000..1e5d1de533
--- /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 0000000000..9c364a25ad
--- /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 0000000000..eed7e09e1a
--- /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 d25ae0b6fd..995f4eb1be 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 1a8e487d25..3bb9dcba67 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;
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 class ConversationActivity extends PassphraseRequiredActionBarActivity
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 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
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 0898f51e62..1d6c017c9c 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 class AttachmentTypeSelector extends PopupWindow {
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 class AttachmentTypeSelector extends PopupWindow {
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 class AttachmentTypeSelector extends PopupWindow {
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 e75a8b8d34..29799b548e 100644
--- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java
+++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java
@@ -18,6 +18,7 @@ import android.widget.ImageView;
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 class ThumbnailView extends FrameLayout {
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 @@ public class ThumbnailView extends FrameLayout {
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 1caf6d4bf3..03947b42e0 100644
--- a/src/org/thoughtcrime/securesms/components/ZoomingImageView.java
+++ b/src/org/thoughtcrime/securesms/components/ZoomingImageView.java
@@ -7,6 +7,7 @@ import android.util.AttributeSet;
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 class ZoomingImageView extends ImageView {
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 833fa4ed0f..ccb54d433e 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.support.annotation.Nullable;
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 class ContactPhotoFactory {
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 0000000000..b804e97504
--- /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 0000000000..4ab61b5715
--- /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 0000000000..e832d23bf4
--- /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 0000000000..3f6d02fe15
--- /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 0000000000..e7c64bd487
--- /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 0000000000..9290fc2dab
--- /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 0000000000..a1ce36e23d
--- /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 0000000000..11ae145fb7
--- /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 0000000000..c940937b68
--- /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 0000000000..56cc21d288
--- /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 0000000000..d90065ced0
--- /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 0000000000..ea6b845972
--- /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 0000000000..27a03b326b
--- /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 0000000000..ba82f7e0da
--- /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 0000000000..e2a62ec17d
--- /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 0000000000..fd4fbae5d6
--- /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 0000000000..948b765f7b
--- /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 e5e0c401f6..e6e9f9b19d 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.ThumbnailView;
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 class AttachmentManager {
}
}
+ 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 d7f81ba33f..acb59a9e01 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.Glide;
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 @@ import java.io.InputStream;
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 class TextSecureGlideModule implements GlideModule {
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 093269eb0a..3987c8dd14 100644
--- a/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
+++ b/src/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
@@ -16,6 +16,7 @@ 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;
import org.thoughtcrime.securesms.crypto.MasterSecret;
@@ -200,6 +201,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
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 284d0a9e1b..4d4deef19b 100644
--- a/src/org/thoughtcrime/securesms/util/JsonUtils.java
+++ b/src/org/thoughtcrime/securesms/util/JsonUtils.java
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.Reader;
public class JsonUtils {
@@ -23,7 +24,11 @@ public class JsonUtils {
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 f826de5179..6a776ba3f4 100644
--- a/src/org/thoughtcrime/securesms/util/Util.java
+++ b/src/org/thoughtcrime/securesms/util/Util.java
@@ -384,6 +384,14 @@ public class Util {
}
}
+ 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 59f3a54acb..b943a26fb9 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 447faa9812..818c4f5f17 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.Future;
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 class SettableFuture implements Future, ListenableFuture {
this.result = result;
this.completed = true;
+ notifyAll();
}
notifyAllListeners();
@@ -54,6 +55,7 @@ public class SettableFuture implements Future, ListenableFuture {
this.exception = throwable;
this.completed = true;
+ notifyAll();
}
notifyAllListeners();