/* * Twidere - Twitter client for Android * * Copyright (C) 2012-2014 Mariotaku Lee * * 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 . */ package org.mariotaku.twidere.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.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.NfcAdapter.CreateNdefMessageCallback; import android.nfc.NfcEvent; import android.os.AsyncTask; 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.FragmentManagerTrojan; 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.LayoutParams; import android.support.v7.widget.RecyclerView.ViewHolder; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.URLSpan; import android.view.Gravity; import android.view.KeyEvent; 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.widget.ImageView; import android.widget.Space; import android.widget.TextView; import org.apache.commons.lang3.ArrayUtils; import org.mariotaku.twidere.R; import org.mariotaku.twidere.activity.support.ColorPickerDialogActivity; import org.mariotaku.twidere.adapter.AbsStatusesAdapter.StatusAdapterListener; import org.mariotaku.twidere.adapter.BaseRecyclerViewAdapter; import org.mariotaku.twidere.adapter.decorator.DividerItemDecoration; import org.mariotaku.twidere.adapter.iface.IStatusesAdapter; import org.mariotaku.twidere.api.twitter.Twitter; import org.mariotaku.twidere.api.twitter.TwitterException; import org.mariotaku.twidere.api.twitter.model.Paging; import org.mariotaku.twidere.constant.IntentConstants; import org.mariotaku.twidere.loader.support.ParcelableStatusLoader; import org.mariotaku.twidere.loader.support.StatusRepliesLoader; import org.mariotaku.twidere.model.ListResponse; import org.mariotaku.twidere.model.ParcelableCredentials; import org.mariotaku.twidere.model.ParcelableLocation; import org.mariotaku.twidere.model.ParcelableMedia; import org.mariotaku.twidere.model.ParcelableStatus; import org.mariotaku.twidere.model.SingleResponse; import org.mariotaku.twidere.util.AsyncTaskUtils; import org.mariotaku.twidere.util.AsyncTwitterWrapper; import org.mariotaku.twidere.util.CompareUtils; import org.mariotaku.twidere.util.HtmlSpanBuilder; import org.mariotaku.twidere.util.KeyboardShortcutsHandler; import org.mariotaku.twidere.util.KeyboardShortcutsHandler.KeyboardShortcutCallback; import org.mariotaku.twidere.util.LinkCreator; import org.mariotaku.twidere.util.MathUtils; import org.mariotaku.twidere.util.MediaLoaderWrapper; import org.mariotaku.twidere.util.MediaLoadingHandler; import org.mariotaku.twidere.util.MenuUtils; import org.mariotaku.twidere.util.RecyclerViewNavigationHelper; import org.mariotaku.twidere.util.RecyclerViewUtils; import org.mariotaku.twidere.util.StatusActionModeCallback; import org.mariotaku.twidere.util.StatusAdapterLinkClickHandler; import org.mariotaku.twidere.util.StatusLinkClickHandler; import org.mariotaku.twidere.util.ThemeUtils; import org.mariotaku.twidere.util.TwidereLinkify; import org.mariotaku.twidere.util.TwitterAPIFactory; import org.mariotaku.twidere.util.TwitterCardUtils; import org.mariotaku.twidere.util.UserColorNameManager; import org.mariotaku.twidere.util.Utils; import org.mariotaku.twidere.view.CardMediaContainer; import org.mariotaku.twidere.view.CardMediaContainer.OnMediaClickListener; import org.mariotaku.twidere.view.ColorLabelRelativeLayout; import org.mariotaku.twidere.view.ForegroundColorView; import org.mariotaku.twidere.view.StatusTextView; import org.mariotaku.twidere.view.TwitterCardContainer; import org.mariotaku.twidere.view.holder.GapViewHolder; import org.mariotaku.twidere.view.holder.LoadIndicatorViewHolder; import org.mariotaku.twidere.view.holder.StatusViewHolder; import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder; import java.util.ArrayList; import java.util.List; import java.util.Locale; import edu.tsinghua.hotmobi.HotMobiLogger; import edu.tsinghua.hotmobi.model.MediaEvent; import edu.tsinghua.hotmobi.model.TimelineType; import edu.tsinghua.hotmobi.model.TweetEvent; /** * Created by mariotaku on 14/12/5. */ public class StatusFragment extends BaseSupportFragment implements LoaderCallbacks>, OnMediaClickListener, StatusAdapterListener, KeyboardShortcutCallback { // Constants 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; // Views private View mStatusContent; private View mProgressContainer; private View mErrorContainer; private RecyclerView mRecyclerView; private DividerItemDecoration mItemDecoration; private PopupMenu mPopupMenu; private StatusAdapter mStatusAdapter; private LinearLayoutManager mLayoutManager; private LoadConversationTask mLoadConversationTask; private RecyclerViewNavigationHelper mNavigationHelper; // Data fields private boolean mRepliesLoaderInitialized; private ParcelableStatus mSelectedStatus; private TweetEvent mStatusEvent; // Listeners private LoaderCallbacks> mRepliesLoaderCallback = new LoaderCallbacks>() { @Override public Loader> onCreateLoader(int id, Bundle args) { mStatusAdapter.setRepliesLoading(true); mStatusAdapter.updateItemDecoration(); 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 boolean twitterOptimizedSearches = mPreferences.getBoolean(KEY_TWITTER_OPTIMIZED_SEARCHES); final StatusRepliesLoader loader = new StatusRepliesLoader(getActivity(), accountId, screenName, statusId, maxId, sinceId, null, null, 0, true, twitterOptimizedSearches); loader.setComparator(ParcelableStatus.REVERSE_ID_COMPARATOR); return loader; } @Override public void onLoadFinished(Loader> loader, List data) { mStatusAdapter.updateItemDecoration(); setReplies(data); final ParcelableCredentials account = mStatusAdapter.getStatusAccount(); if (Utils.hasOfficialAPIAccess(loader.getContext(), mPreferences, account)) { mStatusAdapter.setReplyError(null); } else { final SpannableStringBuilder error = SpannableStringBuilder.valueOf(HtmlSpanBuilder.fromHtml(getString(R.string.cant_load_all_replies_message))); ClickableSpan dialogSpan = null; for (URLSpan span : error.getSpans(0, error.length(), URLSpan.class)) { if ("#dialog".equals(span.getURL())) { dialogSpan = span; break; } } if (dialogSpan != null) { final int spanStart = error.getSpanStart(dialogSpan), spanEnd = error.getSpanEnd(dialogSpan); error.removeSpan(dialogSpan); error.setSpan(new ClickableSpan() { @Override public void onClick(View widget) { final FragmentActivity activity = getActivity(); if (activity == null) return; SupportMessageDialogFragment.show(activity, getString(R.string.cant_load_all_replies_explanation), "cant_load_all_replies_explanation"); } }, spanStart, spanEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE); } mStatusAdapter.setReplyError(error); } mStatusAdapter.setRepliesLoading(false); } @Override public void onLoaderReset(Loader> loader) { } }; 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(), mTwitterWrapper, status, item); } }; @Override public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { final FragmentActivity activity = getActivity(); if (activity == null) return; switch (requestCode) { case REQUEST_SET_COLOR: { final ParcelableStatus status = mStatusAdapter.getStatus(); if (status == null) return; final UserColorNameManager manager = UserColorNameManager.getInstance(activity); if (resultCode == Activity.RESULT_OK) { if (data == null) return; final int color = data.getIntExtra(EXTRA_COLOR, Color.TRANSPARENT); manager.setUserColor(status.user_id, color); } else if (resultCode == ColorPickerDialogActivity.RESULT_CLEARED) { manager.clearUserColor(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); Utils.openStatus(activity, 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); setHasOptionsMenu(true); 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)), }); } }); 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); mNavigationHelper = new RecyclerViewNavigationHelper(mRecyclerView, mLayoutManager, mStatusAdapter, null); 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_container); } @Override public void onGapClick(GapViewHolder holder, int position) { } @Override public void onMediaClick(IStatusViewHolder holder, View view, ParcelableMedia media, int position) { final ParcelableStatus status = mStatusAdapter.getStatus(position); if (status == null) return; final Bundle options = Utils.createMediaViewerActivityOption(view); Utils.openMedia(getActivity(), status, media, options); MediaEvent event = MediaEvent.create(getActivity(), status, media, TimelineType.OTHER, mStatusAdapter.isMediaPreviewEnabled()); HotMobiLogger.getInstance(getActivity()).log(status.account_id, event); } @Override public void onStatusActionClick(IStatusViewHolder 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 = mTwitterWrapper; 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(IStatusViewHolder holder, int position) { Utils.openStatus(getActivity(), mStatusAdapter.getStatus(position), null); } @Override public boolean onStatusLongClick(IStatusViewHolder holder, int position) { return false; } @Override public void onStatusMenuClick(IStatusViewHolder 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); Utils.setMenuForStatus(mStatusAdapter.getContext(), mPreferences, popupMenu.getMenu(), status); popupMenu.show(); mPopupMenu = popupMenu; mSelectedStatus = status; } @Override public void onUserProfileClick(IStatusViewHolder holder, ParcelableStatus status, int position) { final FragmentActivity activity = getActivity(); final View profileImageView = holder.getProfileImageView(); final View profileTypeView = holder.getProfileTypeView(); final Bundle options = Utils.makeSceneTransitionOption(activity, new Pair<>(profileImageView, UserFragment.TRANSITION_NAME_PROFILE_IMAGE), new Pair<>(profileTypeView, UserFragment.TRANSITION_NAME_PROFILE_TYPE)); Utils.openUserProfile(activity, status.account_id, status.user_id, status.user_screen_name, options); } @Override public void onMediaClick(View view, ParcelableMedia media, long accountId) { final ParcelableStatus status = mStatusAdapter.getStatus(); if (status == null) return; final Bundle options = Utils.createMediaViewerActivityOption(view); Utils.openMediaDirectly(getActivity(), accountId, status, media, options); // BEGIN HotMobi MediaEvent event = MediaEvent.create(getActivity(), status, media, TimelineType.OTHER, mStatusAdapter.isMediaPreviewEnabled()); HotMobiLogger.getInstance(getActivity()).log(status.account_id, event); // END HotMobi } @Override public boolean handleKeyboardShortcutSingle(@NonNull KeyboardShortcutsHandler handler, int keyCode, @NonNull KeyEvent event, int metaState) { if (!KeyboardShortcutsHandler.isValidForHotkey(keyCode, event)) return false; final View focusedChild = RecyclerViewUtils.findRecyclerViewChild(mRecyclerView, mLayoutManager.getFocusedChild()); final int position; if (focusedChild != null && focusedChild.getParent() == mRecyclerView) { position = mRecyclerView.getChildLayoutPosition(focusedChild); } else { return false; } if (position == -1) return false; final ParcelableStatus status = getAdapter().getStatus(position); if (status == null) return false; String action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState); if (action == null) return false; switch (action) { case ACTION_STATUS_REPLY: { final Intent intent = new Intent(INTENT_ACTION_REPLY); intent.putExtra(EXTRA_STATUS, status); startActivity(intent); return true; } case ACTION_STATUS_RETWEET: { RetweetQuoteDialogFragment.show(getFragmentManager(), status); return true; } case ACTION_STATUS_FAVORITE: { final AsyncTwitterWrapper twitter = mTwitterWrapper; if (status.is_favorite) { twitter.destroyFavoriteAsync(status.account_id, status.id); } else { twitter.createFavoriteAsync(status.account_id, status.id); } return true; } } return false; } @Override public boolean isKeyboardShortcutHandled(@NonNull KeyboardShortcutsHandler handler, int keyCode, @NonNull KeyEvent event, int metaState) { final String action = handler.getKeyAction(CONTEXT_TAG_STATUS, keyCode, event, metaState); if (action == null) return false; switch (action) { case ACTION_STATUS_REPLY: case ACTION_STATUS_RETWEET: case ACTION_STATUS_FAVORITE: return true; } return mNavigationHelper.isKeyboardShortcutHandled(handler, keyCode, event, metaState); } @Override public boolean handleKeyboardShortcutRepeat(@NonNull final KeyboardShortcutsHandler handler, final int keyCode, final int repeatCount, @NonNull final KeyEvent event, int metaState) { return mNavigationHelper.handleKeyboardShortcutRepeat(handler, keyCode, repeatCount, event, metaState); } @Override public Loader> 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 onLoadFinished(final Loader> loader, final SingleResponse data) { if (data.hasData()) { final Pair readPosition = saveReadPosition(); final ParcelableStatus status = data.getData(); final Bundle dataExtra = data.getExtras(); final ParcelableCredentials credentials = dataExtra.getParcelable(EXTRA_ACCOUNT); if (mStatusAdapter.setStatus(status, credentials)) { mStatusAdapter.setConversation(null); mStatusAdapter.setReplies(null); loadConversation(status); loadReplies(status); final int position = mStatusAdapter.getFirstPositionOfItem(StatusAdapter.ITEM_IDX_STATUS); if (position != RecyclerView.NO_POSITION) { mLayoutManager.scrollToPositionWithOffset(position, 0); } final TweetEvent event = TweetEvent.create(getActivity(), status, TimelineType.OTHER); event.setAction(TweetEvent.Action.OPEN); mStatusEvent = event; } else if (readPosition != null) { restoreReadPosition(readPosition); } setState(STATE_LOADED); } else { //TODO show errors setState(STATE_ERROR); } invalidateOptionsMenu(); } @Override public void onLoaderReset(final Loader> loader) { final TweetEvent event = mStatusEvent; if (event == null) return; event.markEnd(); HotMobiLogger.getInstance(getActivity()).log(event.getAccountId(), event); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.menu_status, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { MenuUtils.setMenuItemAvailability(menu, R.id.current_status, mStatusAdapter.getStatus() != null); super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.current_status: { if (mStatusAdapter.getStatus() != null) { final int position = mStatusAdapter.getFirstPositionOfItem(StatusAdapter.ITEM_IDX_STATUS); mRecyclerView.smoothScrollToPosition(position); } return true; } } return super.onOptionsItemSelected(item); } private void setConversation(List data) { final Pair readPosition = saveReadPosition(); mStatusAdapter.setConversation(data); restoreReadPosition(readPosition); } private void addConversation(ParcelableStatus status, int position) { mStatusAdapter.addConversation(status, position); } private StatusAdapter getAdapter() { return mStatusAdapter; } private DividerItemDecoration getItemDecoration() { return mItemDecoration; } private ParcelableStatus getStatus() { return mStatusAdapter.getStatus(); } 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.retweet_id > 0 ? status.retweeted_by_user_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 restoreReadPosition(@Nullable Pair position) { if (position == null) return; // FIXME We don't know why we need to -1 here, but only -1 works. final int adapterPosition = mStatusAdapter.findPositionById(position.first) - 1; if (adapterPosition < 0) return; //TODO maintain read position mLayoutManager.scrollToPositionWithOffset(adapterPosition, position.second); } @Nullable private Pair saveReadPosition() { final int position = mLayoutManager.findFirstVisibleItemPosition(); if (position < 0) return null; final int itemType = mStatusAdapter.getItemType(position); long itemId = mStatusAdapter.getItemId(position); final View positionView; if (itemType == StatusAdapter.ITEM_IDX_CONVERSATION_LOAD_MORE) { // Should be next item final int statusPosition = mStatusAdapter.getFirstPositionOfItem(StatusAdapter.ITEM_IDX_STATUS); positionView = mLayoutManager.findViewByPosition(statusPosition); itemId = mStatusAdapter.getItemId(statusPosition); } else { positionView = mLayoutManager.findViewByPosition(position); } return new Pair<>(itemId, positionView != null ? positionView.getTop() : -1); } private void setReplies(List data) { final Pair readPosition = saveReadPosition(); mStatusAdapter.setReplies(data); //TODO maintain read position restoreReadPosition(readPosition); } 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 void showConversationError(Exception exception) { } 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(); } } static class LoadConversationTask extends AsyncTask> { final Context context; final StatusFragment fragment; LoadConversationTask(final StatusFragment fragment) { context = fragment.getActivity(); this.fragment = fragment; } @Override protected ListResponse doInBackground(final ParcelableStatus... params) { final ArrayList list = new ArrayList<>(); try { ParcelableStatus status = params[0]; final long accountId = status.account_id; if (Utils.isOfficialKeyAccount(context, accountId)) { final Twitter twitter = TwitterAPIFactory.getTwitterInstance(context, accountId, true); while (status.in_reply_to_status_id > 0 && !isCancelled()) { final ParcelableStatus cached = Utils.findStatusInDatabases(context, accountId, status.in_reply_to_status_id); if (cached == null) break; status = cached; publishProgress(status); list.add(0, status); } final Paging paging = new Paging(); paging.setMaxId(status.id); final List conversations = new ArrayList<>(); for (org.mariotaku.twidere.api.twitter.model.Status item : twitter.showConversation(status.id, paging)) { if (item.getId() < status.id) { final ParcelableStatus conversation = new ParcelableStatus(item, accountId, false); publishProgress(conversation); conversations.add(conversation); } } list.addAll(0, conversations); } else { while (status.in_reply_to_status_id > 0 && !isCancelled()) { status = Utils.findStatus(context, accountId, 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 onPreExecute() { super.onPreExecute(); fragment.getAdapter().setConversationsLoading(true); } @Override protected void onPostExecute(final ListResponse data) { final StatusAdapter adapter = fragment.getAdapter(); if (data.hasData()) { fragment.setConversation(data.getData()); } else if (data.hasException()) { fragment.showConversationError(data.getException()); } else { Utils.showErrorMessage(context, context.getString(R.string.action_getting_status), data.getException(), true); } adapter.setConversationsLoading(false); } @Override protected void onProgressUpdate(ParcelableStatus... values) { for (ParcelableStatus status : values) { // fragment.addConversation(status, 0); } } @Override protected void onCancelled() { } } private static class DetailStatusViewHolder extends ViewHolder implements OnClickListener, ActionMenuView.OnMenuItemClickListener { private final StatusFragment fragment; private final StatusAdapter adapter; private final ActionMenuView menuBar; private final TextView nameView, screenNameView; private final StatusTextView textView; private final TextView quotedTextView; private final TextView quotedNameView, quotedScreenNameView; private final ImageView 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 TextView quoteOriginalLink; private final ColorLabelRelativeLayout profileContainer; 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 TwidereLinkify linkify; private final TextView favoritesLabel; public DetailStatusViewHolder(StatusFragment fragment, StatusAdapter adapter, View itemView) { super(itemView); this.fragment = fragment; this.linkClickHandler = new StatusLinkClickHandler(adapter.getContext(), null); this.linkify = new TwidereLinkify(linkClickHandler, false); this.adapter = adapter; 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 = (ImageView) 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); 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); quoteOriginalLink = (TextView) itemView.findViewById(R.id.quote_original_link); profileContainer = (ColorLabelRelativeLayout) itemView.findViewById(R.id.profile_container); twitterCard = (TwitterCardContainer) itemView.findViewById(R.id.twitter_card); favoritesLabel = (TextView) itemView.findViewById(R.id.favorites_label); quotedTextView = (TextView) itemView.findViewById(R.id.quoted_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 MediaLoaderWrapper loader = adapter.getMediaLoader(); final UserColorNameManager manager = adapter.getUserColorNameManager(); final boolean nameFirst = adapter.isNameFirst(); linkClickHandler.setStatus(status); if (status.retweet_id > 0) { final String retweetedBy = manager.getDisplayName(status.retweeted_by_user_id, status.retweeted_by_user_name, status.retweeted_by_user_screen_name, nameFirst, false); retweetedByView.setText(context.getString(R.string.name_retweeted, retweetedBy)); retweetedByView.setVisibility(View.VISIBLE); } else { retweetedByView.setText(null); retweetedByView.setVisibility(View.GONE); } profileContainer.drawEnd(Utils.getAccountColor(context, status.account_id)); final int layoutPosition = getLayoutPosition(); if (status.is_quote && ArrayUtils.isEmpty(status.media)) { quoteOriginalLink.setVisibility(View.VISIBLE); quotedNameContainer.setVisibility(View.VISIBLE); quotedTextView.setVisibility(View.VISIBLE); quoteIndicator.setVisibility(View.VISIBLE); quotedNameView.setText(manager.getUserNickname(status.quoted_user_id, status.quoted_user_name, false)); quotedScreenNameView.setText("@" + status.quoted_user_screen_name); final Spanned quotedText = HtmlSpanBuilder.fromHtml(status.quoted_text_html); if (!TextUtils.equals(quotedTextView.getText(), quotedText)) { quotedTextView.setText(quotedText); } linkify.applyAllLinks(quotedTextView, status.account_id, layoutPosition, status.is_possibly_sensitive); ThemeUtils.applyParagraphSpacing(quotedTextView, 1.1f); quoteIndicator.setColor(manager.getUserColor(status.user_id, false)); profileContainer.drawStart(manager.getUserColor(status.quoted_user_id, false)); } else { quoteOriginalLink.setVisibility(View.GONE); quotedNameContainer.setVisibility(View.GONE); quotedTextView.setVisibility(View.GONE); quoteIndicator.setVisibility(View.GONE); profileContainer.drawStart(manager.getUserColor(status.user_id, false)); } final long timestamp; if (status.is_retweet) { timestamp = status.retweet_timestamp; } else { timestamp = status.timestamp; } nameView.setText(manager.getUserNickname(status.user_id, status.user_name, false)); screenNameView.setText("@" + status.user_screen_name); loader.displayProfileImage(profileImageView, status.user_profile_image_url); final int typeIconRes = Utils.getUserTypeIconRes(status.user_is_verified, status.user_is_protected); final int typeDescriptionRes = Utils.getUserTypeDescriptionRes(status.user_is_verified, status.user_is_protected); 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 = Utils.formatToLongTimeString(context, timestamp); if (!TextUtils.isEmpty(timeString) && !TextUtils.isEmpty(status.source)) { timeSourceView.setText(HtmlSpanBuilder.fromHtml(context.getString(R.string.time_source, timeString, status.source))); } else if (TextUtils.isEmpty(timeString) && !TextUtils.isEmpty(status.source)) { timeSourceView.setText(HtmlSpanBuilder.fromHtml(context.getString(R.string.source, status.source))); } else if (!TextUtils.isEmpty(timeString) && TextUtils.isEmpty(status.source)) { timeSourceView.setText(timeString); } timeSourceView.setMovementMethod(LinkMovementMethod.getInstance()); final Spanned text = HtmlSpanBuilder.fromHtml(status.text_html); if (!TextUtils.equals(textView.getText(), text)) textView.setText(text); linkify.applyAllLinks(textView, status.account_id, layoutPosition, 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); favoritesContainer.setVisibility(status.favorite_count < 0 ? View.GONE : View.VISIBLE); final Locale locale = context.getResources().getConfiguration().locale; repliesCountView.setText(Utils.getLocalizedNumber(locale, status.reply_count)); retweetsCountView.setText(Utils.getLocalizedNumber(locale, status.retweet_count)); favoritesCountView.setText(Utils.getLocalizedNumber(locale, status.favorite_count)); final ParcelableMedia[] media = Utils.getPrimaryMedia(status); if (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(media, loader, status.account_id, adapter.getFragment(), adapter.getMediaLoadingHandler()); } 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(); if (cardFragment != null && !FragmentManagerTrojan.isStateSaved(fm)) { final FragmentTransaction ft = fm.beginTransaction(); ft.replace(R.id.twitter_card, cardFragment); ft.commit(); } else { twitterCard.setVisibility(View.GONE); } } else { twitterCard.setVisibility(View.GONE); } Utils.setMenuForStatus(context, fragment.mPreferences, menuBar.getMenu(), status, adapter.getStatusAccount()); textView.setTextIsSelectable(true); quotedTextView.setTextIsSelectable(true); textView.setMovementMethod(LinkMovementMethod.getInstance()); quotedTextView.setMovementMethod(LinkMovementMethod.getInstance()); } @Override public void onClick(View v) { final ParcelableStatus status = adapter.getStatus(getLayoutPosition()); final StatusFragment fragment = adapter.getFragment(); if (status == null || fragment == null) return; 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(profileImageView, UserFragment.TRANSITION_NAME_PROFILE_IMAGE), new Pair(profileTypeView, UserFragment.TRANSITION_NAME_PROFILE_TYPE)); Utils.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.favorites_container: { final FragmentActivity activity = fragment.getActivity(); if (!Utils.isOfficialCredentials(activity, adapter.getStatusAccount())) return; if (status.is_retweet) { Utils.openStatusFavoriters(activity, status.account_id, status.retweet_id); } else { Utils.openStatusFavoriters(activity, status.account_id, status.id); } break; } case R.id.retweeted_by: { if (status.retweet_id > 0) { Utils.openUserProfile(adapter.getContext(), status.account_id, status.retweeted_by_user_id, status.retweeted_by_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; } case R.id.quoted_name_container: { Utils.openUserProfile(adapter.getContext(), status.account_id, status.quoted_user_id, status.quoted_user_screen_name, null); break; } case R.id.quote_original_link: { Utils.openStatus(adapter.getContext(), status.account_id, status.quoted_id); } } } @Override public boolean onMenuItemClick(MenuItem item) { final int layoutPosition = getLayoutPosition(); if (layoutPosition < 0) return false; final StatusFragment fragment = adapter.getFragment(); final ParcelableStatus status = adapter.getStatus(layoutPosition); if (status == null || fragment == null) return false; final AsyncTwitterWrapper twitter = fragment.mTwitterWrapper; final FragmentActivity activity = fragment.getActivity(); final FragmentManager fm = fragment.getFragmentManager(); if (item.getItemId() == R.id.retweet) { RetweetQuoteDialogFragment.show(fm, status); return true; } 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_detail_status, menuBar.getMenu()); ThemeUtils.wrapMenuIcon(menuBar, MENU_GROUP_STATUS_SHARE); mediaPreviewLoad.setOnClickListener(this); profileContainer.setOnClickListener(this); quotedNameContainer.setOnClickListener(this); retweetsContainer.setOnClickListener(this); favoritesContainer.setOnClickListener(this); retweetedByView.setOnClickListener(this); locationView.setOnClickListener(this); quoteOriginalLink.setOnClickListener(this); final float defaultTextSize = adapter.getTextSize(); nameView.setTextSize(defaultTextSize * 1.25f); quotedNameView.setTextSize(defaultTextSize * 1.25f); textView.setTextSize(defaultTextSize * 1.25f); quotedTextView.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()); quotedTextView.setCustomSelectionActionModeCallback(new StatusActionModeCallback(quotedTextView, activity)); textView.setCustomSelectionActionModeCallback(new StatusActionModeCallback(textView, activity)); if (adapter.shouldUseStarsForLikes()) { favoritesLabel.setText(R.string.favorites); } } } private static class SpaceViewHolder extends ViewHolder { public SpaceViewHolder(View itemView) { super(itemView); } } private static class StatusAdapter extends BaseRecyclerViewAdapter implements IStatusesAdapter> { private static final int VIEW_TYPE_LIST_STATUS = 0; private static final int VIEW_TYPE_DETAIL_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_REPLY_ERROR = 4; private static final int VIEW_TYPE_SPACE = 5; private static final int ITEM_IDX_CONVERSATION_ERROR = 0; private static final int ITEM_IDX_CONVERSATION_LOAD_MORE = 1; private static final int ITEM_IDX_CONVERSATION = 2; private static final int ITEM_IDX_STATUS = 3; private static final int ITEM_IDX_REPLY = 4; private static final int ITEM_IDX_REPLY_LOAD_MORE = 5; private static final int ITEM_IDX_REPLY_ERROR = 6; private static final int ITEM_IDX_SPACE = 7; private static final int ITEM_TYPES_SUM = 8; private final StatusFragment mFragment; private final LayoutInflater mInflater; private final MediaLoadingHandler mMediaLoadingHandler; private final TwidereLinkify mTwidereLinkify; private final int[] mItemCounts; 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 mLinkHighlightingStyle; private final boolean mDisplayMediaPreview; private final boolean mDisplayProfileImage; private final boolean mSensitiveContentEnabled; private final boolean mHideCardActions; private final boolean mUseStarsForLikes; private boolean mLoadMoreSupported; private boolean mLoadMoreIndicatorVisible; private boolean mDetailMediaExpanded; private ParcelableStatus mStatus; private ParcelableCredentials mStatusAccount; private List mConversation, mReplies; private StatusAdapterListener mStatusAdapterListener; private RecyclerView mRecyclerView; private CharSequence mReplyError; public StatusAdapter(StatusFragment fragment, boolean compact) { super(fragment.getContext()); setHasStableIds(true); final Context context = fragment.getActivity(); final Resources res = context.getResources(); mItemCounts = new int[ITEM_TYPES_SUM]; // There's always a space at the end of the list mItemCounts[ITEM_IDX_SPACE] = 1; mFragment = fragment; mInflater = LayoutInflater.from(context); mMediaLoadingHandler = new MediaLoadingHandler(R.id.media_preview_progress); mCardBackgroundColor = ThemeUtils.getCardBackgroundColor(context, ThemeUtils.getThemeBackgroundOption(context), ThemeUtils.getUserThemeBackgroundAlpha(context)); mNameFirst = mPreferences.getBoolean(KEY_NAME_FIRST, true); mTextSize = mPreferences.getInt(KEY_TEXT_SIZE, res.getInteger(R.integer.default_text_size)); mProfileImageStyle = Utils.getProfileImageStyle(mPreferences.getString(KEY_PROFILE_IMAGE_STYLE, null)); mMediaPreviewStyle = Utils.getMediaPreviewStyle(mPreferences.getString(KEY_MEDIA_PREVIEW_STYLE, null)); mLinkHighlightingStyle = Utils.getLinkHighlightingStyleInt(mPreferences.getString(KEY_LINK_HIGHLIGHT_OPTION, null)); mIsCompact = compact; mDisplayProfileImage = mPreferences.getBoolean(KEY_DISPLAY_PROFILE_IMAGE, true); mDisplayMediaPreview = mPreferences.getBoolean(KEY_MEDIA_PREVIEW, false); mSensitiveContentEnabled = mPreferences.getBoolean(KEY_DISPLAY_SENSITIVE_CONTENTS, false); mHideCardActions = mPreferences.getBoolean(KEY_HIDE_CARD_ACTIONS, false); mUseStarsForLikes = mPreferences.getBoolean(KEY_I_WANT_MY_STARS_BACK); if (compact) { mCardLayoutResource = R.layout.card_item_status_compact; } else { mCardLayoutResource = R.layout.card_item_status; } mTwidereLinkify = new TwidereLinkify(new StatusAdapterLinkClickHandler<>(this)); } public void addConversation(ParcelableStatus status, int position) { if (mConversation == null) { mConversation = new ArrayList<>(); } mConversation.add(position, status); mItemCounts[ITEM_IDX_CONVERSATION] = mConversation.size(); 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; } @Override public int getProfileImageStyle() { return mProfileImageStyle; } @Override public float getTextSize() { return mTextSize; } @NonNull @Override public AsyncTwitterWrapper getTwitterWrapper() { return mFragment.mTwitterWrapper; } @Override public boolean isProfileImageEnabled() { return mDisplayProfileImage; } @NonNull @Override public MediaLoaderWrapper getMediaLoader() { return mMediaLoader; } public StatusFragment getFragment() { return mFragment; } @Override public int getLinkHighlightingStyle() { return mLinkHighlightingStyle; } @Override public int getMediaPreviewStyle() { return mMediaPreviewStyle; } @Override public ParcelableStatus getStatus(int position) { final int itemStart = getItemTypeStart(position); final int itemType = getItemType(position); return getStatusByItemType(position, itemStart, itemType); } private ParcelableStatus getStatusByItemType(int position, int itemStart, int itemType) { switch (itemType) { case ITEM_IDX_CONVERSATION: { return mConversation != null ? mConversation.get(position - itemStart) : null; } case ITEM_IDX_REPLY: { return mReplies != null ? mReplies.get(position - itemStart) : null; } case ITEM_IDX_STATUS: { return mStatus; } } return null; } @Override public long getStatusId(int position) { final ParcelableStatus status = getStatus(position); return status != null ? status.hashCode() : position; } @Override public int getStatusesCount() { return mItemCounts[ITEM_IDX_CONVERSATION] + mItemCounts[ITEM_IDX_STATUS] + mItemCounts[ITEM_IDX_REPLY]; } @Override public TwidereLinkify getTwidereLinkify() { return mTwidereLinkify; } @Override public boolean isCardActionsHidden() { return mHideCardActions; } @Override public boolean isMediaPreviewEnabled() { return mDisplayMediaPreview; } @Override public boolean isNameFirst() { return mNameFirst; } @Override public boolean isSensitiveContentEnabled() { return mSensitiveContentEnabled; } @Override public void setData(List data) { } @Override public boolean shouldShowAccountsColor() { return false; } @Override public boolean shouldUseStarsForLikes() { return mUseStarsForLikes; } @Override public MediaLoadingHandler getMediaLoadingHandler() { return mMediaLoadingHandler; } @NonNull @Override public UserColorNameManager getUserColorNameManager() { return mUserColorNameManager; } public ParcelableStatus getStatus() { return mStatus; } public ParcelableCredentials getStatusAccount() { return mStatusAccount; } public boolean isDetailMediaExpanded() { if (mDetailMediaExpanded) return true; if (mDisplayMediaPreview) { final ParcelableStatus status = mStatus; return status != null && (mSensitiveContentEnabled || !status.is_possibly_sensitive); } return false; } 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 isLoadMoreIndicatorVisible() { return mLoadMoreIndicatorVisible; } @Override public void setLoadMoreIndicatorVisible(boolean enabled) { if (mLoadMoreIndicatorVisible == enabled) return; mLoadMoreIndicatorVisible = enabled && mLoadMoreSupported; updateItemDecoration(); notifyDataSetChanged(); } @Override public boolean isLoadMoreSupported() { return mLoadMoreSupported; } @Override public void setLoadMoreSupported(boolean supported) { mLoadMoreSupported = supported; if (!supported) { mLoadMoreIndicatorVisible = false; } notifyDataSetChanged(); updateItemDecoration(); } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_DETAIL_STATUS: { 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(mFragment, 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(getContext())); } case VIEW_TYPE_REPLY_ERROR: { final View view = mInflater.inflate(R.layout.adapter_item_status_error, parent, false); return new StatusErrorItemViewHolder(view); } } return null; } @Override public void onBindViewHolder(ViewHolder holder, int position) { final int itemType = getItemType(position); final int itemViewType = getItemViewTypeByItemType(itemType); switch (itemViewType) { 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 IStatusViewHolder statusHolder = (IStatusViewHolder) holder; // Display 'in reply to' for first item // useful to indicate whether first tweet has reply or not // We only display that indicator for first conversation item statusHolder.displayStatus(status, itemType == ITEM_IDX_CONVERSATION && (position - getItemTypeStart(position)) == 0); break; } case VIEW_TYPE_REPLY_ERROR: { final StatusErrorItemViewHolder errorHolder = (StatusErrorItemViewHolder) holder; errorHolder.showError(mReplyError); break; } } } @Override public int getItemViewType(int position) { return getItemViewTypeByItemType(getItemType(position)); } private int getItemViewTypeByItemType(int type) { switch (type) { case ITEM_IDX_CONVERSATION: case ITEM_IDX_REPLY: return VIEW_TYPE_LIST_STATUS; case ITEM_IDX_CONVERSATION_LOAD_MORE: return VIEW_TYPE_CONVERSATION_LOAD_INDICATOR; case ITEM_IDX_REPLY_LOAD_MORE: return VIEW_TYPE_REPLIES_LOAD_INDICATOR; case ITEM_IDX_STATUS: return VIEW_TYPE_DETAIL_STATUS; case ITEM_IDX_SPACE: return VIEW_TYPE_SPACE; case ITEM_IDX_REPLY_ERROR: return VIEW_TYPE_REPLY_ERROR; } throw new IllegalStateException(); } private int getItemType(int position) { int typeStart = 0; for (int type = 0; type < ITEM_TYPES_SUM; type++) { int typeCount = mItemCounts[type]; final int typeEnd = typeStart + typeCount; if (position >= typeStart && position < typeEnd) return type; typeStart = typeEnd; } throw new IllegalStateException("Unknown position " + position); } private int getItemTypeStart(int position) { int typeStart = 0; for (int type = 0; type < ITEM_TYPES_SUM; type++) { int typeCount = mItemCounts[type]; final int typeEnd = typeStart + typeCount; if (position >= typeStart && position < typeEnd) return typeStart; typeStart = typeEnd; } throw new IllegalStateException(); } @Override public long getItemId(int position) { final ParcelableStatus status = getStatus(position); if (status != null) return status.id; return getItemType(position); } @Override public int getItemCount() { return MathUtils.sum(mItemCounts); } @Override public void onAttachedToRecyclerView(RecyclerView recyclerView) { super.onAttachedToRecyclerView(recyclerView); mRecyclerView = recyclerView; } @Override public void onDetachedFromRecyclerView(RecyclerView recyclerView) { super.onDetachedFromRecyclerView(recyclerView); mRecyclerView = null; } @Override public void onItemActionClick(ViewHolder holder, int id, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onStatusActionClick((IStatusViewHolder) holder, id, position); } } @Override public void onItemMenuClick(ViewHolder holder, View itemView, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onStatusMenuClick((IStatusViewHolder) holder, itemView, position); } } @Override public void onMediaClick(IStatusViewHolder holder, View view, ParcelableMedia media, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onMediaClick(holder, view, media, position); } } @Override public final void onStatusClick(IStatusViewHolder holder, int position) { if (mStatusAdapterListener != null) { mStatusAdapterListener.onStatusClick(holder, position); } } @Override public boolean onStatusLongClick(IStatusViewHolder holder, int position) { return false; } @Override public void onUserProfileClick(IStatusViewHolder 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); } } public void setConversation(List conversation) { mConversation = conversation; setCount(ITEM_IDX_CONVERSATION, conversation != null ? conversation.size() : 0); updateItemDecoration(); } private void setCount(int idx, int size) { mItemCounts[idx] = size; notifyDataSetChanged(); } public void setEventListener(StatusAdapterListener listener) { mStatusAdapterListener = listener; } public void setReplyError(CharSequence error) { mReplyError = error; setCount(ITEM_IDX_REPLY_ERROR, error != null ? 1 : 0); updateItemDecoration(); } public void setReplies(List replies) { mReplies = replies; setCount(ITEM_IDX_REPLY, replies != null ? replies.size() : 0); updateItemDecoration(); } public boolean setStatus(final ParcelableStatus status, final ParcelableCredentials credentials) { final ParcelableStatus old = mStatus; mStatus = status; mStatusAccount = credentials; setCount(ITEM_IDX_STATUS, status != null ? 1 : 0); updateItemDecoration(); return !CompareUtils.objectEquals(old, status); } private void updateItemDecoration() { if (mRecyclerView == null) return; final DividerItemDecoration decoration = mFragment.getItemDecoration(); decoration.setDecorationStart(0); if (mReplies == null) { decoration.setDecorationEndOffset(2); } else { decoration.setDecorationEndOffset(1); } mRecyclerView.invalidateItemDecorations(); } public void setRepliesLoading(boolean loading) { setCount(ITEM_IDX_REPLY_LOAD_MORE, loading ? 1 : 0); updateItemDecoration(); } public void setConversationsLoading(boolean loading) { setCount(ITEM_IDX_CONVERSATION_LOAD_MORE, loading ? 1 : 0); updateItemDecoration(); } public int getFirstPositionOfItem(int itemIdx) { int position = 0; for (int i = 0; i < ITEM_TYPES_SUM; i++) { if (itemIdx == i) return position; position += mItemCounts[i]; } return RecyclerView.NO_POSITION; } public static class StatusErrorItemViewHolder extends ViewHolder { private final TextView textView; public StatusErrorItemViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(android.R.id.text1); textView.setMovementMethod(LinkMovementMethod.getInstance()); textView.setLinksClickable(true); } public void showError(CharSequence text) { textView.setText(text); } } } 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_SPACE) { break; } if (typeToMeasure == StatusAdapter.VIEW_TYPE_DETAIL_STATUS || heightBeforeSpace != 0) { heightBeforeSpace += super.getDecoratedMeasuredHeight(childToMeasure) + paramsToMeasure.topMargin + paramsToMeasure.bottomMargin; } } 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); } } }