| /* |
| * Copyright (C) 2012 Google Inc. |
| * Licensed to The Android Open Source Project. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.mail.ui; |
| |
| import android.animation.Animator.AnimatorListener; |
| import android.app.Activity; |
| import android.app.ListFragment; |
| import android.content.res.Resources; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Parcelable; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewGroup.MarginLayoutParams; |
| import android.widget.AdapterView; |
| import android.widget.AdapterView.OnItemLongClickListener; |
| import android.widget.ListView; |
| import android.widget.TextView; |
| |
| import com.android.mail.R; |
| import com.android.mail.ConversationListContext; |
| import com.android.mail.browse.ConversationItemView.StarHandler; |
| import com.android.mail.ui.ViewMode.ModeChangeListener; |
| import com.android.mail.utils.LogUtils; |
| import com.android.mail.utils.Utils; |
| |
| /** |
| * The conversation list UI component. |
| */ |
| public final class ConversationListFragment extends ListFragment |
| implements ConversationSetObserver, |
| OnItemLongClickListener, |
| ModeChangeListener { |
| // Keys used to pass data to {@link ConversationListFragment}. |
| private static final String CONVERSATION_LIST_KEY = "conversation-list"; |
| |
| // Key used to keep track of the scroll state of the list. |
| private static final String LIST_STATE_KEY = "list-state"; |
| |
| private static final String LOG_TAG = new LogUtils().getLogTag(); |
| |
| // True if we are on a tablet device |
| private static boolean mTabletDevice; |
| |
| /** |
| * Frequency of update of timestamps. Initialized in {@link #onCreate(Bundle)} and final |
| * afterwards. |
| */ |
| private static int TIMESTAMP_UPDATE_INTERVAL = 0; |
| |
| private ControllableActivity mActivity; |
| |
| private boolean mAnimateChanges; |
| // Control state. |
| private ConversationListCallbacks mCallbacks; |
| |
| private View mEmptyView; |
| private final Handler mHandler = new Handler(); |
| // True if the view is in CAB (Contextual Action Bar: some conversations are selected) mode |
| private boolean mIsCabMode; |
| |
| // List save state. |
| private Parcelable mListSavedState; |
| // The internal view objects. |
| private ListView mListView; |
| private int mPosition = ListView.INVALID_POSITION; |
| |
| private TextView mSearchResultCountTextView; |
| private TextView mSearchStatusTextView; |
| |
| private View mSearchStatusView; |
| private int mSelectedCursorPosition = mPosition; |
| |
| private AnimatorListener mUndoHideListener; |
| |
| /** |
| * A simple method to update the timestamps of conversations periodically. |
| */ |
| private Runnable mUpdateTimestampsRunnable = null; |
| |
| private ConversationListContext mViewContext; |
| |
| /** |
| * Creates a new instance of {@link ConversationListFragment}, initialized to display |
| * conversation list context. |
| */ |
| public static ConversationListFragment newInstance(ConversationListContext viewContext) { |
| ConversationListFragment fragment = new ConversationListFragment(); |
| Bundle args = new Bundle(); |
| args.putBundle(CONVERSATION_LIST_KEY, viewContext.toBundle()); |
| fragment.setArguments(args); |
| return fragment; |
| } |
| |
| /** |
| * Initializes all internal state for a rendering. |
| */ |
| private void bindActivityInfo() { |
| final Activity activity = getActivity(); |
| mCallbacks = (ConversationListCallbacks) activity; |
| StarHandler starHandler = (StarHandler) activity; |
| mActivity.setViewModeListener(this); |
| mActivity.getBatchConversations().addObserver(this); |
| |
| // TODO(mindyp): find some way to make the notification container more re-usable. |
| // TODO(viki): refactor according to comment in configureSearchResultHandler() |
| mSearchStatusView = activity.findViewById(R.id.search_status_view); |
| mSearchStatusTextView = (TextView) activity.findViewById(R.id.search_status_text_view); |
| mSearchResultCountTextView = (TextView) activity |
| .findViewById(R.id.search_result_count_view); |
| } |
| |
| /** |
| * Show the header if the current conversation list is showing search results. |
| */ |
| private void configureSearchResultHeader() { |
| // Only show the header if the context is for a search result |
| final Resources res = getResources(); |
| final boolean showHeader = isSearchResult(); |
| // TODO(viki): This code contains intimate understanding of the view. Much of this logic |
| // needs to reside in a separate class that handles the text view in isolation. Then, |
| // that logic can be reused in other fragments. |
| if (showHeader) { |
| mSearchStatusTextView.setText(res.getString(R.string.search_results_searching_header)); |
| // Initially reset the count |
| mSearchResultCountTextView.setText(""); |
| } |
| mSearchStatusView.setVisibility(showHeader ? View.VISIBLE : View.GONE); |
| int marginTop = showHeader ? (int) res.getDimension(R.dimen.notification_view_height) : 0; |
| MarginLayoutParams layoutParams = (MarginLayoutParams) mListView.getLayoutParams(); |
| layoutParams.topMargin = marginTop; |
| mListView.setLayoutParams(layoutParams); |
| } |
| |
| private boolean isSearchResult() { |
| return mViewContext != null && mViewContext.isSearchResult(); |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| LogUtils.v(LOG_TAG, "onActivityCreated in ConversationListFragment(this=%s)", |
| this); |
| super.onActivityCreated(savedInstanceState); |
| mActivity = (ControllableActivity) getActivity(); |
| mActivity.attachConversationList(this); |
| mTabletDevice = Utils.useTabletUI(mActivity.getApplicationContext()); |
| bindActivityInfo(); |
| |
| // The onViewModeChanged callback doesn't get called when the mode object is created, so |
| // force setting the mode manually this time around. |
| onViewModeChanged(mActivity.getViewMode()); |
| |
| if (mActivity.isFinishing()) { |
| // Activity is finishing, just bail. |
| return; |
| } |
| |
| // Show list and start loading list. |
| showList(); |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| LogUtils.v(LOG_TAG, "onCreate in ConversationListFragment(this=%s)", this); |
| super.onCreate(savedInstanceState); |
| |
| // Initialize fragment constants from resources |
| Resources res = getResources(); |
| TIMESTAMP_UPDATE_INTERVAL = res.getInteger(R.integer.timestamp_update_interval); |
| mUpdateTimestampsRunnable = new Runnable(){ |
| @Override |
| public void run() { |
| mListView.invalidateViews(); |
| mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL); |
| } |
| }; |
| |
| Bundle args = getArguments(); |
| mViewContext = ConversationListContext.forBundle(args.getBundle(CONVERSATION_LIST_KEY)); |
| if (savedInstanceState != null) { |
| mListSavedState = savedInstanceState.getParcelable(LIST_STATE_KEY); |
| } |
| |
| setRetainInstance(true); |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, |
| ViewGroup container, Bundle savedInstanceState) { |
| LogUtils.v(LOG_TAG, "onCreateView in ConversationListFragment(this=%s)", this); |
| View rootView = inflater.inflate(R.layout.conversation_list, null); |
| |
| mEmptyView = rootView.findViewById(R.id.empty_view); |
| |
| mListView = (ListView) rootView.findViewById(android.R.id.list); |
| mListView.setHeaderDividersEnabled(false); |
| mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); |
| mListView.setOnItemLongClickListener(this); |
| |
| // Note - we manually save/restore the listview state. |
| mListView.setSaveEnabled(false); |
| |
| return rootView; |
| } |
| |
| @Override |
| public void onDestroyView() { |
| // Clear the selected position and save list state manually so we can restore it after data |
| // has finished loading. |
| if (mPosition != ListView.INVALID_POSITION) { |
| mListView.setItemChecked(mPosition, false); |
| } |
| mListSavedState = mListView.onSaveInstanceState(); |
| |
| // Clear the adapter. |
| mListView.setAdapter(null); |
| |
| mActivity.unsetViewModeListener(this); |
| mActivity.attachConversationList(null); |
| mActivity.getBatchConversations().removeObserver(this); |
| |
| if (!mActivity.isChangingConfigurations()) { |
| mActivity.getLoaderManager().destroyLoader(mViewContext.hashCode()); |
| } |
| |
| super.onDestroyView(); |
| } |
| |
| @Override |
| public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { |
| // TODO(viki): Add some functionality here. |
| return true; |
| } |
| |
| @Override |
| public void onListItemClick(ListView l, View v, int position, long id) { |
| viewConversation(position); |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| if (mListView != null) { |
| outState.putParcelable(LIST_STATE_KEY, mListView.onSaveInstanceState()); |
| } |
| } |
| |
| @Override |
| public void onSetChanged(ConversationSelectionSet set) { |
| // No-op, since all set operations will cause a list refresh anyways, except for batch |
| // operations like "select all" or "deselect all" which are done through menus external |
| // to the list. |
| } |
| |
| @Override |
| public void onSetEmpty() { |
| mIsCabMode = false; |
| refreshList(); |
| } |
| |
| @Override |
| public void onSetPopulated(ConversationSelectionSet set) { |
| mIsCabMode = true; |
| refreshList(); |
| } |
| |
| @Override |
| public void onStart() { |
| LogUtils.v(LOG_TAG, "onStart in ConversationListFragment(this=%s)", this); |
| super.onStart(); |
| |
| mHandler.postDelayed(mUpdateTimestampsRunnable, TIMESTAMP_UPDATE_INTERVAL); |
| } |
| |
| @Override |
| public void onStop() { |
| LogUtils.v(LOG_TAG, "onStop in ConversationListFragment(this=%s)", this); |
| super.onStop(); |
| |
| mHandler.removeCallbacks(mUpdateTimestampsRunnable); |
| } |
| |
| public void onTouchEvent(MotionEvent event) { |
| } |
| |
| @Override |
| public void onViewModeChanged(int newMode) { |
| // Change the divider based on view mode. |
| if (mTabletDevice) { |
| if (newMode == ViewMode.CONVERSATION) { |
| mListView.setBackgroundResource(R.drawable.panel_conversation_leftstroke); |
| } else { |
| mListView.setBackgroundDrawable(null); |
| } |
| mAnimateChanges = true; |
| } else { |
| mListView.setBackgroundDrawable(null); |
| mAnimateChanges = newMode == ViewMode.CONVERSATION_LIST; |
| } |
| } |
| |
| /** |
| * Visually refresh the conversation list. This does not start a sync. |
| */ |
| private void refreshList() { |
| // Do nothing for now. |
| } |
| |
| /** |
| * Handles a request to show a new conversation list, either from a search query or for viewing |
| * a label. This will initiate a data load, and hence must be called on the UI thread. |
| */ |
| private void showList() { |
| configureSearchResultHeader(); |
| } |
| |
| public void updateSelectedPosition() { |
| if (mPosition != mSelectedCursorPosition) { |
| // Clear temporary selection position. This would occur |
| // if the position was after items that were being destroyed. |
| mListView.setItemChecked(mPosition, false); |
| mPosition = mSelectedCursorPosition; |
| mListView.setItemChecked(mPosition, true); |
| } |
| } |
| |
| /** |
| * View the message at the given position. |
| * @param position |
| */ |
| private void viewConversation(int position){ |
| mCallbacks.onConversationSelected(position); |
| } |
| } |