Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2013 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 package com.android.mail.ui;
     18 
     19 import android.app.LoaderManager;
     20 import android.app.LoaderManager.LoaderCallbacks;
     21 import android.content.Context;
     22 import android.content.Loader;
     23 import android.content.res.Resources;
     24 import android.graphics.Color;
     25 import android.net.Uri;
     26 import android.os.Bundle;
     27 import android.support.v4.text.BidiFormatter;
     28 import android.support.v4.util.SparseArrayCompat;
     29 import android.text.TextUtils;
     30 import android.util.AttributeSet;
     31 import android.view.LayoutInflater;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.widget.ImageView;
     35 import android.widget.LinearLayout;
     36 import android.widget.TextView;
     37 
     38 import com.android.emailcommon.mail.Address;
     39 import com.android.mail.R;
     40 import com.android.mail.browse.ConversationCursor;
     41 import com.android.mail.content.ObjectCursor;
     42 import com.android.mail.content.ObjectCursorLoader;
     43 import com.android.mail.providers.Account;
     44 import com.android.mail.providers.Conversation;
     45 import com.android.mail.providers.Folder;
     46 import com.android.mail.providers.ParticipantInfo;
     47 import com.android.mail.providers.UIProvider;
     48 import com.android.mail.providers.UIProvider.AccountCapabilities;
     49 import com.android.mail.providers.UIProvider.ConversationListQueryParameters;
     50 import com.android.mail.utils.LogUtils;
     51 import com.android.mail.utils.Utils;
     52 import com.google.common.collect.ImmutableList;
     53 import com.google.common.collect.ImmutableSortedSet;
     54 import com.google.common.collect.Lists;
     55 import com.google.common.collect.Maps;
     56 
     57 import java.util.ArrayList;
     58 import java.util.Collections;
     59 import java.util.Comparator;
     60 import java.util.List;
     61 import java.util.Map;
     62 
     63 /**
     64  * The teaser list item in the conversation list that shows nested folders.
     65  */
     66 public class NestedFolderTeaserView extends LinearLayout implements ConversationSpecialItemView {
     67     private static final String LOG_TAG = "NestedFolderTeaserView";
     68 
     69     private boolean mShouldDisplayInList = false;
     70 
     71     private Account mAccount;
     72     private Uri mFolderListUri;
     73     private FolderSelector mListener;
     74 
     75     private LoaderManager mLoaderManager = null;
     76     private AnimatedAdapter mAdapter = null;
     77 
     78     private final SparseArrayCompat<FolderHolder> mFolderHolders =
     79             new SparseArrayCompat<FolderHolder>();
     80 
     81     private final int mFolderItemUpdateDelayMs;
     82 
     83     private int mAnimatedHeight = -1;
     84 
     85     private ViewGroup mNestedFolderContainer;
     86 
     87     private View mShowMoreFoldersRow;
     88     private TextView mShowMoreFoldersTextView;
     89     private TextView mShowMoreFoldersCountTextView;
     90 
     91     /**
     92      * If <code>true</code> we show a limited set of folders, and a means to show all folders. If
     93      * <code>false</code>, we show all folders.
     94      */
     95     private boolean mCollapsed = true;
     96 
     97     private View mTeaserRightEdge;
     98     /** Whether we are on a tablet device or not */
     99     private final boolean mTabletDevice;
    100     /** When in conversation mode, true if the list is hidden */
    101     private final boolean mListCollapsible;
    102 
    103     /** If <code>true</code>, the list of folders has updated since the view was last shown. */
    104     private boolean mListUpdated;
    105 
    106     // Each folder's loader will be this value plus the folder id
    107     private static final int LOADER_FOLDER_LIST =
    108             AbstractActivityController.LAST_FRAGMENT_LOADER_ID + 100000;
    109 
    110     /**
    111      * The maximum number of senders to show in the sender snippet.
    112      */
    113     private static final String MAX_SENDERS = "20";
    114 
    115     /**
    116      * The number of folders to show when the teaser is collapsed.
    117      */
    118     private static int sCollapsedFolderThreshold = -1;
    119 
    120     private static class FolderHolder {
    121         private final View mItemView;
    122         private final TextView mSendersTextView;
    123         private final TextView mCountTextView;
    124         private Folder mFolder;
    125         private List<String> mUnreadSenders = ImmutableList.of();
    126 
    127         public FolderHolder(final View itemView, final TextView sendersTextView,
    128                 final TextView countTextView) {
    129             mItemView = itemView;
    130             mSendersTextView = sendersTextView;
    131             mCountTextView = countTextView;
    132         }
    133 
    134         public void setFolder(final Folder folder) {
    135             mFolder = folder;
    136         }
    137 
    138         public View getItemView() {
    139             return mItemView;
    140         }
    141 
    142         public TextView getSendersTextView() {
    143             return mSendersTextView;
    144         }
    145 
    146         public TextView getCountTextView() {
    147             return mCountTextView;
    148         }
    149 
    150         public Folder getFolder() {
    151             return mFolder;
    152         }
    153 
    154         /**
    155          * @return a {@link List} of senders of unread messages
    156          */
    157         public List<String> getUnreadSenders() {
    158             return mUnreadSenders;
    159         }
    160 
    161         public void setUnreadSenders(final List<String> unreadSenders) {
    162             mUnreadSenders = unreadSenders;
    163         }
    164 
    165         public static final Comparator<FolderHolder> NAME_COMPARATOR =
    166                 new Comparator<FolderHolder>() {
    167             @Override
    168             public int compare(final FolderHolder lhs, final FolderHolder rhs) {
    169                 return lhs.getFolder().name.compareTo(rhs.getFolder().name);
    170             }
    171         };
    172     }
    173 
    174     public NestedFolderTeaserView(final Context context) {
    175         this(context, null);
    176     }
    177 
    178     public NestedFolderTeaserView(final Context context, final AttributeSet attrs) {
    179         this(context, attrs, -1);
    180     }
    181 
    182     public NestedFolderTeaserView(
    183             final Context context, final AttributeSet attrs, final int defStyle) {
    184         super(context, attrs, defStyle);
    185 
    186         final Resources resources = context.getResources();
    187 
    188         if (sCollapsedFolderThreshold < 0) {
    189             sCollapsedFolderThreshold =
    190                     resources.getInteger(R.integer.nested_folders_collapse_threshold);
    191         }
    192 
    193         mFolderItemUpdateDelayMs =
    194                 resources.getInteger(R.integer.folder_item_refresh_delay_ms);
    195 
    196         mTabletDevice = com.android.mail.utils.Utils.useTabletUI(resources);
    197         mListCollapsible = resources.getBoolean(R.bool.list_collapsible);
    198     }
    199 
    200     @Override
    201     protected void onFinishInflate() {
    202         mNestedFolderContainer = (ViewGroup) findViewById(R.id.nested_folder_container);
    203         mTeaserRightEdge = findViewById(R.id.teaser_right_edge);
    204 
    205         mShowMoreFoldersRow = findViewById(R.id.show_more_folders_row);
    206         mShowMoreFoldersRow.setOnClickListener(mShowMoreOnClickListener);
    207 
    208         mShowMoreFoldersTextView = (TextView) findViewById(R.id.show_more_folders_textView);
    209         mShowMoreFoldersCountTextView =
    210                 (TextView) findViewById(R.id.show_more_folders_count_textView);
    211     }
    212 
    213     public void bind(final Account account, final FolderSelector listener) {
    214         mAccount = account;
    215         mListener = listener;
    216     }
    217 
    218     /**
    219      * Creates a {@link FolderHolder}.
    220      */
    221     private FolderHolder createFolderHolder(final CharSequence folderName) {
    222         final View itemView =
    223                 LayoutInflater.from(getContext()).inflate(R.layout.folder_teaser_item, null);
    224 
    225         final ImageView imageView = (ImageView) itemView.findViewById(R.id.folder_imageView);
    226         imageView.setImageResource(R.drawable.ic_menu_move_to_holo_light);
    227         // Remove background
    228         imageView.setBackgroundColor(Color.TRANSPARENT);
    229 
    230         ((TextView) itemView.findViewById(R.id.folder_textView)).setText(folderName);
    231         final TextView sendersTextView = (TextView) itemView.findViewById(R.id.senders_textView);
    232         final TextView countTextView = (TextView) itemView.findViewById(R.id.count_textView);
    233         final FolderHolder holder = new FolderHolder(itemView, sendersTextView, countTextView);
    234 
    235         attachOnClickListener(itemView, holder);
    236 
    237         return holder;
    238     }
    239 
    240     private void attachOnClickListener(final View view, final FolderHolder holder) {
    241         view.setOnClickListener(new OnClickListener() {
    242             @Override
    243             public void onClick(final View v) {
    244                 mListener.onFolderSelected(holder.getFolder());
    245             }
    246         });
    247     }
    248 
    249     @Override
    250     public void onUpdate(final Folder folder, final ConversationCursor cursor) {
    251         mShouldDisplayInList = false; // Assume disabled
    252 
    253         if (folder == null) {
    254             return;
    255         }
    256 
    257         final Uri folderListUri = folder.childFoldersListUri;
    258         if (folderListUri == null) {
    259             return;
    260         }
    261 
    262         // If we don't support nested folders, don't show this view
    263         if (!mAccount.supportsCapability(AccountCapabilities.NESTED_FOLDERS)) {
    264             return;
    265         }
    266 
    267         if (mFolderListUri == null || !mFolderListUri.equals(folder.childFoldersListUri)) {
    268             // We have a new uri
    269             mFolderListUri = folderListUri;
    270 
    271             // Restart the loader
    272             mLoaderManager.destroyLoader(LOADER_FOLDER_LIST);
    273             mLoaderManager.initLoader(LOADER_FOLDER_LIST, null, mFolderListLoaderCallbacks);
    274         }
    275 
    276         mShouldDisplayInList = true; // Now we know we have something to display
    277     }
    278 
    279     @Override
    280     public void onGetView() {
    281         if (mListUpdated) {
    282             // Clear out the folder views
    283             mNestedFolderContainer.removeAllViews();
    284 
    285             // Sort the folders by name
    286             // TODO(skennedy) recents? starred?
    287             final ImmutableSortedSet.Builder<FolderHolder> folderHoldersBuilder =
    288                     new ImmutableSortedSet.Builder<FolderHolder>(FolderHolder.NAME_COMPARATOR);
    289 
    290             for (int i = 0; i < mFolderHolders.size(); i++) {
    291                 folderHoldersBuilder.add(mFolderHolders.valueAt(i));
    292             }
    293 
    294             final ImmutableSortedSet<FolderHolder> folderHolders = folderHoldersBuilder.build();
    295 
    296             // Add all folder views to the teaser
    297             int added = 0;
    298             // If we're only over the limit by one, don't truncate the list.
    299             boolean truncate = folderHolders.size() > sCollapsedFolderThreshold + 1;
    300             for (final FolderHolder folderHolder : folderHolders) {
    301                 mNestedFolderContainer.addView(folderHolder.getItemView());
    302                 added++;
    303 
    304                 if (truncate && added >= sCollapsedFolderThreshold && mCollapsed) {
    305                     // We will display the rest when "Show more" is clicked
    306                     break;
    307                 }
    308             }
    309 
    310             updateShowMoreView();
    311 
    312             mListUpdated = false;
    313         }
    314     }
    315 
    316     private final OnClickListener mShowMoreOnClickListener = new OnClickListener() {
    317         @Override
    318         public void onClick(final View v) {
    319             mCollapsed = !mCollapsed;
    320             mListUpdated = true;
    321             mAdapter.notifyDataSetChanged();
    322         }
    323     };
    324 
    325     private void updateShowMoreView() {
    326         final int total = mFolderHolders.size();
    327         final int displayed = mNestedFolderContainer.getChildCount();
    328         final int notShown = total - displayed;
    329 
    330         if (notShown > 0) {
    331             // We are not displaying all the folders
    332             mShowMoreFoldersRow.setVisibility(VISIBLE);
    333             mShowMoreFoldersTextView.setText(String.format(
    334                     getContext().getString(R.string.show_n_more_folders), notShown));
    335             mShowMoreFoldersCountTextView.setVisibility(VISIBLE);
    336 
    337             // Get a count of unread messages in other folders
    338             int unreadCount = 0;
    339             for (int i = 0; i < mFolderHolders.size(); i++) {
    340                 final FolderHolder holder = mFolderHolders.valueAt(i);
    341 
    342                 if (holder.getItemView().getParent() == null) {
    343                     // This view is not shown, so we want to use its unread count
    344                     // TODO(skennedy) We want a "nested" unread count, that includes the unread
    345                     // count of nested folders
    346                     unreadCount += holder.getFolder().unreadCount;
    347                 }
    348             }
    349 
    350             mShowMoreFoldersCountTextView.setText(Integer.toString(unreadCount));
    351         } else if (displayed > sCollapsedFolderThreshold + 1) {
    352             // We are expanded
    353             mShowMoreFoldersRow.setVisibility(VISIBLE);
    354             mShowMoreFoldersTextView.setText(R.string.hide_folders);
    355             mShowMoreFoldersCountTextView.setVisibility(GONE);
    356         } else {
    357             // We don't need to collapse the folders
    358             mShowMoreFoldersRow.setVisibility(GONE);
    359         }
    360     }
    361 
    362     private void updateViews(final FolderHolder folderHolder) {
    363         final Folder folder = folderHolder.getFolder();
    364 
    365         final String unreadText = Utils.getUnreadCountString(getContext(), folder.unreadCount);
    366         folderHolder.getCountTextView().setText(unreadText.isEmpty() ? "0" : unreadText);
    367 
    368         final String sendersText = TextUtils.join(
    369                 getResources().getString(R.string.enumeration_comma),
    370                 folderHolder.getUnreadSenders());
    371         folderHolder.getSendersTextView().setText(sendersText);
    372     }
    373 
    374     @Override
    375     public boolean getShouldDisplayInList() {
    376         return mShouldDisplayInList;
    377     }
    378 
    379     @Override
    380     public int getPosition() {
    381         return 0;
    382     }
    383 
    384     @Override
    385     public void setAdapter(final AnimatedAdapter adapter) {
    386         mAdapter = adapter;
    387     }
    388 
    389     @Override
    390     public void bindFragment(final LoaderManager loaderManager, final Bundle savedInstanceState) {
    391         if (mLoaderManager != null) {
    392             throw new IllegalStateException("This view has already been bound to a LoaderManager.");
    393         }
    394 
    395         mLoaderManager = loaderManager;
    396     }
    397 
    398     @Override
    399     public void cleanup() {
    400         // Do nothing
    401     }
    402 
    403     @Override
    404     public void onConversationSelected() {
    405         // Do nothing
    406     }
    407 
    408     @Override
    409     public void onCabModeEntered() {
    410         // Do nothing
    411     }
    412 
    413     @Override
    414     public void onCabModeExited() {
    415         // Do nothing
    416     }
    417 
    418     @Override
    419     public void onConversationListVisibilityChanged(final boolean visible) {
    420         // Do nothing
    421     }
    422 
    423     @Override
    424     public void saveInstanceState(final Bundle outState) {
    425         // Do nothing
    426     }
    427 
    428     @Override
    429     public boolean acceptsUserTaps() {
    430         // The teaser does not allow user tap in the list.
    431         return false;
    432     }
    433 
    434     @Override
    435     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
    436         if (com.android.mail.utils.Utils.getDisplayListRightEdgeEffect(mTabletDevice,
    437                 mListCollapsible, mAdapter.getViewMode())) {
    438             mTeaserRightEdge.setVisibility(VISIBLE);
    439         } else {
    440             mTeaserRightEdge.setVisibility(GONE);
    441         }
    442 
    443         if (mAnimatedHeight == -1) {
    444             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    445         } else {
    446             setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mAnimatedHeight);
    447         }
    448     }
    449 
    450     private static int getLoaderId(final int folderId) {
    451         return folderId + LOADER_FOLDER_LIST;
    452     }
    453 
    454     private static int getFolderId(final int loaderId) {
    455         return loaderId - LOADER_FOLDER_LIST;
    456     }
    457 
    458     private final LoaderCallbacks<ObjectCursor<Folder>> mFolderListLoaderCallbacks =
    459             new LoaderCallbacks<ObjectCursor<Folder>>() {
    460         @Override
    461         public void onLoaderReset(final Loader<ObjectCursor<Folder>> loader) {
    462             // Do nothing
    463         }
    464 
    465         @Override
    466         public void onLoadFinished(final Loader<ObjectCursor<Folder>> loader,
    467                 final ObjectCursor<Folder> data) {
    468             if (data != null) {
    469                 // We need to keep track of all current folders in case one has been removed
    470                 final List<Integer> oldFolderIds = new ArrayList<Integer>(mFolderHolders.size());
    471                 for (int i = 0; i < mFolderHolders.size(); i++) {
    472                     oldFolderIds.add(mFolderHolders.keyAt(i));
    473                 }
    474 
    475                 if (data.moveToFirst()) {
    476                     do {
    477                         final Folder folder = data.getModel();
    478                         final FolderHolder holder = mFolderHolders.get(folder.id);
    479 
    480                         if (holder != null) {
    481                             final Folder oldFolder = holder.getFolder();
    482                             holder.setFolder(folder);
    483 
    484                             /*
    485                              * We only need to change anything if the old Folder was null, or the
    486                              * unread count has changed.
    487                              */
    488                             if (oldFolder == null || oldFolder.unreadCount != folder.unreadCount) {
    489                                 populateUnreadSenders(holder, folder.unreadSenders);
    490                                 updateViews(holder);
    491                             }
    492                         } else {
    493                             // Create the holder, and init a loader
    494                             final FolderHolder newHolder = createFolderHolder(folder.name);
    495                             newHolder.setFolder(folder);
    496                             mFolderHolders.put(folder.id, newHolder);
    497 
    498                             // We can not support displaying sender info with nested folders
    499                             // because it doesn't scale. Disabling it for now, until we can
    500                             // optimize it.
    501                             // initFolderLoader(getLoaderId(folder.id));
    502                             populateUnreadSenders(newHolder, folder.unreadSenders);
    503 
    504                             updateViews(newHolder);
    505 
    506                             mListUpdated = true;
    507                         }
    508 
    509                         // Note: #remove(int) removes from that POSITION
    510                         //       #remove(Integer) removes that OBJECT
    511                         oldFolderIds.remove(Integer.valueOf(folder.id));
    512                     } while (data.moveToNext());
    513                 }
    514 
    515                 for (final int folderId : oldFolderIds) {
    516                     // We have a folder that no longer exists
    517                     mFolderHolders.remove(folderId);
    518                     mLoaderManager.destroyLoader(getLoaderId(folderId));
    519                     mListUpdated = true;
    520                 }
    521 
    522                 // If the list has not changed, we've already updated the counts, etc.
    523                 // If the list has changed, we need to rebuild it
    524                 if (mListUpdated) {
    525                     mAdapter.notifyDataSetChanged();
    526                 }
    527             } else {
    528                 LogUtils.w(LOG_TAG, "Problem with folder list cursor returned from loader");
    529             }
    530         }
    531 
    532         private void initFolderLoader(final int loaderId) {
    533             LogUtils.d(LOG_TAG, "Initializing folder loader %d", loaderId);
    534             mLoaderManager.initLoader(loaderId, null, mFolderLoaderCallbacks);
    535         }
    536 
    537         @Override
    538         public Loader<ObjectCursor<Folder>> onCreateLoader(final int id, final Bundle args) {
    539             final ObjectCursorLoader<Folder> loader = new ObjectCursorLoader<Folder>(getContext(),
    540                     mFolderListUri, UIProvider.FOLDERS_PROJECTION_WITH_UNREAD_SENDERS,
    541                     Folder.FACTORY);
    542             loader.setUpdateThrottle(mFolderItemUpdateDelayMs);
    543             return loader;
    544         }
    545     };
    546 
    547     /**
    548      * This code is intended to roughly duplicate the FolderLoaderCallback's onLoadFinished
    549      */
    550     private void populateUnreadSenders(final FolderHolder folderHolder,
    551             final String unreadSenders) {
    552         if (TextUtils.isEmpty(unreadSenders)) {
    553             folderHolder.setUnreadSenders(Collections.<String>emptyList());
    554             return;
    555         }
    556         // Use a LinkedHashMap here to maintain ordering
    557         final Map<String, String> emailtoNameMap = Maps.newLinkedHashMap();
    558 
    559         final Address[] senderAddresses = Address.parse(unreadSenders);
    560 
    561         final BidiFormatter bidiFormatter = mAdapter.getBidiFormatter();
    562         for (final Address senderAddress : senderAddresses) {
    563             String sender = senderAddress.getPersonal();
    564             sender = (sender != null) ? bidiFormatter.unicodeWrap(sender) : null;
    565             final String senderEmail = senderAddress.getAddress();
    566 
    567             if (!TextUtils.isEmpty(sender)) {
    568                 final String existingSender = emailtoNameMap.get(senderEmail);
    569                 if (!TextUtils.isEmpty(existingSender)) {
    570                     // Prefer longer names
    571                     if (existingSender.length() >= sender.length()) {
    572                         // old name is longer
    573                         sender = existingSender;
    574                     }
    575                 }
    576                 emailtoNameMap.put(senderEmail, sender);
    577             }
    578             if (emailtoNameMap.size() >= 20) {
    579                 break;
    580             }
    581         }
    582 
    583         final List<String> senders = Lists.newArrayList(emailtoNameMap.values());
    584         folderHolder.setUnreadSenders(senders);
    585     }
    586 
    587     private final LoaderCallbacks<ObjectCursor<Conversation>> mFolderLoaderCallbacks =
    588             new LoaderCallbacks<ObjectCursor<Conversation>>() {
    589         @Override
    590         public void onLoaderReset(final Loader<ObjectCursor<Conversation>> loader) {
    591             // Do nothing
    592         }
    593 
    594         @Override
    595         public void onLoadFinished(final Loader<ObjectCursor<Conversation>> loader,
    596                 final ObjectCursor<Conversation> data) {
    597             // Sometimes names are condensed to just the first name.
    598             // This data structure keeps a map of emails to names
    599             final Map<String, String> emailToNameMap = Maps.newHashMap();
    600             final List<String> senders = Lists.newArrayList();
    601 
    602             final int folderId = getFolderId(loader.getId());
    603 
    604             final FolderHolder folderHolder = mFolderHolders.get(folderId);
    605             final int maxSenders = folderHolder.mFolder.unreadCount;
    606 
    607             if (maxSenders > 0 && data != null && data.moveToFirst()) {
    608                 LogUtils.d(LOG_TAG, "Folder id %d loader finished", folderId);
    609 
    610                 // Look through all conversations until we find 'maxSenders' unread
    611                 int sendersFound = 0;
    612 
    613                 do {
    614                     final Conversation conversation = data.getModel();
    615 
    616                     if (!conversation.read) {
    617                         String sender = null;
    618                         String senderEmail = null;
    619                         int priority = Integer.MIN_VALUE;
    620 
    621                         // Find the highest priority participant
    622                         for (final ParticipantInfo p :
    623                                 conversation.conversationInfo.participantInfos) {
    624                             if (sender == null || priority < p.priority) {
    625                                 sender = p.name;
    626                                 senderEmail = p.email;
    627                                 priority = p.priority;
    628                             }
    629                         }
    630 
    631                         if (sender != null) {
    632                             sendersFound++;
    633                             final String existingSender = emailToNameMap.get(senderEmail);
    634                             if (existingSender != null) {
    635                                 // Prefer longer names
    636                                 if (existingSender.length() >= sender.length()) {
    637                                     // old name is longer
    638                                     sender = existingSender;
    639                                 } else {
    640                                     // new name is longer
    641                                     int index = senders.indexOf(existingSender);
    642                                     senders.set(index, sender);
    643                                 }
    644                             } else {
    645                                 senders.add(sender);
    646                             }
    647                             emailToNameMap.put(senderEmail, sender);
    648                         }
    649                     }
    650                 } while (data.moveToNext() && sendersFound < maxSenders);
    651             } else {
    652                 LogUtils.w(LOG_TAG, "Problem with folder cursor returned from loader");
    653             }
    654 
    655             folderHolder.setUnreadSenders(senders);
    656 
    657             /*
    658              * Just update the views in place. We don't need to call notifyDataSetChanged()
    659              * because we aren't changing the teaser's visibility or position.
    660              */
    661             updateViews(folderHolder);
    662         }
    663 
    664         @Override
    665         public Loader<ObjectCursor<Conversation>> onCreateLoader(final int id, final Bundle args) {
    666             final int folderId = getFolderId(id);
    667             final Uri uri = mFolderHolders.get(folderId).mFolder.conversationListUri
    668                     .buildUpon()
    669                     .appendQueryParameter(ConversationListQueryParameters.USE_NETWORK,
    670                             Boolean.FALSE.toString())
    671                     .appendQueryParameter(ConversationListQueryParameters.LIMIT, MAX_SENDERS)
    672                     .build();
    673             return new ObjectCursorLoader<Conversation>(getContext(), uri,
    674                     UIProvider.CONVERSATION_PROJECTION, Conversation.FACTORY);
    675         }
    676     };
    677 
    678     @Override
    679     public boolean commitLeaveBehindItem() {
    680         // This view has no leave-behind
    681         return false;
    682     }
    683 }
    684