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