Home | History | Annotate | Download | only in conversationlist
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.messaging.ui.conversationlist;
     17 
     18 import android.app.Activity;
     19 import android.app.Fragment;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.graphics.Rect;
     23 import android.net.Uri;
     24 import android.os.Bundle;
     25 import android.os.Parcelable;
     26 import android.support.v4.view.ViewCompat;
     27 import android.support.v4.view.ViewGroupCompat;
     28 import android.support.v7.widget.LinearLayoutManager;
     29 import android.support.v7.widget.RecyclerView;
     30 import android.view.LayoutInflater;
     31 import android.view.Menu;
     32 import android.view.MenuInflater;
     33 import android.view.MenuItem;
     34 import android.view.View;
     35 import android.view.View.OnClickListener;
     36 import android.view.ViewGroup;
     37 import android.view.ViewGroup.MarginLayoutParams;
     38 import android.view.ViewPropertyAnimator;
     39 import android.view.accessibility.AccessibilityManager;
     40 import android.widget.AbsListView;
     41 import android.widget.ImageView;
     42 
     43 import com.android.messaging.R;
     44 import com.android.messaging.annotation.VisibleForAnimation;
     45 import com.android.messaging.datamodel.DataModel;
     46 import com.android.messaging.datamodel.binding.Binding;
     47 import com.android.messaging.datamodel.binding.BindingBase;
     48 import com.android.messaging.datamodel.data.ConversationListData;
     49 import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
     50 import com.android.messaging.datamodel.data.ConversationListItemData;
     51 import com.android.messaging.ui.BugleAnimationTags;
     52 import com.android.messaging.ui.ListEmptyView;
     53 import com.android.messaging.ui.SnackBarInteraction;
     54 import com.android.messaging.ui.UIIntents;
     55 import com.android.messaging.util.AccessibilityUtil;
     56 import com.android.messaging.util.Assert;
     57 import com.android.messaging.util.ImeUtil;
     58 import com.android.messaging.util.LogUtil;
     59 import com.android.messaging.util.UiUtils;
     60 import com.google.common.annotations.VisibleForTesting;
     61 
     62 import java.util.ArrayList;
     63 import java.util.List;
     64 
     65 /**
     66  * Shows a list of conversations.
     67  */
     68 public class ConversationListFragment extends Fragment implements ConversationListDataListener,
     69         ConversationListItemView.HostInterface {
     70     private static final String BUNDLE_ARCHIVED_MODE = "archived_mode";
     71     private static final String BUNDLE_FORWARD_MESSAGE_MODE = "forward_message_mode";
     72     private static final boolean VERBOSE = false;
     73 
     74     private MenuItem mShowBlockedMenuItem;
     75     private boolean mArchiveMode;
     76     private boolean mBlockedAvailable;
     77     private boolean mForwardMessageMode;
     78 
     79     public interface ConversationListFragmentHost {
     80         public void onConversationClick(final ConversationListData listData,
     81                                         final ConversationListItemData conversationListItemData,
     82                                         final boolean isLongClick,
     83                                         final ConversationListItemView conversationView);
     84         public void onCreateConversationClick();
     85         public boolean isConversationSelected(final String conversationId);
     86         public boolean isSwipeAnimatable();
     87         public boolean isSelectionMode();
     88         public boolean hasWindowFocus();
     89     }
     90 
     91     private ConversationListFragmentHost mHost;
     92     private RecyclerView mRecyclerView;
     93     private ImageView mStartNewConversationButton;
     94     private ListEmptyView mEmptyListMessageView;
     95     private ConversationListAdapter mAdapter;
     96 
     97     // Saved Instance State Data - only for temporal data which is nice to maintain but not
     98     // critical for correctness.
     99     private static final String SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY =
    100             "conversationListViewState";
    101     private Parcelable mListState;
    102 
    103     @VisibleForTesting
    104     final Binding<ConversationListData> mListBinding = BindingBase.createBinding(this);
    105 
    106     public static ConversationListFragment createArchivedConversationListFragment() {
    107         return createConversationListFragment(BUNDLE_ARCHIVED_MODE);
    108     }
    109 
    110     public static ConversationListFragment createForwardMessageConversationListFragment() {
    111         return createConversationListFragment(BUNDLE_FORWARD_MESSAGE_MODE);
    112     }
    113 
    114     public static ConversationListFragment createConversationListFragment(String modeKeyName) {
    115         final ConversationListFragment fragment = new ConversationListFragment();
    116         final Bundle bundle = new Bundle();
    117         bundle.putBoolean(modeKeyName, true);
    118         fragment.setArguments(bundle);
    119         return fragment;
    120     }
    121 
    122     /**
    123      * {@inheritDoc} from Fragment
    124      */
    125     @Override
    126     public void onCreate(final Bundle bundle) {
    127         super.onCreate(bundle);
    128         mListBinding.getData().init(getLoaderManager(), mListBinding);
    129         mAdapter = new ConversationListAdapter(getActivity(), null, this);
    130     }
    131 
    132     @Override
    133     public void onResume() {
    134         super.onResume();
    135 
    136         Assert.notNull(mHost);
    137         setScrolledToNewestConversationIfNeeded();
    138 
    139         updateUi();
    140     }
    141 
    142     public void setScrolledToNewestConversationIfNeeded() {
    143         if (!mArchiveMode
    144                 && !mForwardMessageMode
    145                 && isScrolledToFirstConversation()
    146                 && mHost.hasWindowFocus()) {
    147             mListBinding.getData().setScrolledToNewestConversation(true);
    148         }
    149     }
    150 
    151     private boolean isScrolledToFirstConversation() {
    152         int firstItemPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager())
    153                 .findFirstCompletelyVisibleItemPosition();
    154         return firstItemPosition == 0;
    155     }
    156 
    157     /**
    158      * {@inheritDoc} from Fragment
    159      */
    160     @Override
    161     public void onDestroy() {
    162         super.onDestroy();
    163         mListBinding.unbind();
    164         mHost = null;
    165     }
    166 
    167     /**
    168      * {@inheritDoc} from Fragment
    169      */
    170     @Override
    171     public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
    172             final Bundle savedInstanceState) {
    173         final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.conversation_list_fragment,
    174                 container, false);
    175         mRecyclerView = (RecyclerView) rootView.findViewById(android.R.id.list);
    176         mEmptyListMessageView = (ListEmptyView) rootView.findViewById(R.id.no_conversations_view);
    177         mEmptyListMessageView.setImageHint(R.drawable.ic_oobe_conv_list);
    178         // The default behavior for default layout param generation by LinearLayoutManager is to
    179         // provide width and height of WRAP_CONTENT, but this is not desirable for
    180         // ConversationListFragment; the view in each row should be a width of MATCH_PARENT so that
    181         // the entire row is tappable.
    182         final Activity activity = getActivity();
    183         final LinearLayoutManager manager = new LinearLayoutManager(activity) {
    184             @Override
    185             public RecyclerView.LayoutParams generateDefaultLayoutParams() {
    186                 return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    187                         ViewGroup.LayoutParams.WRAP_CONTENT);
    188             }
    189         };
    190         mRecyclerView.setLayoutManager(manager);
    191         mRecyclerView.setHasFixedSize(true);
    192         mRecyclerView.setAdapter(mAdapter);
    193         mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
    194             int mCurrentState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
    195 
    196             @Override
    197             public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
    198                 if (mCurrentState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL
    199                         || mCurrentState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
    200                     ImeUtil.get().hideImeKeyboard(getActivity(), mRecyclerView);
    201                 }
    202 
    203                 if (isScrolledToFirstConversation()) {
    204                     setScrolledToNewestConversationIfNeeded();
    205                 } else {
    206                     mListBinding.getData().setScrolledToNewestConversation(false);
    207                 }
    208             }
    209 
    210             @Override
    211             public void onScrollStateChanged(final RecyclerView recyclerView, final int newState) {
    212                 mCurrentState = newState;
    213             }
    214         });
    215         mRecyclerView.addOnItemTouchListener(new ConversationListSwipeHelper(mRecyclerView));
    216 
    217         if (savedInstanceState != null) {
    218             mListState = savedInstanceState.getParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY);
    219         }
    220 
    221         mStartNewConversationButton = (ImageView) rootView.findViewById(
    222                 R.id.start_new_conversation_button);
    223         if (mArchiveMode) {
    224             mStartNewConversationButton.setVisibility(View.GONE);
    225         } else {
    226             mStartNewConversationButton.setVisibility(View.VISIBLE);
    227             mStartNewConversationButton.setOnClickListener(new OnClickListener() {
    228                 @Override
    229                 public void onClick(final View clickView) {
    230                     mHost.onCreateConversationClick();
    231                 }
    232             });
    233         }
    234         ViewCompat.setTransitionName(mStartNewConversationButton, BugleAnimationTags.TAG_FABICON);
    235 
    236         // The root view has a non-null background, which by default is deemed by the framework
    237         // to be a "transition group," where all child views are animated together during an
    238         // activity transition. However, we want each individual items in the recycler view to
    239         // show explode animation themselves, so we explicitly tag the root view to be a non-group.
    240         ViewGroupCompat.setTransitionGroup(rootView, false);
    241 
    242         setHasOptionsMenu(true);
    243         return rootView;
    244     }
    245 
    246     @Override
    247     public void onAttach(final Activity activity) {
    248         super.onAttach(activity);
    249         if (VERBOSE) {
    250             LogUtil.v(LogUtil.BUGLE_TAG, "Attaching List");
    251         }
    252         final Bundle arguments = getArguments();
    253         if (arguments != null) {
    254             mArchiveMode = arguments.getBoolean(BUNDLE_ARCHIVED_MODE, false);
    255             mForwardMessageMode = arguments.getBoolean(BUNDLE_FORWARD_MESSAGE_MODE, false);
    256         }
    257         mListBinding.bind(DataModel.get().createConversationListData(activity, this, mArchiveMode));
    258     }
    259 
    260 
    261     @Override
    262     public void onSaveInstanceState(final Bundle outState) {
    263         super.onSaveInstanceState(outState);
    264         if (mListState != null) {
    265             outState.putParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY, mListState);
    266         }
    267     }
    268 
    269     @Override
    270     public void onPause() {
    271         super.onPause();
    272         mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();
    273         mListBinding.getData().setScrolledToNewestConversation(false);
    274     }
    275 
    276     /**
    277      * Call this immediately after attaching the fragment
    278      */
    279     public void setHost(final ConversationListFragmentHost host) {
    280         Assert.isNull(mHost);
    281         mHost = host;
    282     }
    283 
    284     @Override
    285     public void onConversationListCursorUpdated(final ConversationListData data,
    286             final Cursor cursor) {
    287         mListBinding.ensureBound(data);
    288         final Cursor oldCursor = mAdapter.swapCursor(cursor);
    289         updateEmptyListUi(cursor == null || cursor.getCount() == 0);
    290         if (mListState != null && cursor != null && oldCursor == null) {
    291             mRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);
    292         }
    293     }
    294 
    295     @Override
    296     public void setBlockedParticipantsAvailable(final boolean blockedAvailable) {
    297         mBlockedAvailable = blockedAvailable;
    298         if (mShowBlockedMenuItem != null) {
    299             mShowBlockedMenuItem.setVisible(blockedAvailable);
    300         }
    301     }
    302 
    303     public void updateUi() {
    304         mAdapter.notifyDataSetChanged();
    305     }
    306 
    307     @Override
    308     public void onPrepareOptionsMenu(final Menu menu) {
    309         super.onPrepareOptionsMenu(menu);
    310         final MenuItem startNewConversationMenuItem =
    311                 menu.findItem(R.id.action_start_new_conversation);
    312         if (startNewConversationMenuItem != null) {
    313             // It is recommended for the Floating Action button functionality to be duplicated as a
    314             // menu
    315             AccessibilityManager accessibilityManager = (AccessibilityManager)
    316                     getActivity().getSystemService(Context.ACCESSIBILITY_SERVICE);
    317             startNewConversationMenuItem.setVisible(accessibilityManager
    318                     .isTouchExplorationEnabled());
    319         }
    320 
    321         final MenuItem archive = menu.findItem(R.id.action_show_archived);
    322         if (archive != null) {
    323             archive.setVisible(true);
    324         }
    325     }
    326 
    327     @Override
    328     public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
    329         if (!isAdded()) {
    330             // Guard against being called before we're added to the activity
    331             return;
    332         }
    333 
    334         mShowBlockedMenuItem = menu.findItem(R.id.action_show_blocked_contacts);
    335         if (mShowBlockedMenuItem != null) {
    336             mShowBlockedMenuItem.setVisible(mBlockedAvailable);
    337         }
    338     }
    339 
    340     /**
    341      * {@inheritDoc} from ConversationListItemView.HostInterface
    342      */
    343     @Override
    344     public void onConversationClicked(final ConversationListItemData conversationListItemData,
    345             final boolean isLongClick, final ConversationListItemView conversationView) {
    346         final ConversationListData listData = mListBinding.getData();
    347         mHost.onConversationClick(listData, conversationListItemData, isLongClick,
    348                 conversationView);
    349     }
    350 
    351     /**
    352      * {@inheritDoc} from ConversationListItemView.HostInterface
    353      */
    354     @Override
    355     public boolean isConversationSelected(final String conversationId) {
    356         return mHost.isConversationSelected(conversationId);
    357     }
    358 
    359     @Override
    360     public boolean isSwipeAnimatable() {
    361         return mHost.isSwipeAnimatable();
    362     }
    363 
    364     // Show and hide empty list UI as needed with appropriate text based on view specifics
    365     private void updateEmptyListUi(final boolean isEmpty) {
    366         if (isEmpty) {
    367             int emptyListText;
    368             if (!mListBinding.getData().getHasFirstSyncCompleted()) {
    369                 emptyListText = R.string.conversation_list_first_sync_text;
    370             } else if (mArchiveMode) {
    371                 emptyListText = R.string.archived_conversation_list_empty_text;
    372             } else {
    373                 emptyListText = R.string.conversation_list_empty_text;
    374             }
    375             mEmptyListMessageView.setTextHint(emptyListText);
    376             mEmptyListMessageView.setVisibility(View.VISIBLE);
    377             mEmptyListMessageView.setIsImageVisible(true);
    378             mEmptyListMessageView.setIsVerticallyCentered(true);
    379         } else {
    380             mEmptyListMessageView.setVisibility(View.GONE);
    381         }
    382     }
    383 
    384     @Override
    385     public List<SnackBarInteraction> getSnackBarInteractions() {
    386         final List<SnackBarInteraction> interactions = new ArrayList<SnackBarInteraction>(1);
    387         final SnackBarInteraction fabInteraction =
    388                 new SnackBarInteraction.BasicSnackBarInteraction(mStartNewConversationButton);
    389         interactions.add(fabInteraction);
    390         return interactions;
    391     }
    392 
    393     private ViewPropertyAnimator getNormalizedFabAnimator() {
    394         return mStartNewConversationButton.animate()
    395                 .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
    396                 .setDuration(getActivity().getResources().getInteger(
    397                         R.integer.fab_animation_duration_ms));
    398     }
    399 
    400     public ViewPropertyAnimator dismissFab() {
    401         // To prevent clicking while animating.
    402         mStartNewConversationButton.setEnabled(false);
    403         final MarginLayoutParams lp =
    404                 (MarginLayoutParams) mStartNewConversationButton.getLayoutParams();
    405         final float fabWidthWithLeftRightMargin = mStartNewConversationButton.getWidth()
    406                 + lp.leftMargin + lp.rightMargin;
    407         final int direction = AccessibilityUtil.isLayoutRtl(mStartNewConversationButton) ? -1 : 1;
    408         return getNormalizedFabAnimator().translationX(direction * fabWidthWithLeftRightMargin);
    409     }
    410 
    411     public ViewPropertyAnimator showFab() {
    412         return getNormalizedFabAnimator().translationX(0).withEndAction(new Runnable() {
    413             @Override
    414             public void run() {
    415                 // Re-enable clicks after the animation.
    416                 mStartNewConversationButton.setEnabled(true);
    417             }
    418         });
    419     }
    420 
    421     public View getHeroElementForTransition() {
    422         return mArchiveMode ? null : mStartNewConversationButton;
    423     }
    424 
    425     @VisibleForAnimation
    426     public RecyclerView getRecyclerView() {
    427         return mRecyclerView;
    428     }
    429 
    430     @Override
    431     public void startFullScreenPhotoViewer(
    432             final Uri initialPhoto, final Rect initialPhotoBounds, final Uri photosUri) {
    433         UIIntents.get().launchFullScreenPhotoViewer(
    434                 getActivity(), initialPhoto, initialPhotoBounds, photosUri);
    435     }
    436 
    437     @Override
    438     public void startFullScreenVideoViewer(final Uri videoUri) {
    439         UIIntents.get().launchFullScreenVideoViewer(getActivity(), videoUri);
    440     }
    441 
    442     @Override
    443     public boolean isSelectionMode() {
    444         return mHost != null && mHost.isSelectionMode();
    445     }
    446 }
    447