2015-03-31 22:44:41 +00:00
package org.thoughtcrime.securesms.components ;
import android.content.Context ;
2015-07-15 20:42:59 +00:00
import android.content.res.TypedArray ;
2015-10-16 20:59:40 +00:00
import android.net.Uri ;
2015-03-31 22:44:41 +00:00
import android.support.annotation.NonNull ;
2018-03-20 18:27:11 +00:00
import android.support.annotation.UiThread ;
2015-03-31 22:44:41 +00:00
import android.util.AttributeSet ;
2018-08-01 15:09:24 +00:00
import org.thoughtcrime.securesms.logging.Log ;
2015-03-31 22:44:41 +00:00
import android.view.View ;
2018-05-22 20:59:42 +00:00
import android.view.ViewGroup ;
2015-06-27 03:14:51 +00:00
import android.widget.FrameLayout ;
2015-07-15 20:42:59 +00:00
import android.widget.ImageView ;
2015-03-31 22:44:41 +00:00
2017-10-12 00:12:46 +00:00
import com.bumptech.glide.RequestBuilder ;
2016-10-17 02:05:07 +00:00
import com.bumptech.glide.load.engine.DiskCacheStrategy ;
2018-03-20 18:27:11 +00:00
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation ;
import com.bumptech.glide.load.resource.bitmap.CenterCrop ;
import com.bumptech.glide.load.resource.bitmap.FitCenter ;
2017-10-12 00:12:46 +00:00
import com.bumptech.glide.load.resource.bitmap.RoundedCorners ;
import com.bumptech.glide.request.RequestOptions ;
2015-03-31 22:44:41 +00:00
2019-07-24 02:30:23 +00:00
import network.loki.messenger.R ;
2015-10-13 01:25:05 +00:00
import org.thoughtcrime.securesms.database.AttachmentDatabase ;
2015-03-31 22:44:41 +00:00
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri ;
2018-03-20 18:27:11 +00:00
import org.thoughtcrime.securesms.mms.GlideRequest ;
2017-10-16 20:11:42 +00:00
import org.thoughtcrime.securesms.mms.GlideRequests ;
2015-03-31 22:44:41 +00:00
import org.thoughtcrime.securesms.mms.Slide ;
2015-10-21 22:32:29 +00:00
import org.thoughtcrime.securesms.mms.SlideClickListener ;
2018-11-09 07:33:37 +00:00
import org.thoughtcrime.securesms.mms.SlidesClickedListener ;
2015-05-18 15:38:48 +00:00
import org.thoughtcrime.securesms.util.Util ;
2015-07-30 22:02:20 +00:00
import org.thoughtcrime.securesms.util.ViewUtil ;
2018-07-25 15:30:48 +00:00
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture ;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture ;
2016-03-23 17:34:41 +00:00
import org.whispersystems.libsignal.util.guava.Optional ;
2015-03-31 22:44:41 +00:00
2018-11-09 07:33:37 +00:00
import java.util.Collections ;
2018-03-20 18:27:11 +00:00
import java.util.Locale ;
2017-10-12 00:12:46 +00:00
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade ;
2015-06-27 03:14:51 +00:00
public class ThumbnailView extends FrameLayout {
2015-10-21 22:32:29 +00:00
2018-06-26 17:27:44 +00:00
private static final String TAG = ThumbnailView . class . getSimpleName ( ) ;
2018-03-20 18:27:11 +00:00
private static final int WIDTH = 0 ;
private static final int HEIGHT = 1 ;
private static final int MIN_WIDTH = 0 ;
private static final int MAX_WIDTH = 1 ;
private static final int MIN_HEIGHT = 2 ;
private static final int MAX_HEIGHT = 3 ;
2015-06-27 03:14:51 +00:00
2015-09-24 23:46:57 +00:00
private ImageView image ;
2018-06-26 17:27:44 +00:00
private View playOverlay ;
2018-11-09 07:33:37 +00:00
private View captionIcon ;
2015-09-24 23:46:57 +00:00
private OnClickListener parentClickListener ;
2015-05-18 15:38:48 +00:00
2018-03-20 18:27:11 +00:00
private final int [ ] dimens = new int [ 2 ] ;
private final int [ ] bounds = new int [ 4 ] ;
private final int [ ] measureDimens = new int [ 2 ] ;
2015-10-21 22:32:29 +00:00
private Optional < TransferControlView > transferControls = Optional . absent ( ) ;
private SlideClickListener thumbnailClickListener = null ;
2018-11-09 07:33:37 +00:00
private SlidesClickedListener downloadClickListener = null ;
2015-10-21 22:32:29 +00:00
private Slide slide = null ;
2015-03-31 22:44:41 +00:00
2018-06-26 17:27:44 +00:00
private int radius ;
2015-03-31 22:44:41 +00:00
public ThumbnailView ( Context context ) {
2015-06-27 03:14:51 +00:00
this ( context , null ) ;
2015-03-31 22:44:41 +00:00
}
public ThumbnailView ( Context context , AttributeSet attrs ) {
2015-06-27 03:14:51 +00:00
this ( context , attrs , 0 ) ;
2015-03-31 22:44:41 +00:00
}
2015-08-24 22:24:31 +00:00
public ThumbnailView ( final Context context , AttributeSet attrs , int defStyle ) {
2015-03-31 22:44:41 +00:00
super ( context , attrs , defStyle ) ;
2015-10-21 22:32:29 +00:00
2015-06-27 03:14:51 +00:00
inflate ( context , R . layout . thumbnail_view , this ) ;
2015-10-21 22:32:29 +00:00
2018-01-25 03:17:44 +00:00
this . image = findViewById ( R . id . thumbnail_image ) ;
this . playOverlay = findViewById ( R . id . play_overlay ) ;
2018-11-09 07:33:37 +00:00
this . captionIcon = findViewById ( R . id . thumbnail_caption_icon ) ;
2015-09-24 23:46:57 +00:00
super . setOnClickListener ( new ThumbnailClickDispatcher ( ) ) ;
2015-09-09 18:55:06 +00:00
2015-07-15 20:42:59 +00:00
if ( attrs ! = null ) {
TypedArray typedArray = context . getTheme ( ) . obtainStyledAttributes ( attrs , R . styleable . ThumbnailView , 0 , 0 ) ;
2018-06-26 17:27:44 +00:00
bounds [ MIN_WIDTH ] = typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView_minWidth , 0 ) ;
bounds [ MAX_WIDTH ] = typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView_maxWidth , 0 ) ;
bounds [ MIN_HEIGHT ] = typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView_minHeight , 0 ) ;
bounds [ MAX_HEIGHT ] = typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView_maxHeight , 0 ) ;
2019-01-15 08:41:05 +00:00
radius = typedArray . getDimensionPixelSize ( R . styleable . ThumbnailView_thumbnail_radius , getResources ( ) . getDimensionPixelSize ( R . dimen . thumbnail_default_radius ) ) ;
2015-07-15 20:42:59 +00:00
typedArray . recycle ( ) ;
2018-07-12 23:03:32 +00:00
} else {
radius = getResources ( ) . getDimensionPixelSize ( R . dimen . message_corner_collapse_radius ) ;
2015-07-15 20:42:59 +00:00
}
2015-06-27 03:14:51 +00:00
}
2018-03-20 18:27:11 +00:00
@Override
protected void onMeasure ( int originalWidthMeasureSpec , int originalHeightMeasureSpec ) {
fillTargetDimensions ( measureDimens , dimens , bounds ) ;
if ( measureDimens [ WIDTH ] = = 0 & & measureDimens [ HEIGHT ] = = 0 ) {
super . onMeasure ( originalWidthMeasureSpec , originalHeightMeasureSpec ) ;
return ;
}
int finalWidth = measureDimens [ WIDTH ] + getPaddingLeft ( ) + getPaddingRight ( ) ;
int finalHeight = measureDimens [ HEIGHT ] + getPaddingTop ( ) + getPaddingBottom ( ) ;
super . onMeasure ( MeasureSpec . makeMeasureSpec ( finalWidth , MeasureSpec . EXACTLY ) ,
MeasureSpec . makeMeasureSpec ( finalHeight , MeasureSpec . EXACTLY ) ) ;
}
@SuppressWarnings ( " SuspiciousNameCombination " )
private void fillTargetDimensions ( int [ ] targetDimens , int [ ] dimens , int [ ] bounds ) {
int dimensFilledCount = getNonZeroCount ( dimens ) ;
int boundsFilledCount = getNonZeroCount ( bounds ) ;
if ( dimensFilledCount = = 0 | | boundsFilledCount = = 0 ) {
targetDimens [ WIDTH ] = 0 ;
targetDimens [ HEIGHT ] = 0 ;
return ;
}
double naturalWidth = dimens [ WIDTH ] ;
double naturalHeight = dimens [ HEIGHT ] ;
int minWidth = bounds [ MIN_WIDTH ] ;
int maxWidth = bounds [ MAX_WIDTH ] ;
int minHeight = bounds [ MIN_HEIGHT ] ;
int maxHeight = bounds [ MAX_HEIGHT ] ;
if ( dimensFilledCount > 0 & & dimensFilledCount < dimens . length ) {
throw new IllegalStateException ( String . format ( Locale . ENGLISH , " Width or height has been specified, but not both. Dimens: %f x %f " ,
naturalWidth , naturalHeight ) ) ;
}
if ( boundsFilledCount > 0 & & boundsFilledCount < bounds . length ) {
throw new IllegalStateException ( String . format ( Locale . ENGLISH , " One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d] " ,
minWidth , maxWidth , minHeight , maxHeight ) ) ;
}
double measuredWidth = naturalWidth ;
double measuredHeight = naturalHeight ;
boolean widthInBounds = measuredWidth > = minWidth & & measuredWidth < = maxWidth ;
boolean heightInBounds = measuredHeight > = minHeight & & measuredHeight < = maxHeight ;
if ( ! widthInBounds | | ! heightInBounds ) {
double minWidthRatio = naturalWidth / minWidth ;
double maxWidthRatio = naturalWidth / maxWidth ;
double minHeightRatio = naturalHeight / minHeight ;
double maxHeightRatio = naturalHeight / maxHeight ;
if ( maxWidthRatio > 1 | | maxHeightRatio > 1 ) {
if ( maxWidthRatio > = maxHeightRatio ) {
measuredWidth / = maxWidthRatio ;
measuredHeight / = maxWidthRatio ;
} else {
measuredWidth / = maxHeightRatio ;
measuredHeight / = maxHeightRatio ;
}
measuredWidth = Math . max ( measuredWidth , minWidth ) ;
measuredHeight = Math . max ( measuredHeight , minHeight ) ;
} else if ( minWidthRatio < 1 | | minHeightRatio < 1 ) {
if ( minWidthRatio < = minHeightRatio ) {
measuredWidth / = minWidthRatio ;
measuredHeight / = minWidthRatio ;
} else {
measuredWidth / = minHeightRatio ;
measuredHeight / = minHeightRatio ;
}
measuredWidth = Math . min ( measuredWidth , maxWidth ) ;
measuredHeight = Math . min ( measuredHeight , maxHeight ) ;
}
}
targetDimens [ WIDTH ] = ( int ) measuredWidth ;
targetDimens [ HEIGHT ] = ( int ) measuredHeight ;
}
private int getNonZeroCount ( int [ ] vals ) {
int count = 0 ;
for ( int val : vals ) {
if ( val > 0 ) {
count + + ;
}
}
return count ;
}
2015-10-13 01:25:05 +00:00
@Override
public void setOnClickListener ( OnClickListener l ) {
2015-09-24 23:46:57 +00:00
parentClickListener = l ;
}
2015-10-13 01:25:05 +00:00
@Override
public void setFocusable ( boolean focusable ) {
2015-09-24 23:46:57 +00:00
super . setFocusable ( focusable ) ;
if ( transferControls . isPresent ( ) ) transferControls . get ( ) . setFocusable ( focusable ) ;
}
2015-10-13 01:25:05 +00:00
@Override
public void setClickable ( boolean clickable ) {
2015-09-24 22:55:17 +00:00
super . setClickable ( clickable ) ;
2015-09-24 23:46:57 +00:00
if ( transferControls . isPresent ( ) ) transferControls . get ( ) . setClickable ( clickable ) ;
2015-09-24 22:55:17 +00:00
}
2015-09-08 01:08:44 +00:00
private TransferControlView getTransferControls ( ) {
2015-09-24 23:46:57 +00:00
if ( ! transferControls . isPresent ( ) ) {
2018-01-25 03:17:44 +00:00
transferControls = Optional . of ( ViewUtil . inflateStub ( this , R . id . transfer_controls_stub ) ) ;
2015-09-24 23:46:57 +00:00
}
return transferControls . get ( ) ;
2015-03-31 22:44:41 +00:00
}
2018-06-26 17:27:44 +00:00
public void setBounds ( int minWidth , int maxWidth , int minHeight , int maxHeight ) {
bounds [ MIN_WIDTH ] = minWidth ;
bounds [ MAX_WIDTH ] = maxWidth ;
bounds [ MIN_HEIGHT ] = minHeight ;
bounds [ MAX_HEIGHT ] = maxHeight ;
forceLayout ( ) ;
}
2018-03-20 18:27:11 +00:00
@UiThread
2018-07-25 15:30:48 +00:00
public ListenableFuture < Boolean > setImageResource ( @NonNull GlideRequests glideRequests , @NonNull Slide slide ,
boolean showControls , boolean isPreview )
2017-10-16 20:11:42 +00:00
{
2018-07-25 15:30:48 +00:00
return setImageResource ( glideRequests , slide , showControls , isPreview , 0 , 0 ) ;
2018-03-20 18:27:11 +00:00
}
@UiThread
2018-07-25 15:30:48 +00:00
public ListenableFuture < Boolean > setImageResource ( @NonNull GlideRequests glideRequests , @NonNull Slide slide ,
2018-11-09 07:33:37 +00:00
boolean showControls , boolean isPreview ,
int naturalWidth , int naturalHeight )
2018-03-20 18:27:11 +00:00
{
2015-11-02 21:55:16 +00:00
if ( showControls ) {
getTransferControls ( ) . setSlide ( slide ) ;
getTransferControls ( ) . setDownloadClickListener ( new DownloadClickDispatcher ( ) ) ;
} else if ( transferControls . isPresent ( ) ) {
getTransferControls ( ) . setVisibility ( View . GONE ) ;
}
2015-10-21 22:32:29 +00:00
2017-04-20 04:23:57 +00:00
if ( slide . getThumbnailUri ( ) ! = null & & slide . hasPlayOverlay ( ) & &
( slide . getTransferState ( ) = = AttachmentDatabase . TRANSFER_PROGRESS_DONE | | isPreview ) )
{
2016-12-11 21:37:27 +00:00
this . playOverlay . setVisibility ( View . VISIBLE ) ;
} else {
this . playOverlay . setVisibility ( View . GONE ) ;
}
2015-06-27 03:14:51 +00:00
if ( Util . equals ( slide , this . slide ) ) {
2018-08-02 13:25:33 +00:00
Log . i ( TAG , " Not re-loading slide " + slide . asAttachment ( ) . getDataUri ( ) ) ;
2018-07-25 15:30:48 +00:00
return new SettableFuture < > ( false ) ;
2015-06-27 03:14:51 +00:00
}
2015-09-10 04:05:21 +00:00
2017-04-22 23:29:26 +00:00
if ( this . slide ! = null & & this . slide . getFastPreflightId ( ) ! = null & &
this . slide . getFastPreflightId ( ) . equals ( slide . getFastPreflightId ( ) ) )
{
2018-08-02 13:25:33 +00:00
Log . i ( TAG , " Not re-loading slide for fast preflight: " + slide . getFastPreflightId ( ) ) ;
2017-04-22 23:29:26 +00:00
this . slide = slide ;
2018-07-25 15:30:48 +00:00
return new SettableFuture < > ( false ) ;
2017-04-22 23:29:26 +00:00
}
2018-08-02 13:25:33 +00:00
Log . i ( TAG , " loading part with id " + slide . asAttachment ( ) . getDataUri ( )
2017-04-22 23:29:26 +00:00
+ " , progress " + slide . getTransferState ( ) + " , fast preflight id: " +
slide . asAttachment ( ) . getFastPreflightId ( ) ) ;
2015-10-13 01:25:05 +00:00
this . slide = slide ;
2018-11-09 07:33:37 +00:00
this . captionIcon . setVisibility ( slide . getCaption ( ) . isPresent ( ) ? VISIBLE : GONE ) ;
2018-05-22 20:59:42 +00:00
dimens [ WIDTH ] = naturalWidth ;
dimens [ HEIGHT ] = naturalHeight ;
invalidate ( ) ;
2018-07-25 15:30:48 +00:00
SettableFuture < Boolean > result = new SettableFuture < > ( ) ;
2018-03-20 18:27:11 +00:00
2018-07-25 15:30:48 +00:00
if ( slide . getThumbnailUri ( ) ! = null ) {
2018-07-27 13:12:54 +00:00
buildThumbnailGlideRequest ( glideRequests , slide ) . into ( new GlideDrawableListeningTarget ( image , result ) ) ;
2018-07-25 15:30:48 +00:00
} else if ( slide . hasPlaceholder ( ) ) {
2018-07-27 13:12:54 +00:00
buildPlaceholderGlideRequest ( glideRequests , slide ) . into ( new GlideBitmapListeningTarget ( image , result ) ) ;
2018-07-25 15:30:48 +00:00
} else {
glideRequests . clear ( image ) ;
result . set ( false ) ;
}
return result ;
2015-03-31 22:44:41 +00:00
}
2018-07-25 15:30:48 +00:00
public ListenableFuture < Boolean > setImageResource ( @NonNull GlideRequests glideRequests , @NonNull Uri uri ) {
SettableFuture < Boolean > future = new SettableFuture < > ( ) ;
2015-10-16 20:59:40 +00:00
if ( transferControls . isPresent ( ) ) getTransferControls ( ) . setVisibility ( View . GONE ) ;
2018-11-20 17:59:23 +00:00
GlideRequest request = glideRequests . load ( new DecryptableUri ( uri ) )
. diskCacheStrategy ( DiskCacheStrategy . NONE )
. transition ( withCrossFade ( ) ) ;
if ( radius > 0 ) {
request = request . transforms ( new CenterCrop ( ) , new RoundedCorners ( radius ) ) ;
} else {
request = request . transforms ( new CenterCrop ( ) ) ;
}
request . into ( new GlideDrawableListeningTarget ( image , future ) ) ;
2018-07-25 15:30:48 +00:00
return future ;
2015-10-16 20:59:40 +00:00
}
2015-10-21 22:32:29 +00:00
public void setThumbnailClickListener ( SlideClickListener listener ) {
2015-03-31 22:44:41 +00:00
this . thumbnailClickListener = listener ;
}
2018-11-09 07:33:37 +00:00
public void setDownloadClickListener ( SlidesClickedListener listener ) {
2015-08-24 22:24:31 +00:00
this . downloadClickListener = listener ;
}
2017-10-16 20:11:42 +00:00
public void clear ( GlideRequests glideRequests ) {
glideRequests . clear ( image ) ;
if ( transferControls . isPresent ( ) ) {
getTransferControls ( ) . clear ( ) ;
}
2015-10-21 22:32:19 +00:00
slide = null ;
2015-05-31 05:10:40 +00:00
}
2019-01-15 08:41:05 +00:00
public void showDownloadText ( boolean showDownloadText ) {
getTransferControls ( ) . setShowDownloadText ( showDownloadText ) ;
}
2015-09-05 00:33:22 +00:00
public void showProgressSpinner ( ) {
2015-09-08 01:08:44 +00:00
getTransferControls ( ) . showProgressSpinner ( ) ;
2015-09-05 00:33:22 +00:00
}
2019-01-15 08:41:05 +00:00
protected void setRadius ( int radius ) {
this . radius = radius ;
}
2018-03-20 18:27:11 +00:00
private GlideRequest buildThumbnailGlideRequest ( @NonNull GlideRequests glideRequests , @NonNull Slide slide ) {
GlideRequest request = applySizing ( glideRequests . load ( new DecryptableUri ( slide . getThumbnailUri ( ) ) )
2018-03-18 21:52:49 +00:00
. diskCacheStrategy ( DiskCacheStrategy . RESOURCE )
2018-03-20 18:27:11 +00:00
. transition ( withCrossFade ( ) ) , new CenterCrop ( ) ) ;
2015-09-10 04:05:21 +00:00
2018-03-20 18:27:11 +00:00
if ( slide . isInProgress ( ) ) return request ;
else return request . apply ( RequestOptions . errorOf ( R . drawable . ic_missing_thumbnail_picture ) ) ;
2015-03-31 22:44:41 +00:00
}
2017-10-16 20:11:42 +00:00
private RequestBuilder buildPlaceholderGlideRequest ( @NonNull GlideRequests glideRequests , @NonNull Slide slide ) {
2018-03-20 18:27:11 +00:00
return applySizing ( glideRequests . asBitmap ( )
2017-10-16 20:11:42 +00:00
. load ( slide . getPlaceholderRes ( getContext ( ) . getTheme ( ) ) )
2018-03-20 18:27:11 +00:00
. diskCacheStrategy ( DiskCacheStrategy . NONE ) , new FitCenter ( ) ) ;
}
2018-05-22 20:59:42 +00:00
private GlideRequest applySizing ( @NonNull GlideRequest request , @NonNull BitmapTransformation fitting ) {
2018-03-20 18:27:11 +00:00
int [ ] size = new int [ 2 ] ;
fillTargetDimensions ( size , dimens , bounds ) ;
if ( size [ WIDTH ] = = 0 & & size [ HEIGHT ] = = 0 ) {
2018-05-22 20:59:42 +00:00
size [ WIDTH ] = getDefaultWidth ( ) ;
size [ HEIGHT ] = getDefaultHeight ( ) ;
2018-03-20 18:27:11 +00:00
}
2018-11-09 07:33:37 +00:00
request = request . override ( size [ WIDTH ] , size [ HEIGHT ] ) ;
if ( radius > 0 ) {
return request . transforms ( fitting , new RoundedCorners ( radius ) ) ;
} else {
return request . transforms ( fitting ) ;
}
2018-05-22 20:59:42 +00:00
}
private int getDefaultWidth ( ) {
ViewGroup . LayoutParams params = getLayoutParams ( ) ;
if ( params ! = null ) {
return Math . max ( params . width , 0 ) ;
}
return 0 ;
}
private int getDefaultHeight ( ) {
ViewGroup . LayoutParams params = getLayoutParams ( ) ;
if ( params ! = null ) {
return Math . max ( params . height , 0 ) ;
}
return 0 ;
2015-03-31 22:44:41 +00:00
}
2015-09-09 18:55:06 +00:00
private class ThumbnailClickDispatcher implements View . OnClickListener {
@Override
public void onClick ( View view ) {
2015-10-13 01:25:05 +00:00
if ( thumbnailClickListener ! = null & &
slide ! = null & &
slide . asAttachment ( ) . getDataUri ( ) ! = null & &
slide . getTransferState ( ) = = AttachmentDatabase . TRANSFER_PROGRESS_DONE )
2015-09-09 18:55:06 +00:00
{
thumbnailClickListener . onClick ( view , slide ) ;
2015-09-24 23:46:57 +00:00
} else if ( parentClickListener ! = null ) {
parentClickListener . onClick ( view ) ;
2015-09-09 18:55:06 +00:00
}
2015-03-31 22:44:41 +00:00
}
2015-09-09 18:55:06 +00:00
}
2015-03-31 22:44:41 +00:00
2015-09-09 18:55:06 +00:00
private class DownloadClickDispatcher implements View . OnClickListener {
2015-03-31 22:44:41 +00:00
@Override
public void onClick ( View view ) {
2018-08-02 13:50:36 +00:00
Log . i ( TAG , " onClick() for download button " ) ;
2015-09-09 18:55:06 +00:00
if ( downloadClickListener ! = null & & slide ! = null ) {
2018-11-09 07:33:37 +00:00
downloadClickListener . onClick ( view , Collections . singletonList ( slide ) ) ;
2018-08-02 13:50:36 +00:00
} else {
Log . w ( TAG , " Received a download button click, but unable to execute it. slide: " + String . valueOf ( slide ) + " downloadClickListener: " + String . valueOf ( downloadClickListener ) ) ;
2015-04-23 18:33:34 +00:00
}
2015-03-31 22:44:41 +00:00
}
}
}