FolderListFragment.java revision 7e654a970f73e3ebfd6895caa5072c6dcc9e7fb5
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.ContentResolver;
24import android.content.ContentUris;
25import android.content.Context;
26import android.content.Loader;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.net.Uri;
30import android.os.AsyncTask;
31import android.os.Bundle;
32import android.provider.ContactsContract;
33import android.provider.ContactsContract.Data;
34import android.provider.ContactsContract.CommonDataKinds.Email;
35import android.provider.ContactsContract.Contacts.Photo;
36import android.text.TextUtils;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.ViewGroup;
40import android.widget.ArrayAdapter;
41import android.widget.BaseAdapter;
42import android.widget.ImageView;
43import android.widget.ListAdapter;
44import android.widget.ListView;
45
46import com.android.mail.R;
47import com.android.mail.adapter.DrawerItem;
48import com.android.mail.browse.LetterTileUtils;
49import com.android.mail.content.ObjectCursor;
50import com.android.mail.content.ObjectCursorLoader;
51import com.android.mail.photomanager.BitmapUtil;
52import com.android.mail.providers.Account;
53import com.android.mail.providers.AccountObserver;
54import com.android.mail.providers.AllAccountObserver;
55import com.android.mail.providers.Folder;
56import com.android.mail.providers.FolderObserver;
57import com.android.mail.providers.FolderWatcher;
58import com.android.mail.providers.RecentFolderObserver;
59import com.android.mail.providers.UIProvider;
60import com.android.mail.providers.UIProvider.FolderType;
61import com.android.mail.utils.LogTag;
62import com.android.mail.utils.LogUtils;
63import com.google.common.collect.ImmutableSet;
64
65import java.util.ArrayList;
66import java.util.Arrays;
67import java.util.HashMap;
68import java.util.HashSet;
69import java.util.Iterator;
70import java.util.List;
71import java.util.Set;
72
73/**
74 * The folder list UI component.
75 */
76public class FolderListFragment extends ListFragment implements
77        LoaderManager.LoaderCallbacks<ObjectCursor<Folder>> {
78    private static final String LOG_TAG = LogTag.getLogTag();
79    /** The parent activity */
80    private ControllableActivity mActivity;
81    /** The underlying list view */
82    private ListView mListView;
83    /** URI that points to the list of folders for the current account. */
84    private Uri mFolderListUri;
85    /** True if you want a sectioned FolderList, false otherwise. */
86    protected boolean mIsSectioned;
87    /** An {@link ArrayList} of {@link FolderType}s to exclude from displaying. */
88    private ArrayList<Integer> mExcludedFolderTypes;
89    /** Object that changes folders on our behalf. */
90    private FolderListSelectionListener mFolderChanger;
91    /** Object that changes accounts on our behalf */
92    private AccountController mAccountChanger;
93
94    /** The currently selected folder (the folder being viewed).  This is never null. */
95    private Uri mSelectedFolderUri = Uri.EMPTY;
96    /**
97     * The current folder from the controller.  This is meant only to check when the unread count
98     * goes out of sync and fixing it.
99     */
100    private Folder mCurrentFolderForUnreadCheck;
101    /** Parent of the current folder, or null if the current folder is not a child. */
102    private Folder mParentFolder;
103
104    private static final int FOLDER_LOADER_ID = 0;
105    /** Key to store {@link #mParentFolder}. */
106    private static final String ARG_PARENT_FOLDER = "arg-parent-folder";
107    /** Key to store {@link #mIsSectioned} */
108    private static final String ARG_IS_SECTIONED = "arg-is-sectioned";
109    /** Key to store {@link #mFolderListUri}. */
110    private static final String ARG_FOLDER_LIST_URI = "arg-folder-list-uri";
111    /** Key to store {@link #mExcludedFolderTypes} */
112    private static final String ARG_EXCLUDED_FOLDER_TYPES = "arg-excluded-folder-types";
113    /** Key to store {@link #mType} */
114    private static final String ARG_TYPE = "arg-flf-type";
115
116    /** Either {@link #TYPE_DRAWER} for drawers or {@link #TYPE_TREE} for hierarchy trees */
117    private int mType;
118    /** This fragment is a drawer */
119    private static final int TYPE_DRAWER = 0;
120    /** This fragment is a folder tree */
121    private static final int TYPE_TREE = 1;
122
123    private static final String BUNDLE_LIST_STATE = "flf-list-state";
124    private static final String BUNDLE_SELECTED_FOLDER = "flf-selected-folder";
125    private static final String BUNDLE_SELECTED_TYPE = "flf-selected-type";
126
127    private FolderListFragmentCursorAdapter mCursorAdapter;
128    /** Observer to wait for changes to the current folder so we can change the selected folder */
129    private FolderObserver mFolderObserver = null;
130    /** Listen for account changes. */
131    private AccountObserver mAccountObserver = null;
132
133    /** Listen to changes to list of all accounts */
134    private AllAccountObserver mAllAccountsObserver = null;
135    /**
136     * Type of currently selected folder: {@link DrawerItem#FOLDER_SYSTEM},
137     * {@link DrawerItem#FOLDER_RECENT} or {@link DrawerItem#FOLDER_USER}.
138     */
139    // Setting to INERT_HEADER = leaving uninitialized.
140    private int mSelectedFolderType = DrawerItem.UNSET;
141    /** The current account according to the controller */
142    private Account mCurrentAccount;
143
144    /**
145     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
146     */
147    public FolderListFragment() {
148        super();
149    }
150
151    @Override
152    public String toString() {
153        final StringBuilder sb = new StringBuilder(super.toString());
154        sb.setLength(sb.length() - 1);
155        sb.append(" folder=");
156        sb.append(mFolderListUri);
157        sb.append(" parent=");
158        sb.append(mParentFolder);
159        sb.append(" adapterCount=");
160        sb.append(mCursorAdapter != null ? mCursorAdapter.getCount() : -1);
161        sb.append("}");
162        return sb.toString();
163    }
164
165    /**
166     * Creates a new instance of {@link FolderListFragment}. Gets the current account and current
167     * folder through observers.
168     */
169    public static FolderListFragment ofDrawer() {
170        final FolderListFragment fragment = new FolderListFragment();
171        // The drawer is always sectioned
172        final boolean isSectioned = true;
173        fragment.setArguments(getBundleFromArgs(TYPE_DRAWER, null, null, isSectioned, null));
174        return fragment;
175    }
176
177    /**
178     * Creates a new instance of {@link FolderListFragment}, initialized
179     * to display the folder and its immediate children.
180     * @param folder parent folder whose children are shown
181     *
182     */
183    public static FolderListFragment ofTree(Folder folder) {
184        final FolderListFragment fragment = new FolderListFragment();
185        // Trees are never sectioned.
186        final boolean isSectioned = false;
187        fragment.setArguments(getBundleFromArgs(TYPE_TREE, folder, folder.childFoldersListUri,
188                isSectioned, null));
189        return fragment;
190    }
191
192    /**
193     * Creates a new instance of {@link FolderListFragment}, initialized
194     * to display the folder and its immediate children.
195     * @param folderListUri the URI which contains all the list of folders
196     * @param excludedFolderTypes A list of {@link FolderType}s to exclude from displaying
197     */
198    public static FolderListFragment ofTopLevelTree(Uri folderListUri,
199            final ArrayList<Integer> excludedFolderTypes) {
200        final FolderListFragment fragment = new FolderListFragment();
201        // Trees are never sectioned.
202        final boolean isSectioned = false;
203        fragment.setArguments(getBundleFromArgs(TYPE_TREE, null, folderListUri,
204                isSectioned, excludedFolderTypes));
205        return fragment;
206    }
207
208    /**
209     * Construct a bundle that represents the state of this fragment.
210     * @param type the type of FLF: {@link #TYPE_DRAWER} or {@link #TYPE_TREE}
211     * @param parentFolder non-null for trees, the parent of this list
212     * @param isSectioned true if this drawer is sectioned, false otherwise
213     * @param folderListUri the URI which contains all the list of folders
214     * @param excludedFolderTypes if non-null, this indicates folders to exclude in lists.
215     * @return Bundle containing parentFolder, sectioned list boolean and
216     *         excluded folder types
217     */
218    private static Bundle getBundleFromArgs(int type, Folder parentFolder, Uri folderListUri,
219            boolean isSectioned, final ArrayList<Integer> excludedFolderTypes) {
220        final Bundle args = new Bundle();
221        args.putInt(ARG_TYPE, type);
222        if (parentFolder != null) {
223            args.putParcelable(ARG_PARENT_FOLDER, parentFolder);
224        }
225        if (folderListUri != null) {
226            args.putString(ARG_FOLDER_LIST_URI, folderListUri.toString());
227        }
228        args.putBoolean(ARG_IS_SECTIONED, isSectioned);
229        if (excludedFolderTypes != null) {
230            args.putIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES, excludedFolderTypes);
231        }
232        return args;
233    }
234
235    @Override
236    public void onActivityCreated(Bundle savedState) {
237        super.onActivityCreated(savedState);
238        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
239        // only activity creating a ConversationListContext is a MailActivity which is of type
240        // ControllableActivity, so this cast should be safe. If this cast fails, some other
241        // activity is creating ConversationListFragments. This activity must be of type
242        // ControllableActivity.
243        final Activity activity = getActivity();
244        Folder currentFolder = null;
245        if (! (activity instanceof ControllableActivity)){
246            LogUtils.wtf(LOG_TAG, "FolderListFragment expects only a ControllableActivity to" +
247                    "create it. Cannot proceed.");
248        }
249        mActivity = (ControllableActivity) activity;
250        final FolderController controller = mActivity.getFolderController();
251        // Listen to folder changes in the future
252        mFolderObserver = new FolderObserver() {
253            @Override
254            public void onChanged(Folder newFolder) {
255                setSelectedFolder(newFolder);
256            }
257        };
258        if (controller != null) {
259            // Only register for selected folder updates if we have a controller.
260            currentFolder = mFolderObserver.initialize(controller);
261            mCurrentFolderForUnreadCheck = currentFolder;
262        }
263
264        // Initialize adapter for folder/heirarchical list
265        final Folder selectedFolder;
266        if (mParentFolder != null) {
267            mCursorAdapter = new HierarchicalFolderListAdapter(null, mParentFolder);
268            selectedFolder = mActivity.getHierarchyFolder();
269        } else {
270            mCursorAdapter = new FolderListAdapter(mIsSectioned);
271            selectedFolder = currentFolder;
272        }
273        // Is the selected folder fresher than the one we have restored from a bundle?
274        if (selectedFolder != null && !selectedFolder.uri.equals(mSelectedFolderUri)) {
275            setSelectedFolder(selectedFolder);
276        }
277
278        // Assign observers for current account & all accounts
279        final AccountController accountController = mActivity.getAccountController();
280        mAccountObserver = new AccountObserver() {
281            @Override
282            public void onChanged(Account newAccount) {
283                setSelectedAccount(newAccount);
284            }
285        };
286        if (accountController != null) {
287            // Current account and its observer.
288            setSelectedAccount(mAccountObserver.initialize(accountController));
289            // List of all accounts and its observer.
290            mAllAccountsObserver = new AllAccountObserver(){
291                @Override
292                public void onChanged(Account[] allAccounts) {
293                    mCursorAdapter.notifyAllAccountsChanged();
294                }
295            };
296            mAllAccountsObserver.initialize(accountController);
297            mAccountChanger = accountController;
298        }
299
300        mFolderChanger = mActivity.getFolderListSelectionListener();
301        if (mActivity.isFinishing()) {
302            // Activity is finishing, just bail.
303            return;
304        }
305
306        setListAdapter(mCursorAdapter);
307    }
308
309    /**
310     * Set the instance variables from the arguments provided here.
311     * @param args
312     */
313    private void setInstanceFromBundle(Bundle args) {
314        if (args == null) {
315            return;
316        }
317        mParentFolder = (Folder) args.getParcelable(ARG_PARENT_FOLDER);
318        final String folderUri = args.getString(ARG_FOLDER_LIST_URI);
319        if (folderUri == null) {
320            mFolderListUri = Uri.EMPTY;
321        } else {
322            mFolderListUri = Uri.parse(folderUri);
323        }
324        mIsSectioned = args.getBoolean(ARG_IS_SECTIONED);
325        mExcludedFolderTypes = args.getIntegerArrayList(ARG_EXCLUDED_FOLDER_TYPES);
326        mType = args.getInt(ARG_TYPE);
327    }
328
329    @Override
330    public View onCreateView(LayoutInflater inflater, ViewGroup container,
331            Bundle savedState) {
332        setInstanceFromBundle(getArguments());
333        final View rootView = inflater.inflate(R.layout.folder_list, null);
334        mListView = (ListView) rootView.findViewById(android.R.id.list);
335        mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
336        mListView.setEmptyView(null);
337        if (savedState != null && savedState.containsKey(BUNDLE_LIST_STATE)) {
338            mListView.onRestoreInstanceState(savedState.getParcelable(BUNDLE_LIST_STATE));
339        }
340        if (savedState != null && savedState.containsKey(BUNDLE_SELECTED_FOLDER)) {
341            mSelectedFolderUri = Uri.parse(savedState.getString(BUNDLE_SELECTED_FOLDER));
342            mSelectedFolderType = savedState.getInt(BUNDLE_SELECTED_TYPE);
343        } else if (mParentFolder != null) {
344            mSelectedFolderUri = mParentFolder.uri;
345            // No selected folder type required for hierarchical lists.
346        }
347
348        return rootView;
349    }
350
351    @Override
352    public void onStart() {
353        super.onStart();
354    }
355
356    @Override
357    public void onStop() {
358        super.onStop();
359    }
360
361    @Override
362    public void onPause() {
363        super.onPause();
364    }
365
366    @Override
367    public void onSaveInstanceState(Bundle outState) {
368        super.onSaveInstanceState(outState);
369        if (mListView != null) {
370            outState.putParcelable(BUNDLE_LIST_STATE, mListView.onSaveInstanceState());
371        }
372        if (mSelectedFolderUri != null) {
373            outState.putString(BUNDLE_SELECTED_FOLDER, mSelectedFolderUri.toString());
374        }
375        outState.putInt(BUNDLE_SELECTED_TYPE, mSelectedFolderType);
376    }
377
378    @Override
379    public void onDestroyView() {
380        if (mCursorAdapter != null) {
381            mCursorAdapter.destroy();
382        }
383        // Clear the adapter.
384        setListAdapter(null);
385        if (mFolderObserver != null) {
386            mFolderObserver.unregisterAndDestroy();
387            mFolderObserver = null;
388        }
389        if (mAccountObserver != null) {
390            mAccountObserver.unregisterAndDestroy();
391            mAccountObserver = null;
392        }
393        if (mAllAccountsObserver != null) {
394            mAllAccountsObserver.unregisterAndDestroy();
395            mAllAccountsObserver = null;
396        }
397        super.onDestroyView();
398    }
399
400    @Override
401    public void onListItemClick(ListView l, View v, int position, long id) {
402        viewFolderOrChangeAccount(position);
403    }
404
405    /**
406     * Display the conversation list from the folder at the position given.
407     * @param position a zero indexed position into the list.
408     */
409    private void viewFolderOrChangeAccount(int position) {
410        final Object item = getListAdapter().getItem(position);
411        LogUtils.i(LOG_TAG, "viewFolderOrChangeAccount(%d): %s", position, item);
412        final Folder folder;
413        if (item instanceof DrawerItem) {
414            final DrawerItem drawerItem = (DrawerItem) item;
415            // Could be a folder or account.
416            final int itemType = mCursorAdapter.getItemType(drawerItem);
417            if (itemType == DrawerItem.VIEW_ACCOUNT) {
418                // Account, so switch.
419                folder = null;
420                final Account account = drawerItem.mAccount;
421                mAccountChanger.changeAccount(account);
422            } else if (itemType == DrawerItem.VIEW_FOLDER) {
423                // Folder type, so change folders only.
424                folder = drawerItem.mFolder;
425                mSelectedFolderType = drawerItem.mFolderType;
426                LogUtils.i(LOG_TAG, "FLF.viewFolderOrChangeAccount folder=%s, type=%d",
427                        folder, mSelectedFolderType);
428            } else {
429                // Do nothing.
430                LogUtils.i(LOG_TAG, "FolderListFragment: viewFolderOrChangeAccount():"
431                        + " Clicked on unset item in drawer. Offending item is " + item);
432                return;
433            }
434        } else if (item instanceof Folder) {
435            folder = (Folder) item;
436        } else if (item instanceof ObjectCursor){
437            folder = ((ObjectCursor<Folder>) item).getModel();
438        } else {
439            // Don't know how we got here.
440            LogUtils.wtf(LOG_TAG, "viewFolderOrChangeAccount(): invalid item");
441            folder = null;
442        }
443        if (folder != null) {
444            // Since we may be looking at hierarchical views, if we can
445            // determine the parent of the folder we have tapped, set it here.
446            // If we are looking at the folder we are already viewing, don't
447            // update its parent!
448            folder.parent = folder.equals(mParentFolder) ? null : mParentFolder;
449            // Go to the conversation list for this folder.
450            mFolderChanger.onFolderSelected(folder);
451        }
452    }
453
454    @Override
455    public Loader<ObjectCursor<Folder>> onCreateLoader(int id, Bundle args) {
456        mListView.setEmptyView(null);
457        final Uri folderListUri;
458        if (mType == TYPE_TREE) {
459            // Folder trees, they specify a URI at construction time.
460            folderListUri = mFolderListUri;
461        } else if (mType == TYPE_DRAWER) {
462            // Drawers should have a valid account
463            if (mCurrentAccount != null) {
464                folderListUri = mCurrentAccount.folderListUri;
465            } else {
466                LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() for Drawer with null account");
467                return null;
468            }
469        } else {
470            LogUtils.wtf(LOG_TAG, "FLF.onCreateLoader() with weird type");
471            return null;
472        }
473        return new ObjectCursorLoader<Folder>(mActivity.getActivityContext(), folderListUri,
474                UIProvider.FOLDERS_PROJECTION, Folder.FACTORY);
475    }
476
477    @Override
478    public void onLoadFinished(Loader<ObjectCursor<Folder>> loader, ObjectCursor<Folder> data) {
479        if (mCursorAdapter != null) {
480            mCursorAdapter.setCursor(data);
481        }
482    }
483
484    @Override
485    public void onLoaderReset(Loader<ObjectCursor<Folder>> loader) {
486        if (mCursorAdapter != null) {
487            mCursorAdapter.setCursor(null);
488        }
489    }
490
491    /**
492     *  Returns the sorted list of accounts. The AAC always has the current list, sorted by
493     *  frequency of use.
494     * @return a list of accounts, sorted by frequency of use
495     */
496    private Account[] getAllAccounts() {
497        if (mAllAccountsObserver != null) {
498            return mAllAccountsObserver.getAllAccounts();
499        }
500        return new Account[0];
501    }
502
503    /**
504     * Interface for all cursor adapters that allow setting a cursor and being destroyed.
505     */
506    private interface FolderListFragmentCursorAdapter extends ListAdapter {
507        /** Update the folder list cursor with the cursor given here. */
508        void setCursor(ObjectCursor<Folder> cursor);
509        /**
510         * Given an item, find the type of the item, which should only be {@link
511         * DrawerItem#VIEW_FOLDER} or {@link DrawerItem#VIEW_ACCOUNT}
512         * @return item the type of the item.
513         */
514        int getItemType(DrawerItem item);
515        /** Get the folder associated with this item. **/
516        Folder getFullFolder(DrawerItem item);
517        /** Notify that the all accounts changed. */
518        void notifyAllAccountsChanged();
519        /** Remove all observers and destroy the object. */
520        void destroy();
521        /** Notifies the adapter that the data has changed. */
522        void notifyDataSetChanged();
523    }
524
525    /**
526     * An adapter for flat folder lists.
527     */
528    private class FolderListAdapter extends BaseAdapter implements FolderListFragmentCursorAdapter {
529
530        private final RecentFolderObserver mRecentFolderObserver = new RecentFolderObserver() {
531            @Override
532            public void onChanged() {
533                recalculateList();
534            }
535        };
536        /** Database columns for email address -> photo_id query */
537        private final String[] DATA_COLS = new String[] { Email.DATA, Email.PHOTO_ID };
538        /** Database columns for photo_id -> photo query */
539        private final String[] PHOTO_COLS = new String[] { Photo._ID, Photo.PHOTO };
540        /** No resource used for string header in folder list */
541        private static final int NO_HEADER_RESOURCE = -1;
542        /** Cache of most recently used folders */
543        private final RecentFolderList mRecentFolders;
544        /** True if the list is sectioned, false otherwise */
545        private final boolean mIsSectioned;
546        /** All the items */
547        private List<DrawerItem> mItemList = new ArrayList<DrawerItem>();
548        /** Cursor into the folder list. This might be null. */
549        private ObjectCursor<Folder> mCursor = null;
550        /** Watcher for tracking and receiving unread counts for mail */
551        private FolderWatcher mFolderWatcher = null;
552        /**
553         * DO NOT USE off the UI thread. Will cause ConcurrentModificationExceptions otherwise
554         *
555         * Email address -> Bitmap
556         * Caveat: at some point we will want this to be from URI to Bitmap.
557         */
558        private final HashMap<String, Bitmap> mEmailToPhotoMap = new HashMap<String, Bitmap>();
559
560        /**
561         * Creates a {@link FolderListAdapter}.This is a list of all the accounts and folders.
562         *
563         * @param isSectioned true if folder list is flat, false if sectioned by label group
564         */
565        public FolderListAdapter(boolean isSectioned) {
566            super();
567            mIsSectioned = isSectioned;
568            final RecentFolderController controller = mActivity.getRecentFolderController();
569            if (controller != null && mIsSectioned) {
570                mRecentFolders = mRecentFolderObserver.initialize(controller);
571            } else {
572                mRecentFolders = null;
573            }
574            mFolderWatcher = new FolderWatcher(mActivity, this);
575            mFolderWatcher.updateAccountList(getAllAccounts());
576        }
577
578        @Override
579        public void notifyAllAccountsChanged() {
580            mFolderWatcher.updateAccountList(getAllAccounts());
581            retrieveContactPhotos();
582            recalculateList();
583        }
584
585        /**
586         * AsyncTask for loading all photos that populates the email address -> Bitmap Hash Map.
587         * Does the querying and loading of photos in the background along with creating
588         * default images in case contact photos aren't found.
589         *
590         * The task is of type <String, Void, HashMap<String, Bitmap>> which corresponds to
591         * the input being an array of String and the result being a HashMap that will get merged to
592         * {@link FolderListAdapter#mEmailToPhotoMap}.
593         */
594        private class LoadPhotosTask extends AsyncTask<Account, Void, HashMap<String, Bitmap>> {
595            private final ContentResolver mResolver;
596            private final Context mContext;
597            private final int mImageSize;
598
599            /**
600             * Construct the async task for downloading the photos.
601             */
602            public LoadPhotosTask(final Context context, final int imageSize) {
603                mResolver = context.getContentResolver();
604                mContext = context;
605                mImageSize = imageSize;
606            }
607
608            /**
609             * Runs account photo retrieval in the background. Note, mEmailToPhotoMap should NOT be
610             * modified here since this is run on a background thread and not the UI thread.
611             *
612             * The {@link Account#accountFromAddresses} is used for letter tiles and is required
613             * in order to properly assign the tile to the respective account.
614             */
615            @Override
616            protected HashMap<String, Bitmap> doInBackground(final Account... allAccounts) {
617                final HashMap<String, String> addressToDisplayNameMap = new HashMap<
618                        String, String>();
619                for (final Account account : allAccounts) {
620                    addressToDisplayNameMap.put(account.name, account.accountFromAddresses);
621                }
622
623                return getAccountPhoto(addressToDisplayNameMap);
624            }
625
626            @Override
627            protected void onPostExecute(final HashMap<String, Bitmap> accountPhotos) {
628                mEmailToPhotoMap.putAll(accountPhotos);
629            }
630
631            /**
632             * Queries the database for the photos. First finds the corresponding photo_id and then
633             * proceeds to find the photo through subsequent queries for {photo_id, bytes}. If the
634             * photo is not found for the address at the end, creates a letter tile using the
635             * display name/email address and then adds that to the finished HashMap
636             *
637             * @param addresses array of email addresses (strings)
638             * @param addressToDisplayNameMap map of email addresses to display names used for
639             *              letter tiles
640             * @return map of email addresses to the corresponding photos
641             */
642            private HashMap<String, Bitmap> getAccountPhoto(
643                    final HashMap<String, String> addressToDisplayNameMap) {
644                // Columns for email address, photo_id
645                final int DATA_EMAIL_COLUMN = 0;
646                final int DATA_PHOTO_COLUMN = 1;
647                final HashMap<String, Bitmap> photoMap = new HashMap<String, Bitmap>();
648                final Set<String> addressSet = addressToDisplayNameMap.keySet();
649
650                String address;
651                long photoId;
652                Cursor photoIdsCursor = null;
653
654
655                try {
656                    // Build query for address -> photo_id
657                    final StringBuilder query = new StringBuilder().append(Data.MIMETYPE)
658                            .append("='").append(Email.CONTENT_ITEM_TYPE).append("' AND ")
659                            .append(Email.DATA).append(" IN (");
660                    appendQuestionMarks(query, addressSet.size());
661                    query.append(')');
662                    photoIdsCursor = mResolver
663                            .query(Data.CONTENT_URI, DATA_COLS, query.toString(),
664                                    addressSet.toArray(new String[addressSet.size()]), null);
665
666                    // Iterate through cursor and attempt to find a matching photo_id
667                    if (photoIdsCursor != null) {
668                        while (photoIdsCursor.moveToNext()) {
669                            // If photo_id is found, query for the encoded bitmap
670                            if (!photoIdsCursor.isNull(DATA_PHOTO_COLUMN)) {
671                                address = photoIdsCursor.getString(DATA_EMAIL_COLUMN);
672                                photoId = photoIdsCursor.getLong(DATA_PHOTO_COLUMN);
673                                final byte[] bitmapBytes = getPhotoForId(photoId);
674                                if (bitmapBytes != null && photoMap.get(address) == null) {
675                                    final Bitmap contactPhoto = BitmapUtil.decodeBitmapFromBytes(
676                                            bitmapBytes, mImageSize, mImageSize);
677                                    photoMap.put(address, contactPhoto);
678                                }
679                            }
680                        }
681                    }
682                } finally {
683                    if(photoIdsCursor != null) {
684                        photoIdsCursor.close();
685                    }
686                }
687
688                // Finally, make sure that for any addresses in the original list for which
689                // we are unable to find contact photos, we're adding the LetterTiles
690                for(final String emailAddress : addressSet) {
691                    if(!photoMap.containsKey(emailAddress)) {
692                        final Bitmap letterTile = LetterTileUtils.generateLetterTile(
693                                addressToDisplayNameMap.get(emailAddress), emailAddress, mContext,
694                                mImageSize, mImageSize);
695                        photoMap.put(emailAddress, letterTile);
696                    }
697                }
698
699                return photoMap;
700            }
701
702            /**
703             * Find the photo by running a query on the photoId provided.
704             *
705             * @param resolver ContentResolver to query on
706             * @param photoId id corresponding to the photo (if found)
707             * @return array containing photo bytes
708             */
709            private byte[] getPhotoForId(final long photoId) {
710                // Column for the photo blob
711                final int DATA_PHOTO_COLUMN = 1;
712
713                byte[] bitmapBytes = null;
714                // First try getting photos from Contacts
715                Cursor contactCursor = null;
716                try {
717                    final String[] selectionArgs = { String.valueOf(photoId) };
718                    contactCursor = mResolver.query(Data.CONTENT_URI, PHOTO_COLS,
719                            Photo._ID + " = ?", selectionArgs, null);
720                    while (contactCursor.moveToNext()) {
721                        if (!contactCursor.isNull(1)) {
722                            bitmapBytes = contactCursor.getBlob(1);
723                            break;
724                        }
725                    }
726                } finally {
727                    if (contactCursor != null) {
728                        contactCursor.close();
729                    }
730                }
731
732                // Photo not found in contacts, try profiles instead
733                if(bitmapBytes == null) {
734                    if (ContactsContract.isProfileId(photoId)) {
735                        Cursor profileCursor = null;
736                        try {
737                            profileCursor = mResolver.query(
738                                    ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
739                                    PHOTO_COLS, null, null, null);
740                            if (profileCursor != null && profileCursor.moveToFirst()) {
741                                bitmapBytes = profileCursor.getBlob(DATA_PHOTO_COLUMN);
742                            }
743                        } finally {
744                            if (profileCursor != null) {
745                                profileCursor.close();
746                            }
747                        }
748                    }
749                }
750                return bitmapBytes;
751            }
752
753            /**
754             * Prepare the Selection clause for the given query by appending question marks
755             * followed by commas (Comma-delimited list of question marks as listed by
756             * the itemCount.
757             *
758             * @param query {@link StringBuilder} representing the query thus far
759             * @param itemCount number of selection arguments to add
760             */
761            private void appendQuestionMarks(final StringBuilder query, final int itemCount) {
762                final String[] questionMarks = new String[itemCount];
763                Arrays.fill(questionMarks, "?");
764                final String selection = TextUtils.join(", ", questionMarks);
765                query.append(selection);
766            }
767        }
768
769        /**
770         * Retrieve photos for accounts that do not yet have a mapping in
771         * {@link FolderListAdapter#mEmailToPhotoMap} by querying over the database. Every account
772         * is guaranteed to have either the account contact photo, letter tile, or a default gray
773         * picture for non-English account names.
774         */
775        public synchronized void retrieveContactPhotos() {
776            final Account[] allAccounts = getAllAccounts();
777            if (allAccounts == null) {
778                return;
779            }
780            /** Fresh accounts that were recently added to the system. */
781            final HashSet<Account> freshAccounts = new HashSet<Account>();
782            /** All current account email addresses. */
783            final HashSet<String> currentEmailList = new HashSet<String>();
784            final Context context = mActivity.getActivityContext();
785            final int imageSize = context.getResources().getDimensionPixelSize(
786                    R.dimen.folder_list_item_minimum_height);
787
788            for (final Account account : allAccounts) {
789                final String email = account.name;
790                if (!mEmailToPhotoMap.containsKey(email)) {
791                    freshAccounts.add(account);
792                    // For multiple tasks running very closely together, make sure we don't end up
793                    // loading pictures for an address more than once
794                    mEmailToPhotoMap.put(email, null);
795                }
796                currentEmailList.add(email);
797            }
798            // Find all the stale accounts in our map, and remove them.
799            final Set<String> emails = ImmutableSet.copyOf(mEmailToPhotoMap.keySet());
800            for (final String email : emails) {
801                if (!currentEmailList.contains(email)) {
802                    mEmailToPhotoMap.remove(email);
803                }
804            }
805            // Fetch contact photos or letter tiles for each fresh account.
806            if (!freshAccounts.isEmpty()) {
807                new LoadPhotosTask(context, imageSize).execute(
808                        freshAccounts.toArray(new Account[freshAccounts.size()]));
809            }
810        }
811
812        @Override
813        public View getView(int position, View convertView, ViewGroup parent) {
814            final DrawerItem item = (DrawerItem) getItem(position);
815            final View view = item.getView(position, convertView, parent);
816            final int type = item.mType;
817            if (mListView != null) {
818                final boolean isSelected =
819                        item.isHighlighted(mCurrentFolderForUnreadCheck, mSelectedFolderType);
820                if (type == DrawerItem.VIEW_FOLDER) {
821                    mListView.setItemChecked(position, isSelected);
822                }
823                // If this is the current folder, also check to verify that the unread count
824                // matches what the action bar shows.
825                if (type == DrawerItem.VIEW_FOLDER
826                        && isSelected
827                        && (mCurrentFolderForUnreadCheck != null)
828                        && item.mFolder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount) {
829                    ((FolderItemView) view).overrideUnreadCount(
830                            mCurrentFolderForUnreadCheck.unreadCount);
831                }
832            }
833            LogUtils.i(LOG_TAG, "FLF.getView(%d) returns view of item %s", position, item);
834            return view;
835        }
836
837        @Override
838        public int getViewTypeCount() {
839            // Accounts, headers, folders (all parts of drawer view types)
840            return DrawerItem.getViewTypes();
841        }
842
843        @Override
844        public int getItemViewType(int position) {
845            return ((DrawerItem) getItem(position)).mType;
846        }
847
848        @Override
849        public int getCount() {
850            return mItemList.size();
851        }
852
853        @Override
854        public boolean isEnabled(int position) {
855            return ((DrawerItem) getItem(position)).isItemEnabled();
856        }
857
858        private Uri getCurrentAccountUri() {
859            return mCurrentAccount == null ? Uri.EMPTY : mCurrentAccount.uri;
860        }
861
862        @Override
863        public boolean areAllItemsEnabled() {
864            // We have headers and thus some items are not enabled.
865            return false;
866        }
867
868        /**
869         * Returns all the recent folders from the list given here. Safe to call with a null list.
870         * @param recentList a list of all recently accessed folders.
871         * @return a valid list of folders, which are all recent folders.
872         */
873        private List<Folder> getRecentFolders(RecentFolderList recentList) {
874            final List<Folder> folderList = new ArrayList<Folder>();
875            if (recentList == null) {
876                return folderList;
877            }
878            // Get all recent folders, after removing system folders.
879            for (final Folder f : recentList.getRecentFolderList(null)) {
880                if (!f.isProviderFolder()) {
881                    folderList.add(f);
882                }
883            }
884            return folderList;
885        }
886
887        /**
888         * Responsible for verifying mCursor, and ensuring any recalculate
889         * conditions are met. Also calls notifyDataSetChanged once it's finished
890         * populating {@link FolderListAdapter#mItemList}
891         */
892        private void recalculateList() {
893            final List<DrawerItem> newFolderList = new ArrayList<DrawerItem>();
894            recalculateListAccounts(newFolderList);
895            recalculateListFolders(newFolderList);
896            mItemList = newFolderList;
897            // Ask the list to invalidate its views.
898            notifyDataSetChanged();
899        }
900
901        /**
902         * Recalculates the accounts if not null and adds them to the list.
903         *
904         * @param itemList List of drawer items to populate
905         */
906        private void recalculateListAccounts(List<DrawerItem> itemList) {
907            final Account[] allAccounts = getAllAccounts();
908            // Add all accounts and then the current account
909            final Uri currentAccountUri = getCurrentAccountUri();
910            for (final Account account : allAccounts) {
911                if (!currentAccountUri.equals(account.uri)) {
912                    final int unreadCount = mFolderWatcher.getUnreadCount(account);
913                    itemList.add(DrawerItem.ofAccount(mActivity, account, unreadCount, false,
914                            mEmailToPhotoMap.get(account.name)));
915                }
916            }
917            if (mCurrentAccount == null) {
918                LogUtils.wtf(LOG_TAG, "recalculateListAccounts() with null current account.");
919            } else {
920                // We don't show the unread count for the current account, so set this to zero.
921                itemList.add(DrawerItem.ofAccount(mActivity, mCurrentAccount, 0, true,
922                        mEmailToPhotoMap.get(mCurrentAccount.name)));
923            }
924        }
925
926        /**
927         * Recalculates the system, recent and user label lists.
928         * This method modifies all the three lists on every single invocation.
929         *
930         * @param itemList List of drawer items to populate
931         */
932        private void recalculateListFolders(List<DrawerItem> itemList) {
933            // If we are waiting for folder initialization, we don't have any kinds of folders,
934            // just the "Waiting for initialization" item. Note, this should only be done
935            // when we're waiting for account initialization or initial sync.
936            if (isCursorInvalid(mCursor)) {
937                if(!mCurrentAccount.isAccountReady()) {
938                    itemList.add(DrawerItem.forWaitView(mActivity));
939                }
940                return;
941            }
942
943            if (!mIsSectioned) {
944                // Adapter for a flat list. Everything is a FOLDER_USER, and there are no headers.
945                do {
946                    final Folder f = mCursor.getModel();
947                    if (!isFolderTypeExcluded(f)) {
948                        itemList.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_USER,
949                                mCursor.getPosition()));
950                    }
951                } while (mCursor.moveToNext());
952                return;
953            }
954
955            // Otherwise, this is an adapter for a sectioned list.
956            final List<DrawerItem> allFoldersList = new ArrayList<DrawerItem>();
957            final List<DrawerItem> inboxFolders = new ArrayList<DrawerItem>();
958            do {
959                final Folder f = mCursor.getModel();
960                if (!isFolderTypeExcluded(f)) {
961                    if (f.isProviderFolder() && f.isInbox()) {
962                        inboxFolders.add(DrawerItem.ofFolder(
963                                mActivity, f, DrawerItem.FOLDER_SYSTEM, mCursor.getPosition()));
964                    } else {
965                        allFoldersList.add(DrawerItem.ofFolder(
966                                mActivity, f, DrawerItem.FOLDER_USER, mCursor.getPosition()));
967                    }
968                }
969            } while (mCursor.moveToNext());
970
971            // Add all inboxes (sectioned included) before recents.
972            addFolderSection(itemList, inboxFolders, NO_HEADER_RESOURCE);
973
974            // Add most recently folders (in alphabetical order) next.
975            addRecentsToList(itemList);
976
977            // Add the remaining provider folders followed by all labels.
978            addFolderSection(itemList, allFoldersList,  R.string.all_folders_heading);
979        }
980
981        /**
982         * Given a list of folders as {@link DrawerItem}s, add them to the item
983         * list as needed. Passing in a non-0 integer for the resource will
984         * enable a header
985         *
986         * @param destination List of drawer items to populate
987         * @param source List of drawer items representing folders to add to the drawer
988         * @param headerStringResource
989         *            {@link FolderListAdapter#NO_HEADER_RESOURCE} if no header
990         *            is required, or res-id otherwise
991         */
992        private void addFolderSection(List<DrawerItem> destination, List<DrawerItem> source,
993                int headerStringResource) {
994            if (source.size() > 0) {
995                if(headerStringResource != NO_HEADER_RESOURCE) {
996                    destination.add(DrawerItem.ofHeader(mActivity, headerStringResource));
997                }
998                destination.addAll(source);
999            }
1000        }
1001
1002        /**
1003         * Add recent folders to the list in order as acquired by the {@link RecentFolderList}.
1004         *
1005         * @param destination List of drawer items to populate
1006         */
1007        private void addRecentsToList(List<DrawerItem> destination) {
1008            // If there are recent folders, add them.
1009            final List<Folder> recentFolderList = getRecentFolders(mRecentFolders);
1010
1011            // Remove any excluded folder types
1012            if (mExcludedFolderTypes != null) {
1013                final Iterator<Folder> iterator = recentFolderList.iterator();
1014                while (iterator.hasNext()) {
1015                    if (isFolderTypeExcluded(iterator.next())) {
1016                        iterator.remove();
1017                    }
1018                }
1019            }
1020
1021            if (recentFolderList.size() > 0) {
1022                destination.add(DrawerItem.ofHeader(mActivity, R.string.recent_folders_heading));
1023                // Recent folders are not queried for position.
1024                final int position = -1;
1025                for (Folder f : recentFolderList) {
1026                    destination.add(DrawerItem.ofFolder(mActivity, f, DrawerItem.FOLDER_RECENT,
1027                            position));
1028                }
1029            }
1030        }
1031
1032        /**
1033         * Check if the cursor provided is valid.
1034         * @param mCursor
1035         * @return True if cursor is invalid, false otherwise
1036         */
1037        private boolean isCursorInvalid(Cursor mCursor) {
1038            return mCursor == null || mCursor.isClosed()|| mCursor.getCount() <= 0
1039                    || !mCursor.moveToFirst();
1040        }
1041
1042        @Override
1043        public void setCursor(ObjectCursor<Folder> cursor) {
1044            mCursor = cursor;
1045            recalculateList();
1046        }
1047
1048        @Override
1049        public Object getItem(int position) {
1050            return mItemList.get(position);
1051        }
1052
1053        @Override
1054        public long getItemId(int position) {
1055            return getItem(position).hashCode();
1056        }
1057
1058        @Override
1059        public final void destroy() {
1060            mRecentFolderObserver.unregisterAndDestroy();
1061        }
1062
1063        @Override
1064        public int getItemType(DrawerItem item) {
1065            return item.mType;
1066        }
1067
1068        // TODO(viki): This is strange. We have the full folder and yet we create on from scratch.
1069        @Override
1070        public Folder getFullFolder(DrawerItem folderItem) {
1071            if (folderItem.mFolderType == DrawerItem.FOLDER_RECENT) {
1072                return folderItem.mFolder;
1073            } else {
1074                final int pos = folderItem.mPosition;
1075                if (pos > -1 && mCursor != null && !mCursor.isClosed()
1076                        && mCursor.moveToPosition(folderItem.mPosition)) {
1077                    return mCursor.getModel();
1078                } else {
1079                    return null;
1080                }
1081            }
1082        }
1083    }
1084
1085    private class HierarchicalFolderListAdapter extends ArrayAdapter<Folder>
1086            implements FolderListFragmentCursorAdapter{
1087
1088        private static final int PARENT = 0;
1089        private static final int CHILD = 1;
1090        private final Uri mParentUri;
1091        private final Folder mParent;
1092        private final FolderItemView.DropHandler mDropHandler;
1093        private ObjectCursor<Folder> mCursor;
1094
1095        public HierarchicalFolderListAdapter(ObjectCursor<Folder> c, Folder parentFolder) {
1096            super(mActivity.getActivityContext(), R.layout.folder_item);
1097            mDropHandler = mActivity;
1098            mParent = parentFolder;
1099            mParentUri = parentFolder.uri;
1100            setCursor(c);
1101        }
1102
1103        @Override
1104        public int getViewTypeCount() {
1105            // Child and Parent
1106            return 2;
1107        }
1108
1109        @Override
1110        public int getItemViewType(int position) {
1111            final Folder f = getItem(position);
1112            return f.uri.equals(mParentUri) ? PARENT : CHILD;
1113        }
1114
1115        @Override
1116        public View getView(int position, View convertView, ViewGroup parent) {
1117            final FolderItemView folderItemView;
1118            final Folder folder = getItem(position);
1119            boolean isParent = folder.uri.equals(mParentUri);
1120            if (convertView != null) {
1121                folderItemView = (FolderItemView) convertView;
1122            } else {
1123                int resId = isParent ? R.layout.folder_item : R.layout.child_folder_item;
1124                folderItemView = (FolderItemView) LayoutInflater.from(
1125                        mActivity.getActivityContext()).inflate(resId, null);
1126            }
1127            folderItemView.bind(folder, mDropHandler);
1128            if (folder.uri.equals(mSelectedFolderUri)) {
1129                getListView().setItemChecked(position, true);
1130                // If this is the current folder, also check to verify that the unread count
1131                // matches what the action bar shows.
1132                final boolean unreadCountDiffers = (mCurrentFolderForUnreadCheck != null)
1133                        && folder.unreadCount != mCurrentFolderForUnreadCheck.unreadCount;
1134                if (unreadCountDiffers) {
1135                    folderItemView.overrideUnreadCount(mCurrentFolderForUnreadCheck.unreadCount);
1136                }
1137            }
1138            Folder.setFolderBlockColor(folder, folderItemView.findViewById(R.id.color_block));
1139            Folder.setIcon(folder, (ImageView) folderItemView.findViewById(R.id.folder_icon));
1140            return folderItemView;
1141        }
1142
1143        @Override
1144        public void setCursor(ObjectCursor<Folder> cursor) {
1145            mCursor = cursor;
1146            clear();
1147            if (mParent != null) {
1148                add(mParent);
1149            }
1150            if (cursor != null && cursor.getCount() > 0) {
1151                cursor.moveToFirst();
1152                do {
1153                    Folder f = cursor.getModel();
1154                    f.parent = mParent;
1155                    add(f);
1156                } while (cursor.moveToNext());
1157            }
1158        }
1159
1160        @Override
1161        public void destroy() {
1162            // Do nothing.
1163        }
1164
1165        @Override
1166        public int getItemType(DrawerItem item) {
1167            // Always returns folders for now.
1168            return DrawerItem.VIEW_FOLDER;
1169        }
1170
1171        @Override
1172        public Folder getFullFolder(DrawerItem folderItem) {
1173            final int pos = folderItem.mPosition;
1174            if (mCursor == null || mCursor.isClosed()) {
1175                return null;
1176            }
1177            if (pos > -1 && mCursor != null && !mCursor.isClosed()
1178                    && mCursor.moveToPosition(folderItem.mPosition)) {
1179                return mCursor.getModel();
1180            } else {
1181                return null;
1182            }
1183        }
1184
1185        @Override
1186        public void notifyAllAccountsChanged() {
1187            // Do nothing. We don't care about changes to all accounts.
1188        }
1189    }
1190
1191    public Folder getParentFolder() {
1192        return mParentFolder;
1193    }
1194
1195    /**
1196     * Sets the currently selected folder safely.
1197     * @param folder
1198     */
1199    private void setSelectedFolder(Folder folder) {
1200        if (folder == null) {
1201            mSelectedFolderUri = Uri.EMPTY;
1202            LogUtils.e(LOG_TAG, "FolderListFragment.setSelectedFolder(null) called!");
1203            return;
1204        }
1205        mCurrentFolderForUnreadCheck = folder;
1206        mSelectedFolderUri = folder.uri;
1207        setSelectedFolderType(folder);
1208        final boolean viewChanged =
1209                !FolderItemView.areSameViews(folder, mCurrentFolderForUnreadCheck);
1210        if (mCursorAdapter != null && viewChanged) {
1211            mCursorAdapter.notifyDataSetChanged();
1212        }
1213    }
1214
1215    /**
1216     * Sets the selected folder type safely.
1217     * @param folder folder to set to.
1218     */
1219    private void setSelectedFolderType(Folder folder) {
1220        if (mSelectedFolderType == DrawerItem.UNSET) {
1221            mSelectedFolderType = folder.isProviderFolder() ? DrawerItem.FOLDER_SYSTEM
1222                    : DrawerItem.FOLDER_USER;
1223        }
1224    }
1225
1226    /**
1227     * Sets the current account to the one provided here.
1228     * @param account the current account to set to.
1229     */
1230    private void setSelectedAccount(Account account){
1231        final boolean changed = (account != null) && (mCurrentAccount == null
1232                || !mCurrentAccount.uri.equals(account.uri));
1233        mCurrentAccount = account;
1234        if (changed) {
1235            // We no longer have proper folder objects. Let the new ones come in
1236            mCursorAdapter.setCursor(null);
1237            // If currentAccount is different from the one we set, restart the loader. Look at the
1238            // comment on {@link AbstractActivityController#restartOptionalLoader} to see why we
1239            // don't just do restartLoader.
1240            final LoaderManager manager = getLoaderManager();
1241            manager.destroyLoader(FOLDER_LOADER_ID);
1242            manager.restartLoader(FOLDER_LOADER_ID, Bundle.EMPTY, this);
1243            // An updated cursor causes the entire list to refresh. No need to refresh the list.
1244        } else if (account == null) {
1245            // This should never happen currently, but is a safeguard against a very incorrect
1246            // non-null account -> null account transition.
1247            LogUtils.e(LOG_TAG, "FLF.setSelectedAccount(null) called! Destroying existing loader.");
1248            final LoaderManager manager = getLoaderManager();
1249            manager.destroyLoader(FOLDER_LOADER_ID);
1250        }
1251    }
1252
1253    public interface FolderListSelectionListener {
1254        public void onFolderSelected(Folder folder);
1255    }
1256
1257    /**
1258     * Get whether the FolderListFragment is currently showing the hierarchy
1259     * under a single parent.
1260     */
1261    public boolean showingHierarchy() {
1262        return mParentFolder != null;
1263    }
1264
1265    /**
1266     * Checks if the specified {@link Folder} is a type that we want to exclude from displaying.
1267     */
1268    private boolean isFolderTypeExcluded(final Folder folder) {
1269        if (mExcludedFolderTypes == null) {
1270            return false;
1271        }
1272
1273        for (final int excludedType : mExcludedFolderTypes) {
1274            if (folder.isType(excludedType)) {
1275                return true;
1276            }
1277        }
1278
1279        return false;
1280    }
1281}
1282