FolderListFragment.java revision e716c856d59cc6741e9d9fa16a77bbccd12530ab
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import android.app.Activity;
21import android.app.ListFragment;
22import android.app.LoaderManager;
23import android.content.CursorLoader;
24import android.content.Loader;
25import android.database.Cursor;
26import android.database.DataSetObserver;
27import android.net.Uri;
28import android.os.Bundle;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.ArrayAdapter;
33import android.widget.BaseAdapter;
34import android.widget.ImageView;
35import android.widget.ListAdapter;
36import android.widget.ListView;
37import android.widget.TextView;
38
39import com.android.mail.R;
40import com.android.mail.providers.Folder;
41import com.android.mail.providers.RecentFolderObserver;
42import com.android.mail.providers.UIProvider;
43import com.android.mail.utils.LogTag;
44import com.android.mail.utils.LogUtils;
45import com.android.mail.utils.Utils;
46
47import java.util.ArrayList;
48import java.util.List;
49
50/**
51 * The folder list UI component.
52 */
53public final class FolderListFragment extends ListFragment implements
54        LoaderManager.LoaderCallbacks<Cursor> {
55    private static final String LOG_TAG = LogTag.getLogTag();
56    /** The parent activity */
57    private ControllableActivity mActivity;
58    /** The underlying list view */
59    private ListView mListView;
60    /** URI that points to the list of folders for the current account. */
61    private Uri mFolderListUri;
62    /** True if you want a sectioned FolderList, false otherwise. */
63    private boolean mIsSectioned;
64    /** Callback into the parent */
65    private FolderListSelectionListener mListener;
66
67    /** The currently selected folder (the folder being viewed).  This is never null. */
68    private Uri mSelectedFolderUri = Uri.EMPTY;
69    /** Parent of the current folder, or null if the current folder is not a child. */
70    private Folder mParentFolder;
71
72    private static final int FOLDER_LOADER_ID = 0;
73    public static final int MODE_DEFAULT = 0;
74    public static final int MODE_PICK = 1;
75    /** Key to store {@link #mParentFolder}. */
76    private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
77    /** Key to store {@link #mFolderListUri}. */
78    private static final String ARG_FOLDER_URI = "arg-folder-list-uri";
79    /** Key to store {@link #mIsSectioned} */
80    private static final String ARG_IS_SECTIONED = "arg-is-sectioned";
81
82    private static final String BUNDLE_LIST_STATE = "flf-list-state";
83    private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
84
85    private FolderListFragmentCursorAdapter mCursorAdapter;
86    /** View that we show while we are waiting for the folder list to load */
87    private View mEmptyView;
88    /** Observer to wait for changes to the current folder so we can change the selected folder */
89    private FolderObserver mFolderObserver = null;
90
91    // Listen to folder changes from the controller and update state accordingly.
92    private class FolderObserver extends DataSetObserver {
93        @Override
94        public void onChanged() {
95            if (mActivity == null) {
96                return;
97            }
98            final FolderController controller = mActivity.getFolderController();
99            if (controller == null) {
100                return;
101            }
102            final Folder folder = controller.getFolder();
103            if (folder == null) {
104                return;
105            }
106            mSelectedFolderUri = folder.uri;
107        }
108    }
109
110    /**
111     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
112     */
113    public FolderListFragment() {
114        super();
115    }
116
117    @Override
118    public void onResume() {
119        Utils.dumpLayoutRequests("FLF(" + this + ").onResume()", getView());
120
121        super.onResume();
122        // Hacky workaround for http://b/6946182
123        Utils.fixSubTreeLayoutIfOrphaned(getView(), "FolderListFragment");
124    }
125    /**
126     * Creates a new instance of {@link ConversationListFragment}, initialized
127     * to display conversation list context.
128     * @param isSectioned TODO(viki):
129     */
130    public static FolderListFragment newInstance(Folder parentFolder, Uri folderUri,
131            boolean isSectioned) {
132        final FolderListFragment fragment = new FolderListFragment();
133        final Bundle args = new Bundle();
134        if (parentFolder != null) {
135            args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
136        }
137        args.putString(ARG_FOLDER_URI, folderUri.toString());
138        args.putBoolean(ARG_IS_SECTIONED, isSectioned);
139        fragment.setArguments(args);
140        return fragment;
141    }
142
143    @Override
144    public void onActivityCreated(Bundle savedState) {
145        super.onActivityCreated(savedState);
146        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
147        // only activity creating a ConversationListContext is a MailActivity which is of type
148        // ControllableActivity, so this cast should be safe. If this cast fails, some other
149        // activity is creating ConversationListFragments. This activity must be of type
150        // ControllableActivity.
151        final Activity activity = getActivity();
152        if (! (activity instanceof ControllableActivity)){
153            LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
154                    "create it. Cannot proceed.");
155        }
156        mActivity = (ControllableActivity) activity;
157        final FolderController controller = mActivity.getFolderController();
158        // Listen to folder changes in the future
159        mFolderObserver = new FolderObserver();
160        if (controller != null) {
161            // Only register for selected folder updates if we have a controller.
162            controller.registerFolderObserver(mFolderObserver);
163        }
164
165        mListener = mActivity.getFolderListSelectionListener();
166        if (mActivity.isFinishing()) {
167            // Activity is finishing, just bail.
168            return;
169        }
170
171        if (mParentFolder != null) {
172            mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
173        } else {
174            mCursorAdapter = new FolderListAdapter(R.layout.folder_item, mIsSectioned);
175        }
176        setListAdapter(mCursorAdapter);
177
178        selectInitialFolder(mActivity.getHierarchyFolder());
179        getLoaderManager().initLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this);
180    }
181
182    @Override
183    public View onCreateView(LayoutInflater inflater, ViewGroup container,
184            Bundle savedState) {
185        final Bundle args = getArguments();
186        mFolderListUri = Uri.parse(args.getString(ARG_FOLDER_URI));
187        mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER);
188        mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
189        final View rootView = inflater.inflate(R.layout.folder_list, null);
190        mListView = (ListView) rootView.findViewById(android.R.id.list);
191        mListView.setHeaderDividersEnabled(false);
192        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
193        mListView.setEmptyView(null);
194        if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
195            mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
196        }
197        mEmptyView = rootView.findViewById(R.id.empty_view);
198        if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
199            mSelectedFolderUri = Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER));
200        } else if (mParentFolder != null) {
201            mSelectedFolderUri = mParentFolder.uri;
202        }
203        Utils.dumpLayoutRequests("FLF(" + this + ").onCreateView()", rootView);
204
205        return rootView;
206    }
207
208    @Override
209    public void onStart() {
210        Utils.dumpLayoutRequests("FLF(" + this + ").onStart()", getView());
211        super.onStart();
212    }
213
214    @Override
215    public void onStop() {
216        Utils.dumpLayoutRequests("FLF(" + this + ").onStop()", getView());
217        super.onStop();
218    }
219
220    @Override
221    public void onPause() {
222        Utils.dumpLayoutRequests("FLF(" + this + ").onPause()", getView());
223        super.onPause();
224    }
225
226    @Override
227    public void onSaveInstanceState(Bundle outState) {
228        super.onSaveInstanceState(outState);
229        if (mListView != null) {
230            outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
231        }
232        if (mSelectedFolderUri != null) {
233            outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
234        }
235        outState.putBoolean(ARG_IS_SECTIONED, mIsSectioned);
236    }
237
238    @Override
239    public void onDestroyView() {
240        Utils.dumpLayoutRequests("FLF(" + this + ").onDestoryView()", getView());
241        if (mCursorAdapter != null) {
242            mCursorAdapter.destroy();
243        }
244        // Clear the adapter.
245        setListAdapter(null);
246        if (mFolderObserver != null) {
247            FolderController controller = mActivity.getFolderController();
248            if (controller != null) {
249                controller.unregisterFolderObserver(mFolderObserver);
250                mFolderObserver = null;
251            }
252        }
253        super.onDestroyView();
254    }
255
256    @Override
257    public void onListItemClick(ListView l, View v, int position, long id) {
258        viewFolder(position);
259    }
260
261    /**
262     * Display the conversation list from the folder at the position given.
263     * @param position
264     */
265    private void viewFolder(int position) {
266        Object item = getListAdapter().getItem(position);
267        final Folder folder;
268        if (item instanceof FolderListAdapter.Item) {
269            FolderListAdapter.Item folderItem = (FolderListAdapter.Item) item;
270            folder = folderItem.mFolder;
271            ((FolderListAdapter) getListAdapter()).setSelectedType(folderItem.mFolderType);
272        } else if (item instanceof Folder) {
273            folder = (Folder) item;
274        } else {
275            folder = new Folder((Cursor) item);
276        }
277        if (folder != null) {
278            // Since we may be looking at hierarchical views, if we can
279            // determine the parent of the folder we have tapped, set it here.
280            // If we are looking at the folder we are already viewing, don't
281            // update its parent!
282            folder.parent = folder.equals(mParentFolder) ? null : mParentFolder;
283            // Go to the conversation list for this folder.
284            mListener.onFolderSelected(folder);
285        }
286    }
287
288    @Override
289    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
290        mListView.setEmptyView(null);
291        mEmptyView.setVisibility(View.GONE);
292        return new CursorLoader(mActivity.getActivityContext(), mFolderListUri,
293                UIProvider.FOLDERS_PROJECTION, null, null, null);
294    }
295
296    @Override
297    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
298        mCursorAdapter.setCursor(data);
299        if (data == null || data.getCount() == 0) {
300            mEmptyView.setVisibility(View.VISIBLE);
301            mListView.setEmptyView(mEmptyView);
302        }
303    }
304
305    @Override
306    public void onLoaderReset(Loader<Cursor> loader) {
307        mCursorAdapter.setCursor(null);
308    }
309
310    /**
311     * Interface for all cursor adpaters that allow setting a cursor and being destroyed.
312     */
313    private interface FolderListFragmentCursorAdapter extends ListAdapter {
314        /** Update the folder list cursor with the cursor given here. */
315        void setCursor(Cursor cursor);
316        /** Remove all observers and destroy the object. */
317        void destroy();
318    }
319
320    /**
321     * An adapter for flat folder lists.
322     */
323    private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
324
325        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
326            @Override
327            public void onChanged() {
328                recalculateList();
329            }
330        };
331
332        private final RecentFolderList mRecentFolders;
333        /** True if the list is sectioned, false otherwise */
334        private final boolean mIsSectioned;
335        private final LayoutInflater mInflater;
336        /** All the items */
337        private final List<Item> mItemList = new ArrayList<Item>();
338        /** Cursor into the folder list. This might be null. */
339        private Cursor mCursor = null;
340        /**
341         * Type of currently selected folder: {@link Item#FOLDER_SYSTEM}, {@link Item#FOLDER_RECENT}
342         * or {@link Item#FOLDER_USER}
343         */
344        private int mSelectedFolderType;
345
346        /** A union of either a folder or a resource string */
347        private class Item {
348            public final Folder mFolder;
349            public final int mResource;
350            /** Either {@link #VIEW_FOLDER} or {@link #VIEW_HEADER} */
351            public final int mType;
352            /** A normal folder, also a child, if a parent is specified. */
353            private static final int VIEW_FOLDER = 0;
354            /** A text-label which serves as a header in sectioned lists. */
355            private static final int VIEW_HEADER = 1;
356
357            /**
358             * Either {@link #FOLDER_SYSTEM}, {@link #FOLDER_RECENT} or {@link #FOLDER_USER} when
359             * {@link #mType} is {@link #VIEW_FOLDER}, and {@link #NOT_A_FOLDER} otherwise.
360             */
361            public final int mFolderType;
362            private static final int NOT_A_FOLDER = 0;
363            private static final int FOLDER_SYSTEM = 1;
364            private static final int FOLDER_RECENT = 2;
365            private static final int FOLDER_USER = 3;
366
367            /**
368             * Create a folder item with the given type.
369             * @param folder
370             * @param folderType one of {@link #FOLDER_SYSTEM}, {@link #FOLDER_RECENT} or
371             * {@link #FOLDER_USER}
372             */
373            private Item(Folder folder, int folderType) {
374                mFolder = folder;
375                mResource = -1;
376                mType = VIEW_FOLDER;
377                mFolderType = folderType;
378            }
379            /**
380             * Create a header item with a string resource.
381             * @param resource the string resource: R.string.all_folders_heading
382             */
383            private Item(int resource) {
384                mFolder = null;
385                mResource = resource;
386                mType = VIEW_HEADER;
387                mFolderType = NOT_A_FOLDER;
388            }
389
390            private final View getView(int position, View convertView, ViewGroup parent) {
391                if (mType == VIEW_FOLDER) {
392                    return getFolderView(position, convertView, parent);
393                } else {
394                    return getHeaderView(position, convertView, parent);
395                }
396            }
397
398            /**
399             * Returns a text divider between sections.
400             * @param convertView
401             * @param parent
402             * @return a text header at the given position.
403             */
404            private final View getHeaderView(int position, View convertView, ViewGroup parent) {
405                final TextView headerView;
406                if (convertView != null) {
407                    headerView = (TextView) convertView;
408                } else {
409                    headerView = (TextView) mInflater.inflate(
410                            R.layout.folder_list_header, parent, false);
411                }
412                headerView.setText(mResource);
413                return headerView;
414            }
415
416            /**
417             * Return a folder: either a parent folder or a normal (child or flat)
418             * folder.
419             * @param position
420             * @param convertView
421             * @param parent
422             * @return a view showing a folder at the given position.
423             */
424            private final View getFolderView(int position, View convertView, ViewGroup parent) {
425                final FolderItemView folderItemView;
426                if (convertView != null) {
427                    folderItemView = (FolderItemView) convertView;
428                } else {
429                    folderItemView =
430                            (FolderItemView) mInflater.inflate(R.layout.folder_item, null, false);
431                }
432                folderItemView.bind(mFolder, mActivity, false);
433                if (mListView != null) {
434                    final boolean isSelected = (mFolderType == mSelectedFolderType)
435                            && mFolder.uri.equals(mSelectedFolderUri);
436                    mListView.setItemChecked(position, isSelected);
437                }
438                Folder.setFolderBlockColor(mFolder, folderItemView.findViewById(R.id.color_block));
439                Folder.setIcon(mFolder, (ImageView) folderItemView.findViewById(R.id.folder_box));
440                return folderItemView;
441            }
442        }
443
444        /**
445         * Creates a {@link FolderListAdapter}.This is a flat folder list of all the folders for the
446         * given account.
447         * @param layout
448         * @param isSectioned TODO(viki):
449         */
450        public FolderListAdapter(int layout, boolean isSectioned) {
451            super();
452            mInflater = LayoutInflater.from(mActivity.getActivityContext());
453            mIsSectioned = isSectioned;
454            final RecentFolderController controller = mActivity.getRecentFolderController();
455            if (controller != null && mIsSectioned) {
456                mRecentFolders = mRecentFolderObserver.initialize(controller);
457            } else {
458                mRecentFolders = null;
459            }
460        }
461        /**
462         * Sets the currently selected folder's type to the type given here.
463         */
464        public void setSelectedType(int type) {
465            mSelectedFolderType = type;
466        }
467
468        @Override
469        public View getView(int position, View convertView, ViewGroup parent) {
470            return ((Item) getItem(position)).getView(position, convertView, parent);
471        }
472
473        @Override
474        public int getViewTypeCount() {
475            // Headers and folders
476            return 2;
477        }
478
479        @Override
480        public int getItemViewType(int position) {
481            return ((Item) getItem(position)).mType;
482        }
483
484        @Override
485        public int getCount() {
486            return mItemList.size();
487        }
488
489        @Override
490        public boolean isEnabled(int position) {
491            // We disallow taps on headers
492            return ((Item) getItem(position)).mType != Item.VIEW_HEADER;
493        }
494
495        @Override
496        public boolean areAllItemsEnabled() {
497            // The headers are not enabled.
498            return false;
499        }
500
501        /**
502         * Returns all the recent folders from the list given here. Safe to call with a null list.
503         * @param recentList
504         * @return a valid list of folders, which are all recent folders.
505         */
506        private final List<Folder> getRecentFolders(RecentFolderList recentList) {
507            final List<Folder> folderList = new ArrayList<Folder>();
508            if (recentList == null) {
509                return folderList;
510            }
511            // Get all recent folders, after removing system folders.
512            for (final Folder f : recentList.getRecentFolderList(null)) {
513                if (!f.isProviderFolder()) {
514                    folderList.add(f);
515                }
516            }
517            return folderList;
518        }
519
520        /**
521         * Recalculates the system, recent and user label lists. Notifies that the data has changed.
522         * This method modifies all the three lists on every single invocation.
523         */
524        private void recalculateList() {
525            if (mCursor == null || mCursor.getCount() <= 0 || !mCursor.moveToFirst()) {
526                return;
527            }
528            mItemList.clear();
529            if (!mIsSectioned) {
530                // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers.
531                do {
532                    final Folder f = new Folder(mCursor);
533                    mItemList.add(new Item(f, Item.FOLDER_USER));
534                } while (mCursor.moveToNext());
535                // Ask the list to invalidate its views.
536                notifyDataSetChanged();
537                return;
538            }
539
540            // Otherwise, this is an adapter for a sectioned list.
541            // First add all the system folders.
542            final List<Folder> userFolderList = new ArrayList<Folder>();
543            do {
544                final Folder f = new Folder(mCursor);
545                if (f.isProviderFolder()) {
546                    mItemList.add(new Item(f, Item.FOLDER_SYSTEM));
547                } else {
548                    userFolderList.add(f);
549                }
550            } while (mCursor.moveToNext());
551            // If there are recent folders, add them and a header.
552            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
553            if (recentFolderList.size() > 0) {
554                mItemList.add(new Item(R.string.recent_folders_heading));
555                for (Folder f : recentFolderList) {
556                    mItemList.add(new Item(f, Item.FOLDER_RECENT));
557                }
558            }
559            // If there are user folders, add them and a header.
560            if (userFolderList.size() > 0) {
561                mItemList.add(new Item(R.string.all_folders_heading));
562                for (final Folder f : userFolderList) {
563                    mItemList.add(new Item(f, Item.FOLDER_USER));
564                }
565            }
566            // Ask the list to invalidate its views.
567            notifyDataSetChanged();
568        }
569
570        @Override
571        public void setCursor(Cursor cursor) {
572            mCursor = cursor;
573            recalculateList();
574        }
575
576        @Override
577        public Object getItem(int position) {
578            return mItemList.get(position);
579        }
580
581        @Override
582        public long getItemId(int position) {
583            return getItem(position).hashCode();
584        }
585
586        @Override
587        public final void destroy() {
588            mRecentFolderObserver.unregisterAndDestroy();
589        }
590    }
591
592    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
593            implements FolderListFragmentCursorAdapter{
594
595        private static final int PARENT = 0;
596        private static final int CHILD = 1;
597        private final Uri mParentUri;
598        private final Folder mParent;
599        private final FolderItemView.DropHandler mDropHandler;
600
601        public HierarchicalFolderListAdapter(Cursor c, Folder parentFolder) {
602            super(mActivity.getActivityContext(), R.layout.folder_item);
603            mDropHandler = mActivity;
604            mParent = parentFolder;
605            mParentUri = parentFolder.uri;
606            setCursor(c);
607        }
608
609        @Override
610        public int getViewTypeCount() {
611            // Child and Parent
612            return 2;
613        }
614
615        @Override
616        public int getItemViewType(int position) {
617            Folder f = getItem(position);
618            return f.uri.equals(mParentUri) ? PARENT : CHILD;
619        }
620
621        @Override
622        public View getView(int position, View convertView, ViewGroup parent) {
623            FolderItemView folderItemView;
624            Folder folder = getItem(position);
625            boolean isParent = folder.uri.equals(mParentUri);
626            if (convertView != null) {
627                folderItemView = (FolderItemView) convertView;
628            } else {
629                int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
630                folderItemView = (FolderItemView) LayoutInflater.from(
631                        mActivity.getActivityContext()).inflate(resId, null);
632            }
633            folderItemView.bind(folder, mDropHandler, isParent);
634            if (folder.uri.equals(mSelectedFolderUri)) {
635                getListView().setItemChecked(position, true);
636            }
637            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.folder_box));
638            return folderItemView;
639        }
640
641        @Override
642        public void setCursor(Cursor cursor) {
643            clear();
644            if (mParent != null) {
645                add(mParent);
646            }
647            if (cursor != null && cursor.getCount() > 0) {
648                cursor.moveToFirst();
649                do {
650                    Folder f = new Folder(cursor);
651                    f.parent = mParent;
652                    add(f);
653                } while (cursor.moveToNext());
654            }
655        }
656
657        @Override
658        public void destroy() {
659            // Do nothing.
660        }
661    }
662
663    public void selectInitialFolder(Folder folder) {
664        if (folder == null) {
665            mSelectedFolderUri = Uri.EMPTY;
666            return;
667        }
668        mSelectedFolderUri = folder.uri;
669    }
670
671    public interface FolderListSelectionListener {
672        public void onFolderSelected(Folder folder);
673    }
674
675    /**
676     * Get whether the FolderListFragment is currently showing the hierarchy
677     * under a single parent.
678     */
679    public boolean showingHierarchy() {
680        return mParentFolder != null;
681    }
682}
683