/* * Firetweet - Twitter client for Android * * Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.getlantern.firetweet.fragment.support; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.NfcAdapter.CreateNdefMessageCallback; import android.nfc.NfcEvent; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.support.v4.util.Pair; import android.support.v7.widget.ActionMenuView; import android.support.v7.widget.CardView; import android.support.v7.widget.FixedLinearLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.PopupMenu.OnMenuItemClickListener; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.Adapter; import android.support.v7.widget.RecyclerView.LayoutParams; import android.support.v7.widget.RecyclerView.ViewHolder; import android.text.Html; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.style.URLSpan; import android.view.ActionMode; import android.view.ActionMode.Callback; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.webkit.URLUtil; import android.widget.ImageView; import android.widget.Space; import android.widget.TextView; import org.getlantern.firetweet.R; import org.getlantern.firetweet.activity.iface.IThemedActivity; import org.getlantern.firetweet.activity.support.ColorPickerDialogActivity; import org.getlantern.firetweet.adapter.AbsStatusesAdapter.StatusAdapterListener; import org.getlantern.firetweet.adapter.decorator.DividerItemDecoration; import org.getlantern.firetweet.adapter.iface.IStatusesAdapter; import org.getlantern.firetweet.app.FiretweetApplication; import org.getlantern.firetweet.constant.IntentConstants; import org.getlantern.firetweet.loader.support.ParcelableStatusLoader; import org.getlantern.firetweet.loader.support.StatusRepliesLoader; import org.getlantern.firetweet.model.ListResponse; import org.getlantern.firetweet.model.ParcelableAccount; import org.getlantern.firetweet.model.ParcelableAccount.ParcelableCredentials; import org.getlantern.firetweet.model.ParcelableLocation; import org.getlantern.firetweet.model.ParcelableMedia; import org.getlantern.firetweet.model.ParcelableStatus; import org.getlantern.firetweet.model.SingleResponse; import org.getlantern.firetweet.text.method.StatusContentMovementMethod; import org.getlantern.firetweet.util.AsyncTaskUtils; import org.getlantern.firetweet.util.AsyncTwitterWrapper; import org.getlantern.firetweet.util.ClipboardUtils; import org.getlantern.firetweet.util.CompareUtils; import org.getlantern.firetweet.util.ImageLoadingHandler; import org.getlantern.firetweet.util.LinkCreator; import org.getlantern.firetweet.util.MediaLoaderWrapper; import org.getlantern.firetweet.util.SharedPreferencesWrapper; import org.getlantern.firetweet.util.StatusAdapterLinkClickHandler; import org.getlantern.firetweet.util.StatusLinkClickHandler; import org.getlantern.firetweet.util.ThemeUtils; import org.getlantern.firetweet.util.FiretweetLinkify; import org.getlantern.firetweet.util.TwitterCardUtils; import org.getlantern.firetweet.util.UserColorNameUtils; import org.getlantern.firetweet.util.Utils; import org.getlantern.firetweet.util.Utils.OnMediaClickListener; import org.getlantern.firetweet.view.CardMediaContainer; import org.getlantern.firetweet.view.ColorLabelRelativeLayout; import org.getlantern.firetweet.view.ForegroundColorView; import org.getlantern.firetweet.view.ShapedImageView; import org.getlantern.firetweet.view.StatusTextView; import org.getlantern.firetweet.view.TwitterCardContainer; import org.getlantern.firetweet.view.holder.GapViewHolder; import org.getlantern.firetweet.view.holder.LoadIndicatorViewHolder; import org.getlantern.firetweet.view.holder.StatusViewHolder; import java.util.ArrayList; import java.util.List; import java.util.Locale; import twitter4j.TwitterException; import static android.text.TextUtils.isEmpty; import static org.getlantern.firetweet.util.UserColorNameUtils.clearUserColor; import static org.getlantern.firetweet.util.UserColorNameUtils.getUserNickname; import static org.getlantern.firetweet.util.UserColorNameUtils.setUserColor; import static org.getlantern.firetweet.util.Utils.findStatus; import static org.getlantern.firetweet.util.Utils.formatToLongTimeString; import static org.getlantern.firetweet.util.Utils.getLocalizedNumber; import static org.getlantern.firetweet.util.Utils.getUserTypeIconRes; import static org.getlantern.firetweet.util.Utils.openStatus; import static org.getlantern.firetweet.util.Utils.openUserProfile; import static org.getlantern.firetweet.util.Utils.setMenuForStatus; import static org.getlantern.firetweet.util.Utils.showErrorMessage; /** * Created by mariotaku on 14/12/5. */ public class StatusFragment extends BaseSupportFragment implements LoaderCallbacks<SingleResponse<ParcelableStatus>>, OnMediaClickListener, StatusAdapterListener { private static final int LOADER_ID_DETAIL_STATUS = 1; private static final int LOADER_ID_STATUS_REPLIES = 2; private static final int STATE_LOADED = 1; private static final int STATE_LOADING = 2; private static final int STATE_ERROR = 3; private RecyclerView mRecyclerView; private StatusAdapter mStatusAdapter; private boolean mRepliesLoaderInitialized; private LoadConversationTask mLoadConversationTask; private LinearLayoutManager mLayoutManager; private View mStatusContent; private View mProgressContainer; private View mErrorContainer; private DividerItemDecoration mItemDecoration; private LoaderCallbacks<List<ParcelableStatus>> mRepliesLoaderCallback = new LoaderCallbacks<List<ParcelableStatus>>() { @Override public Loader<List<ParcelableStatus>> onCreateLoader(int id, Bundle args) { final long accountId = args.getLong(EXTRA_ACCOUNT_ID, -1); final String screenName = args.getString(EXTRA_SCREEN_NAME); final long statusId = args.getLong(EXTRA_STATUS_ID, -1); final long maxId = args.getLong(EXTRA_MAX_ID, -1); final long sinceId = args.getLong(EXTRA_SINCE_ID, -1); final StatusRepliesLoader loader = new StatusRepliesLoader(getActivity(), accountId, screenName, statusId, maxId, sinceId, null, null, 0, true); loader.setComparator(ParcelableStatus.REVERSE_ID_COMPARATOR); return loader; } @Override public void onLoadFinished(Loader<List<ParcelableStatus>> loader, List<ParcelableStatus> data) { final Pair<Long, Integer> readPosition = saveReadPosition(); setReplies(data); restoreReadPosition(readPosition); } @Override public void onLoaderReset(Loader<List<ParcelableStatus>> loader) { } }; private void restoreReadPosition(@Nullable Pair<Long, Integer> position) { if (position == null) return; final int adapterPosition = mStatusAdapter.findPositionById(position.first); if (adapterPosition == RecyclerView.NO_POSITION) return; mLayoutManager.scrollToPositionWithOffset(adapterPosition, position.second); } @Nullable private Pair<Long, Integer> saveReadPosition() { final int position = mLayoutManager.findFirstVisibleItemPosition(); if (position == RecyclerView.NO_POSITION) return null; long itemId = mStatusAdapter.getItemId(position); final View positionView; if (itemId == StatusAdapter.VIEW_TYPE_CONVERSATION_LOAD_INDICATOR) { // Should be next item positionView = mLayoutManager.findViewByPosition(position + 1); itemId = mStatusAdapter.getItemId(position + 1); } else { positionView = mLayoutManager.findViewByPosition(position); } return new Pair<>(itemId, positionView != null ? positionView.getTop() : -1); } private PopupMenu mPopupMenu; private ParcelableStatus mSelectedStatus; private OnMenuItemClickListener mOnStatusMenuItemClickListener = new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { final ParcelableStatus status = mSelectedStatus; if (status == null) return false; return Utils.handleMenuItemClick(getActivity(), StatusFragment.this, getFragmentManager(), getTwitterWrapper(), status, item); } }; @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { switch (requestCode) { case REQUEST_SET_COLOR: { final ParcelableStatus status = mStatusAdapter.getStatus(); if (status == null) return; if (resultCode == Activity.RESULT_OK) { if (data == null) return; final int color = data.getIntExtra(EXTRA_COLOR, Color.TRANSPARENT); setUserColor(getActivity(), status.user_id, color); } else if (resultCode == ColorPickerDialogActivity.RESULT_CLEARED) { clearUserColor(getActivity(), status.user_id); } break; } case REQUEST_SELECT_ACCOUNT: { final ParcelableStatus status = mStatusAdapter.getStatus(); if (status == null) return; if (resultCode == Activity.RESULT_OK) { if (data == null || !data.hasExtra(EXTRA_ID)) return; final long accountId = data.getLongExtra(EXTRA_ID, -1); openStatus(getActivity(), accountId, status.id); } break; } } } @Override public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_status, container, false); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); final View view = getView(); if (view == null) throw new AssertionError(); final Context context = view.getContext(); final boolean compact = Utils.isCompactCards(context); Utils.setNdefPushMessageCallback(getActivity(), new CreateNdefMessageCallback() { @Override public NdefMessage createNdefMessage(NfcEvent event) { final ParcelableStatus status = getStatus(); if (status == null) return null; return new NdefMessage(new NdefRecord[]{ NdefRecord.createUri(LinkCreator.getTwitterStatusLink(status.user_screen_name, status.id)), }); } }); mLayoutManager = new StatusListLinearLayoutManager(context, mRecyclerView); mItemDecoration = new DividerItemDecoration(context, mLayoutManager.getOrientation()); if (compact) { mRecyclerView.addItemDecoration(mItemDecoration); } mLayoutManager.setRecycleChildrenOnDetach(true); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.setClipToPadding(false); mStatusAdapter = new StatusAdapter(this, compact); mStatusAdapter.setEventListener(this); mRecyclerView.setAdapter(mStatusAdapter); setState(STATE_LOADING); getLoaderManager().initLoader(LOADER_ID_DETAIL_STATUS, getArguments(), this); } @Override public void onBaseViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onBaseViewCreated(view, savedInstanceState); mStatusContent = view.findViewById(R.id.status_content); mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); mProgressContainer = view.findViewById(R.id.progress_container); mErrorContainer = view.findViewById(R.id.error_retry_container); } @Override protected void fitSystemWindows(Rect insets) { super.fitSystemWindows(insets); final View view = getView(); if (view != null) { view.setPadding(insets.left, insets.top, insets.right, insets.bottom); } } @Override public void onGapClick(GapViewHolder holder, int position) { } @Override public void onMediaClick(StatusViewHolder holder, ParcelableMedia media, int position) { final ParcelableStatus status = mStatusAdapter.getStatus(position); if (status == null) return; Utils.openMedia(getActivity(), status, media); } @Override public void onStatusActionClick(StatusViewHolder holder, int id, int position) { final ParcelableStatus status = mStatusAdapter.getStatus(position); if (status == null) return; switch (id) { case R.id.reply_count: { final Context context = getActivity(); final Intent intent = new Intent(IntentConstants.INTENT_ACTION_REPLY); intent.setPackage(context.getPackageName()); intent.putExtra(IntentConstants.EXTRA_STATUS, status); context.startActivity(intent); break; } case R.id.retweet_count: { RetweetQuoteDialogFragment.show(getFragmentManager(), status); break; } case R.id.favorite_count: { final AsyncTwitterWrapper twitter = getTwitterWrapper(); if (twitter == null) return; if (status.is_favorite) { twitter.destroyFavoriteAsync(status.account_id, status.id); } else { twitter.createFavoriteAsync(status.account_id, status.id); } break; } } } @Override public void onStatusClick(StatusViewHolder holder, int position) { openStatus(getActivity(), mStatusAdapter.getStatus(position), null); } @Override public void onStatusMenuClick(StatusViewHolder holder, View menuView, int position) { //TODO show status menu if (mPopupMenu != null) { mPopupMenu.dismiss(); } final PopupMenu popupMenu = new PopupMenu(mStatusAdapter.getContext(), menuView, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0); popupMenu.setOnMenuItemClickListener(mOnStatusMenuItemClickListener); popupMenu.inflate(R.menu.action_status); final ParcelableStatus status = mStatusAdapter.getStatus(position); setMenuForStatus(mStatusAdapter.getContext(), popupMenu.getMenu(), status); popupMenu.show(); mPopupMenu = popupMenu; mSelectedStatus = status; } @Override public Loader<SingleResponse<ParcelableStatus>> onCreateLoader(final int id, final Bundle args) { final Bundle fragmentArgs = getArguments(); final long accountId = fragmentArgs.getLong(EXTRA_ACCOUNT_ID, -1); final long statusId = fragmentArgs.getLong(EXTRA_STATUS_ID, -1); return new ParcelableStatusLoader(getActivity(), false, fragmentArgs, accountId, statusId); } @Override public void onMediaClick(View view, ParcelableMedia media, long accountId) { final ParcelableStatus status = mStatusAdapter.getStatus(); if (status == null) return; Utils.openMediaDirectly(getActivity(), accountId, status, media, status.media); } private void addConversation(ParcelableStatus status, int position) { mStatusAdapter.addConversation(status, position); } private DividerItemDecoration getItemDecoration() { return mItemDecoration; } private ParcelableStatus getStatus() { return mStatusAdapter.getStatus(); } @Override public void onLoadFinished(final Loader<SingleResponse<ParcelableStatus>> loader, final SingleResponse<ParcelableStatus> data) { if (data.hasData()) { final long itemId = mStatusAdapter.getItemId(mLayoutManager.findFirstVisibleItemPosition()); final View firstChild = mLayoutManager.getChildAt(0); final int top = firstChild != null ? firstChild.getTop() : 0; final ParcelableStatus status = data.getData(); if (mStatusAdapter.setStatus(status)) { mLayoutManager.scrollToPositionWithOffset(1, 0); mStatusAdapter.setConversation(null); mStatusAdapter.setReplies(null); loadReplies(status); loadConversation(status); } else { final int position = mStatusAdapter.findPositionById(itemId); mLayoutManager.scrollToPositionWithOffset(position, top); } setState(STATE_LOADED); } else { //TODO show errors setState(STATE_ERROR); } } private void loadConversation(ParcelableStatus status) { if (AsyncTaskUtils.isTaskRunning(mLoadConversationTask)) { mLoadConversationTask.cancel(true); } mLoadConversationTask = new LoadConversationTask(this); AsyncTaskUtils.executeTask(mLoadConversationTask, status); } private void loadReplies(ParcelableStatus status) { if (status == null) return; final Bundle args = new Bundle(); args.putLong(EXTRA_ACCOUNT_ID, status.account_id); args.putLong(EXTRA_STATUS_ID, status.retweet_id > 0 ? status.retweet_id : status.id); args.putString(EXTRA_SCREEN_NAME, status.user_screen_name); if (mRepliesLoaderInitialized) { getLoaderManager().restartLoader(LOADER_ID_STATUS_REPLIES, args, mRepliesLoaderCallback); return; } getLoaderManager().initLoader(LOADER_ID_STATUS_REPLIES, args, mRepliesLoaderCallback); mRepliesLoaderInitialized = true; } private void setConversation(List<ParcelableStatus> data) { final Pair<Long, Integer> readPosition = saveReadPosition(); mStatusAdapter.setConversation(data); restoreReadPosition(readPosition); } private void setReplies(List<ParcelableStatus> data) { if (mLayoutManager.getChildCount() != 0) { final long itemId = mStatusAdapter.getItemId(mLayoutManager.findFirstVisibleItemPosition()); final int top = mLayoutManager.getChildAt(0).getTop(); mStatusAdapter.setReplies(data); final int position = mStatusAdapter.findPositionById(itemId); mLayoutManager.scrollToPositionWithOffset(position, top); } else { mStatusAdapter.setReplies(data); } } private void setState(int state) { mStatusContent.setVisibility(state == STATE_LOADED ? View.VISIBLE : View.GONE); mProgressContainer.setVisibility(state == STATE_LOADING ? View.VISIBLE : View.GONE); mErrorContainer.setVisibility(state == STATE_ERROR ? View.VISIBLE : View.GONE); } private static class DetailStatusViewHolder extends ViewHolder implements OnClickListener, ActionMenuView.OnMenuItemClickListener { private final StatusAdapter adapter; private final CardView cardView; private final ActionMenuView menuBar; private final TextView nameView, screenNameView; private final StatusTextView textView; private final TextView quoteTextView; private final TextView quotedNameView, quotedScreenNameView; private final ShapedImageView profileImageView; private final ImageView profileTypeView; private final TextView timeSourceView; private final TextView retweetedByView; private final View repliesContainer, retweetsContainer, favoritesContainer; private final TextView repliesCountView, retweetsCountView, favoritesCountView; private final ColorLabelRelativeLayout profileContainer; private final View retweetedByContainer; private final View mediaPreviewContainer; private final View mediaPreviewLoad; private final CardMediaContainer mediaPreview; private final View quotedNameContainer; private final ForegroundColorView quoteIndicator; private final TextView locationView; private final TwitterCardContainer twitterCard; private final StatusLinkClickHandler linkClickHandler; private final FiretweetLinkify linkify; public DetailStatusViewHolder(StatusAdapter adapter, View itemView) { super(itemView); this.linkClickHandler = new StatusLinkClickHandler(adapter.getContext(), null); this.linkify = new FiretweetLinkify(linkClickHandler, false); this.adapter = adapter; cardView = (CardView) itemView.findViewById(R.id.card); menuBar = (ActionMenuView) itemView.findViewById(R.id.menu_bar); nameView = (TextView) itemView.findViewById(R.id.name); screenNameView = (TextView) itemView.findViewById(R.id.screen_name); textView = (StatusTextView) itemView.findViewById(R.id.text); profileImageView = (ShapedImageView) itemView.findViewById(R.id.profile_image); profileTypeView = (ImageView) itemView.findViewById(R.id.profile_type); timeSourceView = (TextView) itemView.findViewById(R.id.time_source); retweetedByView = (TextView) itemView.findViewById(R.id.retweeted_by); retweetedByContainer = itemView.findViewById(R.id.retweeted_by_container); repliesContainer = itemView.findViewById(R.id.replies_container); retweetsContainer = itemView.findViewById(R.id.retweets_container); favoritesContainer = itemView.findViewById(R.id.favorites_container); repliesCountView = (TextView) itemView.findViewById(R.id.replies_count); retweetsCountView = (TextView) itemView.findViewById(R.id.retweets_count); favoritesCountView = (TextView) itemView.findViewById(R.id.favorites_count); mediaPreviewContainer = itemView.findViewById(R.id.media_preview_container); mediaPreviewLoad = itemView.findViewById(R.id.media_preview_load); mediaPreview = (CardMediaContainer) itemView.findViewById(R.id.media_preview); locationView = (TextView) itemView.findViewById(R.id.location_view); profileContainer = (ColorLabelRelativeLayout) itemView.findViewById(R.id.profile_container); twitterCard = (TwitterCardContainer) itemView.findViewById(R.id.twitter_card); quoteTextView = (TextView) itemView.findViewById(R.id.quote_text); quotedNameView = (TextView) itemView.findViewById(R.id.quoted_name); quotedScreenNameView = (TextView) itemView.findViewById(R.id.quoted_screen_name); quotedNameContainer = itemView.findViewById(R.id.quoted_name_container); quoteIndicator = (ForegroundColorView) itemView.findViewById(R.id.quote_indicator); setIsRecyclable(false); initViews(); } public void displayStatus(ParcelableStatus status) { if (status == null) return; final StatusFragment fragment = adapter.getFragment(); final Context context = adapter.getContext(); final Resources resources = context.getResources(); final MediaLoaderWrapper loader = adapter.getImageLoader(); final boolean nameFirst = adapter.isNameFirst(); linkClickHandler.setStatus(status); if (status.retweet_id > 0) { final String retweetedBy = UserColorNameUtils.getDisplayName(context, status.retweeted_by_id, status.retweeted_by_name, status.retweeted_by_screen_name, nameFirst); retweetedByView.setText(context.getString(R.string.name_retweeted, retweetedBy)); retweetedByContainer.setVisibility(View.VISIBLE); } else { retweetedByView.setText(null); retweetedByContainer.setVisibility(View.GONE); } profileContainer.drawEnd(Utils.getAccountColor(context, status.account_id)); final int typeIconRes, typeDescriptionRes; final long timestamp; final String source; if (status.is_quote) { quotedNameView.setText(getUserNickname(context, status.user_id, status.user_name, true)); quotedScreenNameView.setText("@" + status.user_screen_name); nameView.setText(getUserNickname(context, status.quoted_by_user_id, status.quoted_by_user_name, true)); screenNameView.setText("@" + status.quoted_by_user_screen_name); final int idx = status.quote_text_unescaped.lastIndexOf(" twitter.com"); final Spanned quote_text = Html.fromHtml(status.quote_text_html); quoteTextView.setText(idx > 0 ? quote_text.subSequence(0, idx - 1) : quote_text); final SpannableString originalTweetLink = SpannableString.valueOf("Original tweet"); originalTweetLink.setSpan(new URLSpan(LinkCreator.getTwitterStatusLink(status.user_screen_name, status.quote_id).toString()), 0, originalTweetLink.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); quoteTextView.append(originalTweetLink); linkify.applyAllLinks(quoteTextView, status.account_id, getLayoutPosition(), status.is_possibly_sensitive, adapter.getLinkHighlightingStyle()); loader.displayProfileImage(profileImageView, status.quoted_by_user_profile_image); quotedNameContainer.setVisibility(View.VISIBLE); quoteTextView.setVisibility(View.VISIBLE); quoteIndicator.setVisibility(View.VISIBLE); profileContainer.drawStart(UserColorNameUtils.getUserColor(context, status.quoted_by_user_id)); typeIconRes = getUserTypeIconRes(status.quoted_by_user_is_verified, status.quoted_by_user_is_protected); typeDescriptionRes = Utils.getUserTypeDescriptionRes(status.quoted_by_user_is_verified, status.quoted_by_user_is_protected); timestamp = status.quote_timestamp; source = status.quote_source; } else { nameView.setText(getUserNickname(context, status.user_id, status.user_name, true)); screenNameView.setText("@" + status.user_screen_name); loader.displayProfileImage(profileImageView, status.user_profile_image_url); quotedNameContainer.setVisibility(View.GONE); quoteTextView.setVisibility(View.GONE); quoteIndicator.setVisibility(View.GONE); profileContainer.drawStart(UserColorNameUtils.getUserColor(context, status.user_id)); typeIconRes = getUserTypeIconRes(status.user_is_verified, status.user_is_protected); typeDescriptionRes = Utils.getUserTypeDescriptionRes(status.user_is_verified, status.user_is_protected); timestamp = status.timestamp; source = status.source; } if (typeIconRes != 0 && typeDescriptionRes != 0) { profileTypeView.setImageResource(typeIconRes); profileTypeView.setContentDescription(context.getString(typeDescriptionRes)); profileTypeView.setVisibility(View.VISIBLE); } else { profileTypeView.setImageDrawable(null); profileTypeView.setContentDescription(null); profileTypeView.setVisibility(View.GONE); } final String timeString = formatToLongTimeString(context, timestamp); if (!isEmpty(timeString) && !isEmpty(source)) { timeSourceView.setText(Html.fromHtml(context.getString(R.string.time_source, timeString, source))); } else if (isEmpty(timeString) && !isEmpty(source)) { timeSourceView.setText(Html.fromHtml(context.getString(R.string.source, source))); } else if (!isEmpty(timeString) && isEmpty(source)) { timeSourceView.setText(timeString); } timeSourceView.setMovementMethod(null); textView.setText(Html.fromHtml(status.text_html)); linkify.applyAllLinks(textView, status.account_id, getAdapterPosition(), status.is_possibly_sensitive); ThemeUtils.applyParagraphSpacing(textView, 1.1f); if (!TextUtils.isEmpty(status.place_full_name)) { locationView.setVisibility(View.VISIBLE); locationView.setText(status.place_full_name); locationView.setClickable(ParcelableLocation.isValidLocation(status.location)); } else if (ParcelableLocation.isValidLocation(status.location)) { locationView.setVisibility(View.VISIBLE); locationView.setText(R.string.view_map); locationView.setClickable(true); } else { locationView.setVisibility(View.GONE); locationView.setText(null); } retweetsContainer.setVisibility(!status.user_is_protected ? View.VISIBLE : View.GONE); repliesContainer.setVisibility(status.reply_count < 0 ? View.GONE : View.VISIBLE); final Locale locale = context.getResources().getConfiguration().locale; repliesCountView.setText(getLocalizedNumber(locale, status.reply_count)); retweetsCountView.setText(getLocalizedNumber(locale, status.retweet_count)); favoritesCountView.setText(getLocalizedNumber(locale, status.favorite_count)); if (status.media == null) { mediaPreviewContainer.setVisibility(View.GONE); mediaPreview.setVisibility(View.GONE); mediaPreviewLoad.setVisibility(View.GONE); mediaPreview.displayMedia(); } else if (adapter.isDetailMediaExpanded()) { mediaPreviewContainer.setVisibility(View.VISIBLE); mediaPreview.setVisibility(View.VISIBLE); mediaPreviewLoad.setVisibility(View.GONE); mediaPreview.displayMedia(status.media, loader, status.account_id, adapter.getFragment(), adapter.getImageLoadingHandler()); } else { mediaPreviewContainer.setVisibility(View.VISIBLE); mediaPreview.setVisibility(View.GONE); mediaPreviewLoad.setVisibility(View.VISIBLE); mediaPreview.displayMedia(); } if (TwitterCardUtils.isCardSupported(status.card)) { final Point size = TwitterCardUtils.getCardSize(status.card); twitterCard.setVisibility(View.VISIBLE); if (size != null) { twitterCard.setCardSize(size.x, size.y); } else { twitterCard.setCardSize(0, 0); } final Fragment cardFragment = TwitterCardUtils.createCardFragment(status.card); final FragmentManager fm = fragment.getChildFragmentManager(); final FragmentTransaction ft = fm.beginTransaction(); ft.replace(R.id.twitter_card, cardFragment); ft.commit(); } else { twitterCard.setVisibility(View.GONE); } Utils.setMenuForStatus(context, menuBar.getMenu(), status, adapter.getStatusAccount()); } @Override public void onClick(View v) { final ParcelableStatus status = adapter.getStatus(getAdapterPosition()); final StatusFragment fragment = adapter.getFragment(); switch (v.getId()) { case R.id.media_preview_load: { if (adapter.isSensitiveContentEnabled() || !status.is_possibly_sensitive) { adapter.setDetailMediaExpanded(true); } else { final LoadSensitiveImageConfirmDialogFragment f = new LoadSensitiveImageConfirmDialogFragment(); f.show(fragment.getChildFragmentManager(), "load_sensitive_image_confirm"); } break; } case R.id.profile_container: { final FragmentActivity activity = fragment.getActivity(); final Bundle activityOption = Utils.makeSceneTransitionOption(activity, new Pair<View, String>(profileImageView, UserFragment.TRANSITION_NAME_PROFILE_IMAGE), new Pair<View, String>(profileTypeView, UserFragment.TRANSITION_NAME_PROFILE_TYPE)); openUserProfile(activity, status.account_id, status.user_id, status.user_screen_name, activityOption); break; } case R.id.retweets_container: { final FragmentActivity activity = fragment.getActivity(); Utils.openStatusRetweeters(activity, status.account_id, status.id); break; } case R.id.retweeted_by_container: { if (status.retweet_id > 0) { Utils.openUserProfile(adapter.getContext(), status.account_id, status.user_id, status.user_screen_name, null); } break; } case R.id.location_view: { final ParcelableLocation location = status.location; if (!ParcelableLocation.isValidLocation(location)) return; Utils.openMap(adapter.getContext(), location.latitude, location.longitude); break; } } } @Override public boolean onMenuItemClick(MenuItem item) { final StatusFragment fragment = adapter.getFragment(); final ParcelableStatus status = adapter.getStatus(getAdapterPosition()); if (status == null || fragment == null) return false; final AsyncTwitterWrapper twitter = fragment.getTwitterWrapper(); final FragmentActivity activity = fragment.getActivity(); final FragmentManager fm = fragment.getFragmentManager(); return Utils.handleMenuItemClick(activity, fragment, fm, twitter, status, item); } private void initViews() { // menuBar.setOnMenuItemClickListener(this); menuBar.setOnMenuItemClickListener(this); final StatusFragment fragment = adapter.getFragment(); final FragmentActivity activity = fragment.getActivity(); final MenuInflater inflater = activity.getMenuInflater(); inflater.inflate(R.menu.menu_status, menuBar.getMenu()); ThemeUtils.wrapMenuIcon(menuBar, MENU_GROUP_STATUS_SHARE); mediaPreviewLoad.setOnClickListener(this); profileContainer.setOnClickListener(this); retweetsContainer.setOnClickListener(this); retweetedByContainer.setOnClickListener(this); locationView.setOnClickListener(this); final float defaultTextSize = adapter.getTextSize(); nameView.setTextSize(defaultTextSize * 1.25f); quotedNameView.setTextSize(defaultTextSize * 1.25f); textView.setTextSize(defaultTextSize * 1.25f); quoteTextView.setTextSize(defaultTextSize * 1.25f); screenNameView.setTextSize(defaultTextSize * 0.85f); quotedScreenNameView.setTextSize(defaultTextSize * 0.85f); locationView.setTextSize(defaultTextSize * 0.85f); timeSourceView.setTextSize(defaultTextSize * 0.85f); mediaPreview.setStyle(adapter.getMediaPreviewStyle()); textView.setMovementMethod(StatusContentMovementMethod.getInstance()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { quoteTextView.setCustomSelectionActionModeCallback(new StatusActionModeCallback(quoteTextView, fragment, activity)); textView.setCustomSelectionActionModeCallback(new StatusActionModeCallback(textView, fragment, activity)); } } private static class StatusActionModeCallback implements Callback { private final TextView textView; private final StatusFragment fragment; private final FragmentActivity activity; public StatusActionModeCallback(TextView textView, StatusFragment fragment, FragmentActivity activity) { this.textView = textView; this.fragment = fragment; this.activity = activity; } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { final FragmentActivity activity = fragment.getActivity(); if (activity instanceof IThemedActivity) { final int themeRes = ((IThemedActivity) activity).getCurrentThemeResourceId(); final int accentColor = ((IThemedActivity) activity).getCurrentThemeColor(); ThemeUtils.applySupportActionModeBackground(mode, fragment.getActivity(), themeRes, accentColor, true); } mode.getMenuInflater().inflate(R.menu.action_status_text_selection, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { final int start = textView.getSelectionStart(), end = textView.getSelectionEnd(); final SpannableString string = SpannableString.valueOf(textView.getText()); final URLSpan[] spans = string.getSpans(start, end, URLSpan.class); final boolean avail = spans.length == 1 && URLUtil.isValidUrl(spans[0].getURL()); Utils.setMenuItemAvailability(menu, android.R.id.copyUrl, avail); return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case android.R.id.copyUrl: { final int start = textView.getSelectionStart(), end = textView.getSelectionEnd(); final SpannableString string = SpannableString.valueOf(textView.getText()); final URLSpan[] spans = string.getSpans(start, end, URLSpan.class); if (spans.length != 1) return true; ClipboardUtils.setText(activity, spans[0].getURL()); mode.finish(); return true; } } return false; } @Override public void onDestroyActionMode(ActionMode mode) { } } } public static final class LoadSensitiveImageConfirmDialogFragment extends BaseSupportDialogFragment implements DialogInterface.OnClickListener { @Override public void onClick(final DialogInterface dialog, final int which) { switch (which) { case DialogInterface.BUTTON_POSITIVE: { final Fragment f = getParentFragment(); if (f instanceof StatusFragment) { final StatusAdapter adapter = ((StatusFragment) f).getAdapter(); adapter.setDetailMediaExpanded(true); } break; } } } @NonNull @Override public Dialog onCreateDialog(final Bundle savedInstanceState) { final Context wrapped = ThemeUtils.getDialogThemedContext(getActivity()); final AlertDialog.Builder builder = new AlertDialog.Builder(wrapped); builder.setTitle(android.R.string.dialog_alert_title); builder.setMessage(R.string.sensitive_content_warning); builder.setPositiveButton(android.R.string.ok, this); builder.setNegativeButton(android.R.string.cancel, null); return builder.create(); } } private StatusAdapter getAdapter() { return mStatusAdapter; } static class LoadConversationTask extends AsyncTask<ParcelableStatus, ParcelableStatus, ListResponse<ParcelableStatus>> { final Context context; final StatusFragment fragment; LoadConversationTask(final StatusFragment fragment) { context = fragment.getActivity(); this.fragment = fragment; } @Override protected ListResponse<ParcelableStatus> doInBackground(final ParcelableStatus... params) { final ArrayList<ParcelableStatus> list = new ArrayList<>(); try { ParcelableStatus status = params[0]; final long account_id = status.account_id; while (status.in_reply_to_status_id > 0 && !isCancelled()) { status = findStatus(context, account_id, status.in_reply_to_status_id); publishProgress(status); list.add(0, status); } } catch (final TwitterException e) { return ListResponse.getListInstance(e); } return ListResponse.getListInstance(list); } @Override protected void onPostExecute(final ListResponse<ParcelableStatus> data) { if (data.hasData()) { fragment.setConversation(data.getData()); } else { showErrorMessage(context, context.getString(R.string.action_getting_status), data.getException(), true); } } @Override protected void onProgressUpdate(ParcelableStatus... values) { for (ParcelableStatus status : values) { // fragment.addConversation(status, 0); } } @Override protected void onCancelled() { } } private static class SpaceViewHolder extends ViewHolder { public SpaceViewHolder(View itemView) { super(itemView); } } @Override public void onLoaderReset(final Loader<SingleResponse<ParcelableStatus>> loader) { } private static class StatusAdapter extends Adapter<ViewHolder> implements IStatusesAdapter<List<ParcelableStatus>> { private static final int VIEW_TYPE_DETAIL_STATUS = 0; private static final int VIEW_TYPE_LIST_STATUS = 1; private static final int VIEW_TYPE_CONVERSATION_LOAD_INDICATOR = 2; private static final int VIEW_TYPE_REPLIES_LOAD_INDICATOR = 3; private static final int VIEW_TYPE_SPACE = 4; private final Context mContext; private final StatusFragment mFragment; private final LayoutInflater mInflater; private final MediaLoaderWrapper mImageLoader; private final ImageLoadingHandler mImageLoadingHandler; private final FiretweetLinkify mFiretweetLinkify; private final boolean mNameFirst; private final int mCardLayoutResource; private final int mTextSize; private final int mCardBackgroundColor; private final boolean mIsCompact; private final int mProfileImageStyle; private final int mMediaPreviewStyle; private final int mLinkHighligingStyle; private final boolean mDisplayMediaPreview; private final boolean mDisplayProfileImage; private final boolean mSensitiveContentEnabled; private final boolean mHideCardActions; private boolean mLoadMoreSupported; private boolean mLoadMoreIndicatorVisible; private ParcelableStatus mStatus; private ParcelableCredentials mStatusAccount; private List<ParcelableStatus> mConversation, mReplies; private boolean mDetailMediaExpanded; private StatusAdapterListener mStatusAdapterListener; private DetailStatusViewHolder mCachedHolder; public StatusAdapter(StatusFragment fragment, boolean compact) { final Context context = fragment.getActivity(); final Resources res = context.getResources(); final SharedPreferencesWrapper preferences = SharedPreferencesWrapper.getInstance(context, SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); mFragment = fragment; mContext = context; mInflater = LayoutInflater.from(context); mImageLoader = FiretweetApplication.getInstance(context).getMediaLoaderWrapper(); mImageLoadingHandler = new ImageLoadingHandler(R.id.media_preview_progress); mCardBackgroundColor = ThemeUtils.getCardBackgroundColor(context); mNameFirst = preferences.getBoolean(KEY_NAME_FIRST, true); mTextSize = preferences.getInt(KEY_TEXT_SIZE, res.getInteger(R.integer.default_text_size)); mProfileImageStyle = Utils.getProfileImageStyle(preferences.getString(KEY_PROFILE_IMAGE_STYLE, null)); mMediaPreviewStyle = Utils.getMediaPreviewStyle(preferences.getString(KEY_MEDIA_PREVIEW_STYLE, null)); mLinkHighligingStyle = Utils.getLinkHighlightingStyleInt(preferences.getString(KEY_LINK_HIGHLIGHT_OPTION, null)); mIsCompact = compact; mDisplayProfileImage = preferences.getBoolean(KEY_DISPLAY_PROFILE_IMAGE, true); mDisplayMediaPreview = preferences.getBoolean(KEY_MEDIA_PREVIEW, false); mSensitiveContentEnabled = preferences.getBoolean(KEY_DISPLAY_SENSITIVE_CONTENTS, false); mHideCardActions = preferences.getBoolean(KEY_HIDE_CARD_ACTIONS, false); if (compact) { mCardLayoutResource = R.layout.card_item_status_compact; } else { mCardLayoutResource = R.layout.card_item_status; } mFiretweetLinkify = new FiretweetLinkify(new StatusAdapterLinkClickHandler<>(this)); } public void addConversation(ParcelableStatus status, int position) { if (mConversation == null) { mConversation = new ArrayList<>(); } mConversation.add(position, status); notifyDataSetChanged(); updateItemDecoration(); } public int findPositionById(long itemId) { for (int i = 0, j = getItemCount(); i < j; i++) { if (getItemId(i) == itemId) return i; } return RecyclerView.NO_POSITION; } public StatusFragment getFragment() { return mFragment; } public MediaLoaderWrapper getImageLoader() { return mImageLoader; } public Context getContext() { return mContext; } @Override public ImageLoadingHandler getImageLoadingHandler() { return mImageLoadingHandler; } @Override public int getProfileImageStyle() { return mProfileImageStyle; } @Override public int getMediaPreviewStyle() { return mMediaPreviewStyle; } @Override public AsyncTwitterWrapper getTwitterWrapper() { return mFragment.getTwitterWrapper(); } public float getTextSize() { return mTextSize; } @Override public boolean isLoadMoreIndicatorVisible() { return mLoadMoreIndicatorVisible; } @Override public boolean isLoadMoreSupported() { return mLoadMoreSupported; } @Override public void setLoadMoreSupported(boolean supported) { mLoadMoreSupported = supported; if (!supported) { mLoadMoreIndicatorVisible = false; } notifyDataSetChanged(); } @Override public void setLoadMoreIndicatorVisible(boolean enabled) { if (mLoadMoreIndicatorVisible == enabled) return; mLoadMoreIndicatorVisible = enabled && mLoadMoreSupported; updateItemDecoration(); notifyDataSetChanged(); } @Override public ParcelableStatus getStatus(int position) { final int conversationCount = getConversationCount(); if (position == getItemCount() - 1) { return null; } else if (position < conversationCount) { // out of bounds issue if (position < 0) { position = 0; } return mConversation != null ? mConversation.get(position) : null; } else if (position > conversationCount) { return mReplies != null ? mReplies.get(position - conversationCount - 1) : null; } else { return mStatus; } } @Override public int getStatusesCount() { return getConversationCount() + 1 + getRepliesCount() + 1; } @Override public long getStatusId(int position) { final ParcelableStatus status = getStatus(position); return status != null ? status.hashCode() : position; } @Override public FiretweetLinkify getFiretweetLinkify() { return mFiretweetLinkify; } @Override public boolean isMediaPreviewEnabled() { return mDisplayMediaPreview; } @Override public int getLinkHighlightingStyle() { return mLinkHighligingStyle; } public boolean isNameFirst() { return mNameFirst; } @Override public boolean isSensitiveContentEnabled() { return mSensitiveContentEnabled; } @Override public boolean isCardActionsHidden() { return mHideCardActions; } @Override public void setData(List<ParcelableStatus> data) { } @Override public boolean shouldShowAccountsColor() { return false; } public ParcelableStatus getStatus() { return mStatus; } public ParcelableCredentials getStatusAccount() { return mStatusAccount; } public boolean isDetailMediaExpanded() { return mDetailMediaExpanded; } public void setDetailMediaExpanded(boolean expanded) { mDetailMediaExpanded = expanded; notifyDataSetChanged(); updateItemDecoration(); } @Override public boolean isGapItem(int position) { return false; } @Override public final void onGapClick(ViewHolder holder, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onGapClick((GapViewHolder) holder, position); } } @Override public boolean isProfileImageEnabled() { return mDisplayProfileImage; } @Override public final void onStatusClick(StatusViewHolder holder, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onStatusClick(holder, position); } } @Override public void onMediaClick(StatusViewHolder holder, ParcelableMedia media, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onMediaClick(holder, media, position); } } @Override public void onUserProfileClick(StatusViewHolder holder, int position) { final Context context = getContext(); final ParcelableStatus status = getStatus(position); final View profileImageView = holder.getProfileImageView(); final View profileTypeView = holder.getProfileTypeView(); if (context instanceof FragmentActivity) { final Bundle options = Utils.makeSceneTransitionOption((FragmentActivity) context, new Pair<>(profileImageView, UserFragment.TRANSITION_NAME_PROFILE_IMAGE), new Pair<>(profileTypeView, UserFragment.TRANSITION_NAME_PROFILE_TYPE)); Utils.openUserProfile(context, status.account_id, status.user_id, status.user_screen_name, options); } else { Utils.openUserProfile(context, status.account_id, status.user_id, status.user_screen_name, null); } } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_DETAIL_STATUS: { if (mCachedHolder != null) return mCachedHolder; final View view; if (mIsCompact) { view = mInflater.inflate(R.layout.header_status_compact, parent, false); final View cardView = view.findViewById(R.id.compact_card); cardView.setBackgroundColor(mCardBackgroundColor); } else { view = mInflater.inflate(R.layout.header_status, parent, false); final CardView cardView = (CardView) view.findViewById(R.id.card); cardView.setCardBackgroundColor(mCardBackgroundColor); } return new DetailStatusViewHolder(this, view); } case VIEW_TYPE_LIST_STATUS: { final View view = mInflater.inflate(mCardLayoutResource, parent, false); final CardView cardView = (CardView) view.findViewById(R.id.card); if (cardView != null) { cardView.setCardBackgroundColor(mCardBackgroundColor); } final StatusViewHolder holder = new StatusViewHolder(this, view); holder.setupViewOptions(); holder.setOnClickListeners(); return holder; } case VIEW_TYPE_CONVERSATION_LOAD_INDICATOR: case VIEW_TYPE_REPLIES_LOAD_INDICATOR: { final View view = mInflater.inflate(R.layout.card_item_load_indicator, parent, false); return new LoadIndicatorViewHolder(view); } case VIEW_TYPE_SPACE: { return new SpaceViewHolder(new Space(mContext)); } } return null; } @Override public void onBindViewHolder(ViewHolder holder, int position) { switch (getItemViewType(position)) { case VIEW_TYPE_DETAIL_STATUS: { final ParcelableStatus status = getStatus(position); final DetailStatusViewHolder detailHolder = (DetailStatusViewHolder) holder; detailHolder.displayStatus(status); break; } case VIEW_TYPE_LIST_STATUS: { final ParcelableStatus status = getStatus(position); final StatusViewHolder statusHolder = (StatusViewHolder) holder; // Display 'in reply to' for first item // useful to indicate whether first tweet has reply or not statusHolder.displayStatus(status, position == 0); break; } } } @Override public int getItemViewType(int position) { final int conversationCount = getConversationCount(); if (position == getItemCount() - 1) { return VIEW_TYPE_SPACE; } else if (position < conversationCount) { return mConversation != null ? VIEW_TYPE_LIST_STATUS : VIEW_TYPE_CONVERSATION_LOAD_INDICATOR; } else if (position > conversationCount) { return mReplies != null ? VIEW_TYPE_LIST_STATUS : VIEW_TYPE_REPLIES_LOAD_INDICATOR; } else { return VIEW_TYPE_DETAIL_STATUS; } } @Override public long getItemId(int position) { final int conversationCount = getConversationCount(); if (position == getItemCount() - 1) { return VIEW_TYPE_SPACE; } else if (position < conversationCount) { return mConversation != null ? mConversation.get(position).id : VIEW_TYPE_CONVERSATION_LOAD_INDICATOR; } else if (position > conversationCount) { return mReplies != null ? mReplies.get(position - conversationCount - 1).id : VIEW_TYPE_REPLIES_LOAD_INDICATOR; } else { return mStatus != null ? mStatus.id : VIEW_TYPE_DETAIL_STATUS; } } @Override public int getItemCount() { return getStatusesCount(); } @Override public void onViewAttachedToWindow(ViewHolder holder) { super.onViewAttachedToWindow(holder); if (mCachedHolder == holder) { mCachedHolder = null; } } @Override public void onViewDetachedFromWindow(ViewHolder holder) { super.onViewDetachedFromWindow(holder); if (holder instanceof DetailStatusViewHolder) { mCachedHolder = (DetailStatusViewHolder) holder; } } @Override public void onItemActionClick(ViewHolder holder, int id, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onStatusActionClick((StatusViewHolder) holder, id, position); } } @Override public void onItemMenuClick(ViewHolder holder, View itemView, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onStatusMenuClick((StatusViewHolder) holder, itemView, position); } } public void setConversation(List<ParcelableStatus> conversation) { mConversation = conversation; notifyDataSetChanged(); updateItemDecoration(); } public void setEventListener(StatusAdapterListener listener) { mStatusAdapterListener = listener; } public void setReplies(List<ParcelableStatus> replies) { mReplies = replies; notifyDataSetChanged(); updateItemDecoration(); } public boolean setStatus(ParcelableStatus status) { final ParcelableStatus old = mStatus; mStatus = status; if (status != null) { mStatusAccount = ParcelableAccount.getCredentials(mContext, status.account_id); } else { mStatusAccount = null; } notifyDataSetChanged(); updateItemDecoration(); return !CompareUtils.objectEquals(old, status); } private int getConversationCount() { return mConversation != null ? mConversation.size() : 1; } private int getRepliesCount() { return mReplies != null ? mReplies.size() : 1; } private int getStatusPosition() { return getConversationCount(); } private void updateItemDecoration() { final DividerItemDecoration decoration = mFragment.getItemDecoration(); decoration.setDecorationStart(0); if (isLoadMoreIndicatorVisible()) { decoration.setDecorationEndOffset(3); } else { decoration.setDecorationEndOffset(mReplies != null && mReplies.size() > 0 ? 1 : 2); } mFragment.mRecyclerView.invalidateItemDecorations(); } } private static class StatusListLinearLayoutManager extends FixedLinearLayoutManager { private final RecyclerView recyclerView; public StatusListLinearLayoutManager(Context context, RecyclerView recyclerView) { super(context); setOrientation(LinearLayoutManager.VERTICAL); this.recyclerView = recyclerView; } @Override public int getDecoratedMeasuredHeight(View child) { final int height = super.getDecoratedMeasuredHeight(child); int heightBeforeSpace = 0; if (getItemViewType(child) == StatusAdapter.VIEW_TYPE_SPACE) { for (int i = 0, j = getChildCount(); i < j; i++) { final View childToMeasure = getChildAt(i); final LayoutParams paramsToMeasure = (LayoutParams) childToMeasure.getLayoutParams(); final int typeToMeasure = getItemViewType(childToMeasure); if (typeToMeasure == StatusAdapter.VIEW_TYPE_DETAIL_STATUS || heightBeforeSpace != 0) { heightBeforeSpace += super.getDecoratedMeasuredHeight(childToMeasure) + paramsToMeasure.topMargin + paramsToMeasure.bottomMargin; } if (typeToMeasure == StatusAdapter.VIEW_TYPE_REPLIES_LOAD_INDICATOR) { break; } } if (heightBeforeSpace != 0) { final int spaceHeight = recyclerView.getMeasuredHeight() - heightBeforeSpace; return Math.max(0, spaceHeight); } } return height; } @Override public void setOrientation(int orientation) { if (orientation != VERTICAL) throw new IllegalArgumentException("Only VERTICAL orientation supported"); super.setOrientation(orientation); } } }