package org.wordpress.android.ui.media; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; import android.os.AsyncTask; import android.os.Handler; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import com.android.volley.toolbox.ImageLoader; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.fluxc.model.MediaModel; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.models.MediaUploadState; import org.wordpress.android.ui.FadeInNetworkImageView; import org.wordpress.android.util.AniUtils; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.ImageUtils.BitmapWorkerCallback; import org.wordpress.android.util.ImageUtils.BitmapWorkerTask; import org.wordpress.android.util.MediaUtils; import org.wordpress.android.util.PhotonUtils; import org.wordpress.android.util.SiteUtils; import org.wordpress.android.util.UrlUtils; import java.util.ArrayList; import java.util.List; import java.util.concurrent.RejectedExecutionException; /** * An adapter for the media gallery grid. */ public class MediaGridAdapter extends RecyclerView.Adapter<MediaGridAdapter.GridViewHolder> { private MediaGridAdapterCallback mCallback; private boolean mHasRetrievedAll; private boolean mAllowMultiselect; private boolean mInMultiSelect; private final Handler mHandler; private final LayoutInflater mInflater; private ImageLoader mImageLoader; private final Context mContext; private final SiteModel mSite; private final ArrayList<MediaModel> mMediaList = new ArrayList<>(); private final ArrayList<Integer> mSelectedItems = new ArrayList<>(); private final int mThumbWidth; private final int mThumbHeight; private static final float SCALE_NORMAL = 1.0f; private static final float SCALE_SELECTED = .85f; public interface MediaGridAdapterCallback { void onAdapterFetchMoreData(); void onAdapterRetryUpload(int localMediaId); void onAdapterItemSelected(View sourceView, int position); void onAdapterSelectionCountChanged(int count); } private static final int INVALID_POSITION = -1; public MediaGridAdapter(Context context, SiteModel site, ImageLoader imageLoader) { super(); setHasStableIds(true); mContext = context; mSite = site; mInflater = LayoutInflater.from(context); mHandler = new Handler(); int displayWidth = DisplayUtils.getDisplayPixelWidth(mContext); mThumbWidth = displayWidth / getColumnCount(mContext); mThumbHeight = (int) (mThumbWidth * 0.75f); setImageLoader(imageLoader); } @Override public long getItemId(int position) { return getLocalMediaIdAtPosition(position); } private void setImageLoader(ImageLoader imageLoader) { mImageLoader = imageLoader; } public void setMediaList(@NonNull List<MediaModel> mediaList) { if (isSameList(mediaList)) { AppLog.d(AppLog.T.MEDIA, "MediaGridAdapter > list is the same"); return; } mMediaList.clear(); mMediaList.addAll(mediaList); notifyDataSetChanged(); } @Override public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = mInflater.inflate(R.layout.media_grid_item, parent, false); return new GridViewHolder(view); } @Override public void onBindViewHolder(GridViewHolder holder, int position) { if (!isValidPosition(position)) { return; } MediaModel media = mMediaList.get(position); holder.imageView.setTag(null); String strState = media.getUploadState(); MediaUploadState state = MediaUploadState.fromString(strState); boolean isLocalFile = MediaUtils.isLocalFile(strState) && !TextUtils.isEmpty(media.getFilePath()); boolean isSelected = isItemSelected(media.getId()); boolean isImage = media.getMimeType() != null && media.getMimeType().startsWith("image/"); if (isImage) { holder.fileContainer.setVisibility(View.GONE); if (isLocalFile) { loadLocalImage(media.getFilePath(), holder.imageView); } else { // if this isn't a private site use Photon to request the image at the exact size, // otherwise append the standard wp query params to request the desired size String thumbUrl; if (SiteUtils.isPhotonCapable(mSite)) { thumbUrl = PhotonUtils.getPhotonImageUrl(media.getUrl(), mThumbWidth, mThumbHeight); } else { thumbUrl = UrlUtils.removeQuery(media.getUrl()) + "?w=" + mThumbWidth + "&h=" + mThumbHeight; } WordPressMediaUtils.loadNetworkImage(thumbUrl, holder.imageView, mImageLoader); } } else { // not an image, so show file name and file type holder.imageView.setImageDrawable(null); String fileName = media.getFileName(); String title = media.getTitle(); String fileExtension = MediaUtils.getExtensionForMimeType(media.getMimeType()); holder.fileContainer.setVisibility(View.VISIBLE); holder.titleView.setText(TextUtils.isEmpty(title) ? fileName : title); holder.fileTypeView.setText(fileExtension.toUpperCase()); int placeholderResId = WordPressMediaUtils.getPlaceholder(fileName); holder.fileTypeImageView.setImageResource(placeholderResId); } // show selection count when selected holder.selectionCountTextView.setVisibility(isSelected ? View.VISIBLE : View.GONE); if (isSelected) { int count = mSelectedItems.indexOf(media.getId()) + 1; holder.selectionCountTextView.setText(Integer.toString(count)); } // make sure the thumbnail scale reflects its selection state float scale = isSelected ? SCALE_SELECTED : SCALE_NORMAL; if (holder.imageView.getScaleX() != scale) { holder.imageView.setScaleX(scale); holder.imageView.setScaleY(scale); } // show upload state unless it's already uploaded if (state != MediaUploadState.UPLOADED) { holder.stateContainer.setVisibility(View.VISIBLE); // only show progress for items currently being uploaded or deleted boolean showProgress = state == MediaUploadState.UPLOADING || state == MediaUploadState.DELETING; holder.progressUpload.setVisibility(showProgress ? View.VISIBLE : View.GONE); // failed uploads can be retried if (state == MediaUploadState.FAILED) { holder.stateTextView.setText(mContext.getString(R.string.retry)); holder.stateTextView.setCompoundDrawablesWithIntrinsicBounds(0, R.drawable.media_retry_image, 0, 0); } else { holder.stateTextView.setText(MediaUploadState.getLabel(mContext, state)); holder.stateTextView.setCompoundDrawables(null, null, null, null); } } else { holder.stateContainer.setVisibility(View.GONE); holder.stateContainer.setOnClickListener(null); } // if we are near the end, make a call to fetch more if (position == getItemCount() - 1 && !mHasRetrievedAll && mCallback != null) { mCallback.onAdapterFetchMoreData(); } } public ArrayList<Integer> getSelectedItems() { return mSelectedItems; } public int getSelectedItemCount() { return mSelectedItems.size(); } class GridViewHolder extends RecyclerView.ViewHolder { private final TextView titleView; private final FadeInNetworkImageView imageView; private final TextView fileTypeView; private final ImageView fileTypeImageView; private final TextView selectionCountTextView; private final TextView stateTextView; private final ProgressBar progressUpload; private final ViewGroup stateContainer; private final ViewGroup fileContainer; public GridViewHolder(View view) { super(view); imageView = (FadeInNetworkImageView) view.findViewById(R.id.media_grid_item_image); selectionCountTextView = (TextView) view.findViewById(R.id.text_selection_count); stateContainer = (ViewGroup) view.findViewById(R.id.media_grid_item_upload_state_container); stateTextView = (TextView) stateContainer.findViewById(R.id.media_grid_item_upload_state); progressUpload = (ProgressBar) stateContainer.findViewById(R.id.media_grid_item_upload_progress); fileContainer = (ViewGroup) view.findViewById(R.id.media_grid_item_file_container); titleView = (TextView) fileContainer.findViewById(R.id.media_grid_item_name); fileTypeView = (TextView) fileContainer.findViewById(R.id.media_grid_item_filetype); fileTypeImageView = (ImageView) fileContainer.findViewById(R.id.media_grid_item_filetype_image); // make the progress bar white progressUpload.getIndeterminateDrawable().setColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY); // set size of image and container views imageView.getLayoutParams().width = mThumbWidth; imageView.getLayoutParams().height = mThumbHeight; stateContainer.getLayoutParams().width = mThumbWidth; stateContainer.getLayoutParams().height = mThumbHeight; fileContainer.getLayoutParams().width = mThumbWidth; fileContainer.getLayoutParams().height = mThumbHeight; itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { int position = getAdapterPosition(); if (isInMultiSelect()) { if (canSelectPosition(position)) { toggleItemSelected(GridViewHolder.this, position); } } else if (mCallback != null) { mCallback.onAdapterItemSelected(v, position); } } }); View.OnLongClickListener longClickListener = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { int position = getAdapterPosition(); if (canSelectPosition(position)) { if (isInMultiSelect()) { toggleItemSelected(GridViewHolder.this, position); } else if (mAllowMultiselect) { setInMultiSelect(true); setItemSelectedByPosition(GridViewHolder.this, position, true); } } return true; } }; itemView.setOnLongClickListener(longClickListener); stateTextView.setOnLongClickListener(longClickListener); stateTextView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { int position = getAdapterPosition(); if (!isValidPosition(position)) { return; } if (isInMultiSelect()) { if (canSelectPosition(position)) { toggleItemSelected(GridViewHolder.this, position); } } else { // retry uploading this media item if it previously failed MediaModel media = mMediaList.get(position); MediaUploadState state = MediaUploadState.fromString(media.getUploadState()); if (state == MediaUploadState.FAILED) { stateTextView.setText(R.string.media_upload_state_queued); stateTextView.setCompoundDrawables(null, null, null, null); if (mCallback != null) { mCallback.onAdapterRetryUpload(media.getId()); } } else if (mCallback != null) { mCallback.onAdapterItemSelected(v, position); } } } }); } } public void setAllowMultiselect(boolean allow) { mAllowMultiselect = allow; } public boolean isInMultiSelect() { return mInMultiSelect; } public void setInMultiSelect(boolean value) { if (mInMultiSelect != value) { mInMultiSelect = value; clearSelection(); } } private boolean isValidPosition(int position) { return position >= 0 && position < getItemCount(); } public int getLocalMediaIdAtPosition(int position) { if (isValidPosition(position)) { return mMediaList.get(position).getId(); } return INVALID_POSITION; } /* * determines whether the media item at the passed position can be selected - not allowed * for deleted items since the whole purpose of multiselect is to delete multiple items */ private boolean canSelectPosition(int position) { if (!mAllowMultiselect || !isValidPosition(position)) { return false; } MediaUploadState state = MediaUploadState.fromString(mMediaList.get(position).getUploadState()); return state != MediaUploadState.DELETING && state != MediaUploadState.DELETED; } private void loadLocalImage(final String filePath, ImageView imageView) { imageView.setTag(filePath); Bitmap bitmap = WordPress.getBitmapCache().get(filePath); if (bitmap != null) { imageView.setImageBitmap(bitmap); } else { imageView.setImageBitmap(null); try { new BitmapWorkerTask(imageView, mThumbWidth, mThumbHeight, new BitmapWorkerCallback() { @Override public void onBitmapReady(final String path, final ImageView imageView, final Bitmap bitmap) { mHandler.post(new Runnable() { @Override public void run() { WordPress.getBitmapCache().put(path, bitmap); if (imageView != null && imageView.getTag() instanceof String && ((String) imageView.getTag()).equalsIgnoreCase(path)) { imageView.setImageBitmap(bitmap); } } }); } }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, filePath); } catch (RejectedExecutionException e) { AppLog.e(AppLog.T.MEDIA, e); } } } @Override public int getItemCount() { return mMediaList.size(); } public static int getColumnCount(Context context) { return context.getResources().getInteger(R.integer.media_grid_num_columns); } public void setCallback(MediaGridAdapterCallback callback) { mCallback = callback; } public void setHasRetrievedAll(boolean b) { mHasRetrievedAll = b; } public void clearSelection() { if (mSelectedItems.size() > 0) { mSelectedItems.clear(); notifyDataSetChanged(); } } public boolean isItemSelected(int localMediaId) { return mSelectedItems.contains(localMediaId); } public void removeSelectionByLocalId(int localMediaId) { if (isItemSelected(localMediaId)) { mSelectedItems.remove(Integer.valueOf(localMediaId)); if (mCallback != null) { mCallback.onAdapterSelectionCountChanged(mSelectedItems.size()); } notifyDataSetChanged(); } } private void setItemSelectedByPosition(GridViewHolder holder, int position, boolean selected) { if (!isValidPosition(position)) { return; } int localMediaId = mMediaList.get(position).getId(); if (selected) { mSelectedItems.add(localMediaId); } else { mSelectedItems.remove(Integer.valueOf(localMediaId)); } // show and animate the count if (selected) { holder.selectionCountTextView.setText(Integer.toString(mSelectedItems.indexOf(localMediaId) + 1)); } AniUtils.startAnimation(holder.selectionCountTextView, selected ? R.anim.cab_select : R.anim.cab_deselect); holder.selectionCountTextView.setVisibility(selected ? View.VISIBLE : View.GONE); // scale the thumbnail if (selected) { AniUtils.scale(holder.imageView, SCALE_NORMAL, SCALE_SELECTED, AniUtils.Duration.SHORT); } else { AniUtils.scale(holder.imageView, SCALE_SELECTED, SCALE_NORMAL, AniUtils.Duration.SHORT); } // redraw after the scale animation completes long delayMs = AniUtils.Duration.SHORT.toMillis(mContext); new Handler().postDelayed(new Runnable() { @Override public void run() { notifyDataSetChanged(); } }, delayMs); if (mCallback != null) { mCallback.onAdapterSelectionCountChanged(mSelectedItems.size()); } } private void toggleItemSelected(GridViewHolder holder, int position) { if (!isValidPosition(position)) { return; } int localMediaId = mMediaList.get(position).getId(); boolean isSelected = mSelectedItems.contains(localMediaId); setItemSelectedByPosition(holder, position, !isSelected); } public void setSelectedItems(ArrayList<Integer> selectedItems) { mSelectedItems.clear(); mSelectedItems.addAll(selectedItems); if (mCallback != null) { mCallback.onAdapterSelectionCountChanged(mSelectedItems.size()); } notifyDataSetChanged(); } /* * returns true if the passed list is the same as the existing one */ private boolean isSameList(@NonNull List<MediaModel> mediaList) { if (mediaList.size() != mMediaList.size()) { return false; } for (MediaModel media: mediaList) { MediaModel thisMedia = getMediaFromId(media.getId()); if (thisMedia == null || !thisMedia.equals(media)) { return false; } } return true; } /* * returns the media item with the passed media ID in the current media list */ private MediaModel getMediaFromId(int id) { for (int i = 0; i < mMediaList.size(); i++) { if (mMediaList.get(i).getId() == id) { return mMediaList.get(i); } } return null; } }