Giphy integration
// FREEBIE
|
@ -266,6 +266,11 @@
|
|||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".giph.ui.GiphyActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
|
|
@ -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\""
|
||||
|
|
|
@ -7,3 +7,7 @@
|
|||
-keep public class * extends android.support.v4.view.ActionProvider {
|
||||
public <init>(android.content.Context);
|
||||
}
|
||||
|
||||
-keepattributes *Annotation*
|
||||
-keep public class * extends android.support.design.widget.CoordinatorLayout.Behavior { *; }
|
||||
-keep public class * extends android.support.design.widget.ViewOffsetBehavior { *; }
|
||||
|
|
BIN
res/drawable-hdpi/ic_dashboard_white_24dp.png
Normal file
After Width: | Height: | Size: 130 B |
BIN
res/drawable-hdpi/ic_gif_white_36dp.png
Normal file
After Width: | Height: | Size: 243 B |
BIN
res/drawable-hdpi/ic_view_stream_white_24dp.png
Normal file
After Width: | Height: | Size: 105 B |
BIN
res/drawable-hdpi/poweredby_giphy.png
Normal file
After Width: | Height: | Size: 4 KiB |
BIN
res/drawable-mdpi/ic_dashboard_white_24dp.png
Normal file
After Width: | Height: | Size: 94 B |
BIN
res/drawable-mdpi/ic_gif_white_36dp.png
Normal file
After Width: | Height: | Size: 196 B |
BIN
res/drawable-mdpi/ic_view_stream_white_24dp.png
Normal file
After Width: | Height: | Size: 82 B |
BIN
res/drawable-mdpi/poweredby_giphy.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
res/drawable-xhdpi/ic_dashboard_white_24dp.png
Normal file
After Width: | Height: | Size: 104 B |
BIN
res/drawable-xhdpi/ic_gif_white_36dp.png
Normal file
After Width: | Height: | Size: 213 B |
BIN
res/drawable-xhdpi/ic_view_stream_white_24dp.png
Normal file
After Width: | Height: | Size: 92 B |
BIN
res/drawable-xhdpi/poweredby_giphy.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
res/drawable-xxhdpi/ic_dashboard_white_24dp.png
Normal file
After Width: | Height: | Size: 109 B |
BIN
res/drawable-xxhdpi/ic_gif_white_36dp.png
Normal file
After Width: | Height: | Size: 312 B |
BIN
res/drawable-xxhdpi/ic_view_stream_white_24dp.png
Normal file
After Width: | Height: | Size: 94 B |
BIN
res/drawable-xxhdpi/poweredby_giphy.png
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
res/drawable-xxxhdpi/ic_dashboard_white_24dp.png
Normal file
After Width: | Height: | Size: 110 B |
BIN
res/drawable-xxxhdpi/ic_gif_white_36dp.png
Normal file
After Width: | Height: | Size: 308 B |
BIN
res/drawable-xxxhdpi/ic_view_stream_white_24dp.png
Normal file
After Width: | Height: | Size: 98 B |
BIN
res/drawable-xxxhdpi/poweredby_giphy.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
|
@ -169,6 +169,21 @@
|
|||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.thoughtcrime.securesms.components.CircleColorImageView
|
||||
android:id="@+id/giphy_button"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:src="@drawable/ic_gif_white_36dp"
|
||||
android:scaleType="center"
|
||||
android:elevation="4dp"
|
||||
app:circleColor="@color/cyan_400"/>
|
||||
|
||||
<TextView android:layout_marginTop="10dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/AttachmentTypeLabel"
|
||||
android:text="GIF"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
|
|
52
res/layout/giphy_activity.xml
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_above="@+id/giphy_logo"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="?attr/actionBarStyle"
|
||||
android:background="?attr/colorPrimary">
|
||||
|
||||
<org.thoughtcrime.securesms.giph.ui.GiphyActivityToolbar
|
||||
android:id="@+id/giphy_toolbar"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/actionBarStyle"
|
||||
app:layout_scrollFlags="scroll|enterAlways"/>
|
||||
|
||||
<android.support.design.widget.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="horizontal"/>
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<android.support.v4.view.ViewPager
|
||||
android:id="@+id/giphy_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
|
||||
<ImageView android:id="@+id/giphy_logo"
|
||||
android:src="@drawable/poweredby_giphy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:padding="10dp"
|
||||
android:background="@color/black"/>
|
||||
|
||||
</RelativeLayout>
|
86
res/layout/giphy_activity_toolbar.xml
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView android:id="@+id/action_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_search_white_24dp" />
|
||||
|
||||
<LinearLayout android:id="@+id/toggle_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<EditText android:id="@+id/search_view"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="0px"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:hint="@string/giphy_activity_toolbar__search_gifs_and_stickers"
|
||||
android:textColor="@color/white"
|
||||
android:textColorHint="@color/white"
|
||||
android:textCursorDrawable="@null"
|
||||
android:maxLines="1"
|
||||
android:imeOptions="actionSearch"
|
||||
android:background="@android:color/transparent"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"/>
|
||||
|
||||
<ImageView android:id="@+id/search_clear"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:visibility="invisible"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_clear_white_24dp" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
android:id="@+id/button_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView android:id="@+id/view_grid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:visibility="visible"
|
||||
android:focusable="true"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_dashboard_white_24dp" />
|
||||
|
||||
<ImageView android:id="@+id/view_stream"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="gone"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_view_stream_white_24dp" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
27
res/layout/giphy_fragment.xml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/giphy_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical"/>
|
||||
|
||||
<ProgressBar android:id="@+id/loading_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="visible"
|
||||
android:indeterminate="true"/>
|
||||
|
||||
<TextView android:id="@+id/no_results"
|
||||
android:text="@string/giphy_fragment__no_results_found"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
</FrameLayout>
|
22
res/layout/giphy_thumbnail.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<org.thoughtcrime.securesms.giph.ui.AspectRatioImageView
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitXY" />
|
||||
|
||||
<ProgressBar android:id="@+id/gif_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"/>
|
||||
|
||||
</FrameLayout>
|
|
@ -269,6 +269,14 @@
|
|||
<string name="GcmRefreshJob_Permanent_Signal_communication_failure">Permanent Signal communication failure!</string>
|
||||
<string name="GcmRefreshJob_Signal_was_unable_to_register_with_Google_Play_Services">Signal was unable to register with Google Play Services. Signal messages and calls have been disabled, please try re-registering in Settings > Advanced.</string>
|
||||
|
||||
|
||||
<!-- GiphyActivity -->
|
||||
<string name="GiphyActivity_error_while_retrieving_full_resolution_gif">Error while retrieving full resolution GiF...</string>
|
||||
|
||||
<!-- GiphyFragmentPageAdapter -->
|
||||
<string name="GiphyFragmentPagerAdapter_gifs">GIFs</string>
|
||||
<string name="GiphyFragmentPagerAdapter_stickers">Stickers</string>
|
||||
|
||||
<!-- GroupCreateActivity -->
|
||||
<string name="GroupCreateActivity_actionbar_title">New group</string>
|
||||
<string name="GroupCreateActivity_actionbar_update_title">Update group</string>
|
||||
|
@ -751,6 +759,13 @@
|
|||
|
||||
<string name="expiration_weeks_abbreviated">%dw</string>
|
||||
|
||||
<!-- giphy_activity -->
|
||||
<string name="giphy_activity_toolbar__search_gifs_and_stickers">Search GIFs and stickers</string>
|
||||
|
||||
<!-- giphy_fragment -->
|
||||
<string name="giphy_fragment__no_results_found">No results found.</string>
|
||||
|
||||
|
||||
<!-- log_submit_activity -->
|
||||
<string name="log_submit_activity__log_fetch_failed">Could not read the log on your device. You can still use ADB to get a debug log instead.</string>
|
||||
<string name="log_submit_activity__thanks">Thanks for your help!</string>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<DecryptableUri> builder = Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()))
|
||||
.crossFade()
|
||||
.transform(new RoundedCorners(getContext(), true, radius, backgroundColorHint));
|
||||
DrawableRequestBuilder<DecryptableUri> 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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
62
src/org/thoughtcrime/securesms/giph/model/GiphyImage.java
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
17
src/org/thoughtcrime/securesms/giph/model/GiphyResponse.java
Normal file
|
@ -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<GiphyImage> data;
|
||||
|
||||
public List<GiphyImage> getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
23
src/org/thoughtcrime/securesms/giph/net/GiphyGifLoader.java
Normal file
|
@ -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";
|
||||
}
|
||||
}
|
70
src/org/thoughtcrime/securesms/giph/net/GiphyLoader.java
Normal file
|
@ -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<List<GiphyImage>> {
|
||||
|
||||
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<GiphyImage> loadInBackground() {
|
||||
return loadPage(0);
|
||||
}
|
||||
|
||||
public List<GiphyImage> 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();
|
||||
}
|
|
@ -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<Proxy> EMPTY = new ArrayList<>(1);
|
||||
private volatile List<Proxy> GIPHY = null;
|
||||
|
||||
public GiphyProxySelector() {
|
||||
EMPTY.add(Proxy.NO_PROXY);
|
||||
|
||||
if (Util.isMainThread()) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
synchronized (GiphyProxySelector.this) {
|
||||
initializeGiphyProxy();
|
||||
GiphyProxySelector.this.notifyAll();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
} else {
|
||||
initializeGiphyProxy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Proxy> 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<Proxy>(1) {{
|
||||
add(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(BuildConfig.GIPHY_PROXY_HOST,
|
||||
BuildConfig.GIPHY_PROXY_PORT)));
|
||||
}};
|
||||
}
|
||||
|
||||
private List<Proxy> getOrCreateGiphyProxy() {
|
||||
if (GIPHY == null) {
|
||||
synchronized (this) {
|
||||
while (GIPHY == null) Util.wait(this, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return GIPHY;
|
||||
}
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
119
src/org/thoughtcrime/securesms/giph/ui/AspectRatioImageView.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
163
src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
Normal file
|
@ -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<Void, Void, Uri>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
174
src/org/thoughtcrime/securesms/giph/ui/GiphyActivityToolbar.java
Normal file
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
155
src/org/thoughtcrime/securesms/giph/ui/GiphyAdapter.java
Normal file
|
@ -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<GiphyAdapter.GiphyViewHolder> {
|
||||
|
||||
private static final String TAG = GiphyAdapter.class.getSimpleName();
|
||||
|
||||
private List<GiphyImage> images;
|
||||
private Context context;
|
||||
private OnItemClickListener listener;
|
||||
|
||||
class GiphyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, RequestListener<String, GlideDrawable> {
|
||||
|
||||
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<GlideDrawable> 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<GlideDrawable> 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<GiphyImage> images) {
|
||||
this.context = context;
|
||||
this.images = images;
|
||||
}
|
||||
|
||||
public void setImages(List<GiphyImage> images) {
|
||||
this.images = images;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addImages(List<GiphyImage> 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<String> 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);
|
||||
}
|
||||
}
|
122
src/org/thoughtcrime/securesms/giph/ui/GiphyFragment.java
Normal file
|
@ -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<List<GiphyImage>>, 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<GiphyImage>());
|
||||
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<List<GiphyImage>> loader, List<GiphyImage> 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<List<GiphyImage>> loader) {
|
||||
noResultsView.setVisibility(View.GONE);
|
||||
this.giphyAdapter.setImages(new LinkedList<GiphyImage>());
|
||||
}
|
||||
|
||||
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<List<GiphyImage>> loader = getLoaderManager().getLoader(0);
|
||||
if (loader == null) return;
|
||||
|
||||
new AsyncTask<Void, Void, List<GiphyImage>>() {
|
||||
@Override
|
||||
protected List<GiphyImage> doInBackground(Void... params) {
|
||||
return ((GiphyLoader)loader).loadPage(currentPage * GiphyLoader.PAGE_SIZE);
|
||||
}
|
||||
|
||||
protected void onPostExecute(List<GiphyImage> images) {
|
||||
giphyAdapter.addImages(images);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
||||
}
|
19
src/org/thoughtcrime/securesms/giph/ui/GiphyGifFragment.java
Normal file
|
@ -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<List<GiphyImage>> onCreateLoader(int id, Bundle args) {
|
||||
return new GiphyGifLoader(getActivity(), searchString);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<List<GiphyImage>> onCreateLoader(int id, Bundle args) {
|
||||
return new GiphyStickerLoader(getActivity(), searchString);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<InputStream> {
|
||||
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<String, String> 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
|
||||
}
|
||||
}
|
75
src/org/thoughtcrime/securesms/glide/OkHttpUrlLoader.java
Normal file
|
@ -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<GlideUrl, InputStream> {
|
||||
|
||||
/**
|
||||
* The default factory for {@link OkHttpUrlLoader}s.
|
||||
*/
|
||||
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
|
||||
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<GlideUrl, InputStream> 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<InputStream> getResourceFetcher(GlideUrl model, int width, int height) {
|
||||
return new OkHttpStreamFetcher(client, model);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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> T fromJson(InputStreamReader serialized, Class<T> clazz) throws IOException {
|
||||
public static <T> T fromJson(InputStream serialized, Class<T> clazz) throws IOException {
|
||||
return objectMapper.readValue(serialized, clazz);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(Reader serialized, Class<T> clazz) throws IOException {
|
||||
return objectMapper.readValue(serialized, clazz);
|
||||
}
|
||||
|
||||
|
|
|
@ -384,6 +384,14 @@ public class Util {
|
|||
}
|
||||
}
|
||||
|
||||
public static <T> 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));
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package org.thoughtcrime.securesms.util.concurrent;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public interface ListenableFuture<T> {
|
||||
public interface ListenableFuture<T> extends Future<T> {
|
||||
void addListener(Listener<T> listener);
|
||||
|
||||
public interface Listener<T> {
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.util.concurrent.Future;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
public class SettableFuture<T> implements Future<T>, ListenableFuture<T> {
|
||||
public class SettableFuture<T> implements ListenableFuture<T> {
|
||||
|
||||
private final List<Listener<T>> listeners = new LinkedList<>();
|
||||
|
||||
|
@ -42,6 +42,7 @@ public class SettableFuture<T> implements Future<T>, ListenableFuture<T> {
|
|||
|
||||
this.result = result;
|
||||
this.completed = true;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
notifyAllListeners();
|
||||
|
@ -54,6 +55,7 @@ public class SettableFuture<T> implements Future<T>, ListenableFuture<T> {
|
|||
|
||||
this.exception = throwable;
|
||||
this.completed = true;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
notifyAllListeners();
|
||||
|
|