Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts.common.list;
     17 
     18 import android.content.Context;
     19 import android.content.CursorLoader;
     20 import android.database.Cursor;
     21 import android.net.Uri;
     22 import android.os.Bundle;
     23 import android.provider.ContactsContract;
     24 import android.provider.ContactsContract.ContactCounts;
     25 import android.provider.ContactsContract.Contacts;
     26 import android.provider.ContactsContract.Directory;
     27 import android.text.TextUtils;
     28 import android.util.Log;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.widget.QuickContactBadge;
     33 import android.widget.SectionIndexer;
     34 import android.widget.TextView;
     35 
     36 import com.android.contacts.common.ContactPhotoManager;
     37 import com.android.contacts.common.R;
     38 import com.android.contacts.common.util.SearchUtil;
     39 
     40 import java.util.HashSet;
     41 
     42 /**
     43  * Common base class for various contact-related lists, e.g. contact list, phone number list
     44  * etc.
     45  */
     46 public abstract class ContactEntryListAdapter extends IndexerListAdapter {
     47 
     48     private static final String TAG = "ContactEntryListAdapter";
     49 
     50     /**
     51      * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
     52      * be included in the search.
     53      */
     54     public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
     55 
     56     private int mDisplayOrder;
     57     private int mSortOrder;
     58 
     59     private boolean mDisplayPhotos;
     60     private boolean mQuickContactEnabled;
     61 
     62     /**
     63      * indicates if contact queries include profile
     64      */
     65     private boolean mIncludeProfile;
     66 
     67     /**
     68      * indicates if query results includes a profile
     69      */
     70     private boolean mProfileExists;
     71 
     72     private ContactPhotoManager mPhotoLoader;
     73 
     74     private String mQueryString;
     75     private String mUpperCaseQueryString;
     76     private boolean mSearchMode;
     77     private int mDirectorySearchMode;
     78     private int mDirectoryResultLimit = Integer.MAX_VALUE;
     79 
     80     private boolean mEmptyListEnabled = true;
     81 
     82     private boolean mSelectionVisible;
     83 
     84     private ContactListFilter mFilter;
     85     private String mContactsCount = "";
     86     private boolean mDarkTheme = false;
     87 
     88     /** Resource used to provide header-text for default filter. */
     89     private CharSequence mDefaultFilterHeaderText;
     90 
     91     public ContactEntryListAdapter(Context context) {
     92         super(context);
     93         addPartitions();
     94         setDefaultFilterHeaderText(R.string.local_search_label);
     95     }
     96 
     97     protected void setDefaultFilterHeaderText(int resourceId) {
     98         mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
     99     }
    100 
    101     @Override
    102     protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
    103         return new ContactListPinnedHeaderView(context, null);
    104     }
    105 
    106     @Override
    107     protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
    108         ((ContactListPinnedHeaderView)pinnedHeaderView).setSectionHeader(title);
    109     }
    110 
    111     @Override
    112     protected void setPinnedHeaderContactsCount(View header) {
    113         // Update the header with the contacts count only if a profile header exists
    114         // otherwise, the contacts count are shown in the empty profile header view
    115         if (mProfileExists) {
    116             ((ContactListPinnedHeaderView)header).setCountView(mContactsCount);
    117         } else {
    118             clearPinnedHeaderContactsCount(header);
    119         }
    120     }
    121 
    122     @Override
    123     protected void clearPinnedHeaderContactsCount(View header) {
    124         ((ContactListPinnedHeaderView)header).setCountView(null);
    125     }
    126 
    127     protected void addPartitions() {
    128         addPartition(createDefaultDirectoryPartition());
    129     }
    130 
    131     protected DirectoryPartition createDefaultDirectoryPartition() {
    132         DirectoryPartition partition = new DirectoryPartition(true, true);
    133         partition.setDirectoryId(Directory.DEFAULT);
    134         partition.setDirectoryType(getContext().getString(R.string.contactsList));
    135         partition.setPriorityDirectory(true);
    136         partition.setPhotoSupported(true);
    137         return partition;
    138     }
    139 
    140     /**
    141      * Remove all directories after the default directory. This is typically used when contacts
    142      * list screens are asked to exit the search mode and thus need to remove all remote directory
    143      * results for the search.
    144      *
    145      * This code assumes that the default directory and directories before that should not be
    146      * deleted (e.g. Join screen has "suggested contacts" directory before the default director,
    147      * and we should not remove the directory).
    148      */
    149     public void removeDirectoriesAfterDefault() {
    150         final int partitionCount = getPartitionCount();
    151         for (int i = partitionCount - 1; i >= 0; i--) {
    152             final Partition partition = getPartition(i);
    153             if ((partition instanceof DirectoryPartition)
    154                     && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
    155                 break;
    156             } else {
    157                 removePartition(i);
    158             }
    159         }
    160     }
    161 
    162     private int getPartitionByDirectoryId(long id) {
    163         int count = getPartitionCount();
    164         for (int i = 0; i < count; i++) {
    165             Partition partition = getPartition(i);
    166             if (partition instanceof DirectoryPartition) {
    167                 if (((DirectoryPartition)partition).getDirectoryId() == id) {
    168                     return i;
    169                 }
    170             }
    171         }
    172         return -1;
    173     }
    174 
    175     public abstract String getContactDisplayName(int position);
    176     public abstract void configureLoader(CursorLoader loader, long directoryId);
    177 
    178     /**
    179      * Marks all partitions as "loading"
    180      */
    181     public void onDataReload() {
    182         boolean notify = false;
    183         int count = getPartitionCount();
    184         for (int i = 0; i < count; i++) {
    185             Partition partition = getPartition(i);
    186             if (partition instanceof DirectoryPartition) {
    187                 DirectoryPartition directoryPartition = (DirectoryPartition)partition;
    188                 if (!directoryPartition.isLoading()) {
    189                     notify = true;
    190                 }
    191                 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
    192             }
    193         }
    194         if (notify) {
    195             notifyDataSetChanged();
    196         }
    197     }
    198 
    199     @Override
    200     public void clearPartitions() {
    201         int count = getPartitionCount();
    202         for (int i = 0; i < count; i++) {
    203             Partition partition = getPartition(i);
    204             if (partition instanceof DirectoryPartition) {
    205                 DirectoryPartition directoryPartition = (DirectoryPartition)partition;
    206                 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
    207             }
    208         }
    209         super.clearPartitions();
    210     }
    211 
    212     public boolean isSearchMode() {
    213         return mSearchMode;
    214     }
    215 
    216     public void setSearchMode(boolean flag) {
    217         mSearchMode = flag;
    218     }
    219 
    220     public String getQueryString() {
    221         return mQueryString;
    222     }
    223 
    224     public void setQueryString(String queryString) {
    225         mQueryString = queryString;
    226         if (TextUtils.isEmpty(queryString)) {
    227             mUpperCaseQueryString = null;
    228         } else {
    229             mUpperCaseQueryString = SearchUtil
    230                     .cleanStartAndEndOfSearchQuery(queryString.toUpperCase()) ;
    231         }
    232     }
    233 
    234     public String getUpperCaseQueryString() {
    235         return mUpperCaseQueryString;
    236     }
    237 
    238     public int getDirectorySearchMode() {
    239         return mDirectorySearchMode;
    240     }
    241 
    242     public void setDirectorySearchMode(int mode) {
    243         mDirectorySearchMode = mode;
    244     }
    245 
    246     public int getDirectoryResultLimit() {
    247         return mDirectoryResultLimit;
    248     }
    249 
    250     public void setDirectoryResultLimit(int limit) {
    251         this.mDirectoryResultLimit = limit;
    252     }
    253 
    254     public int getContactNameDisplayOrder() {
    255         return mDisplayOrder;
    256     }
    257 
    258     public void setContactNameDisplayOrder(int displayOrder) {
    259         mDisplayOrder = displayOrder;
    260     }
    261 
    262     public int getSortOrder() {
    263         return mSortOrder;
    264     }
    265 
    266     public void setSortOrder(int sortOrder) {
    267         mSortOrder = sortOrder;
    268     }
    269 
    270     public void setPhotoLoader(ContactPhotoManager photoLoader) {
    271         mPhotoLoader = photoLoader;
    272     }
    273 
    274     protected ContactPhotoManager getPhotoLoader() {
    275         return mPhotoLoader;
    276     }
    277 
    278     public boolean getDisplayPhotos() {
    279         return mDisplayPhotos;
    280     }
    281 
    282     public void setDisplayPhotos(boolean displayPhotos) {
    283         mDisplayPhotos = displayPhotos;
    284     }
    285 
    286     public boolean isEmptyListEnabled() {
    287         return mEmptyListEnabled;
    288     }
    289 
    290     public void setEmptyListEnabled(boolean flag) {
    291         mEmptyListEnabled = flag;
    292     }
    293 
    294     public boolean isSelectionVisible() {
    295         return mSelectionVisible;
    296     }
    297 
    298     public void setSelectionVisible(boolean flag) {
    299         this.mSelectionVisible = flag;
    300     }
    301 
    302     public boolean isQuickContactEnabled() {
    303         return mQuickContactEnabled;
    304     }
    305 
    306     public void setQuickContactEnabled(boolean quickContactEnabled) {
    307         mQuickContactEnabled = quickContactEnabled;
    308     }
    309 
    310     public boolean shouldIncludeProfile() {
    311         return mIncludeProfile;
    312     }
    313 
    314     public void setIncludeProfile(boolean includeProfile) {
    315         mIncludeProfile = includeProfile;
    316     }
    317 
    318     public void setProfileExists(boolean exists) {
    319         mProfileExists = exists;
    320         // Stick the "ME" header for the profile
    321         if (exists) {
    322             SectionIndexer indexer = getIndexer();
    323             if (indexer != null) {
    324                 ((ContactsSectionIndexer) indexer).setProfileHeader(
    325                         getContext().getString(R.string.user_profile_contacts_list_header));
    326             }
    327         }
    328     }
    329 
    330     public boolean hasProfile() {
    331         return mProfileExists;
    332     }
    333 
    334     public void setDarkTheme(boolean value) {
    335         mDarkTheme = value;
    336     }
    337 
    338     /**
    339      * Updates partitions according to the directory meta-data contained in the supplied
    340      * cursor.
    341      */
    342     public void changeDirectories(Cursor cursor) {
    343         if (cursor.getCount() == 0) {
    344             // Directory table must have at least local directory, without which this adapter will
    345             // enter very weird state.
    346             Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " +
    347                     "no directory entries.", new RuntimeException());
    348             return;
    349         }
    350         HashSet<Long> directoryIds = new HashSet<Long>();
    351 
    352         int idColumnIndex = cursor.getColumnIndex(Directory._ID);
    353         int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
    354         int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
    355         int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
    356 
    357         // TODO preserve the order of partition to match those of the cursor
    358         // Phase I: add new directories
    359         cursor.moveToPosition(-1);
    360         while (cursor.moveToNext()) {
    361             long id = cursor.getLong(idColumnIndex);
    362             directoryIds.add(id);
    363             if (getPartitionByDirectoryId(id) == -1) {
    364                 DirectoryPartition partition = new DirectoryPartition(false, true);
    365                 partition.setDirectoryId(id);
    366                 partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
    367                 partition.setDisplayName(cursor.getString(displayNameColumnIndex));
    368                 int photoSupport = cursor.getInt(photoSupportColumnIndex);
    369                 partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
    370                         || photoSupport == Directory.PHOTO_SUPPORT_FULL);
    371                 addPartition(partition);
    372             }
    373         }
    374 
    375         // Phase II: remove deleted directories
    376         int count = getPartitionCount();
    377         for (int i = count; --i >= 0; ) {
    378             Partition partition = getPartition(i);
    379             if (partition instanceof DirectoryPartition) {
    380                 long id = ((DirectoryPartition)partition).getDirectoryId();
    381                 if (!directoryIds.contains(id)) {
    382                     removePartition(i);
    383                 }
    384             }
    385         }
    386 
    387         invalidate();
    388         notifyDataSetChanged();
    389     }
    390 
    391     @Override
    392     public void changeCursor(int partitionIndex, Cursor cursor) {
    393         if (partitionIndex >= getPartitionCount()) {
    394             // There is no partition for this data
    395             return;
    396         }
    397 
    398         Partition partition = getPartition(partitionIndex);
    399         if (partition instanceof DirectoryPartition) {
    400             ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED);
    401         }
    402 
    403         if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
    404             mPhotoLoader.refreshCache();
    405         }
    406 
    407         super.changeCursor(partitionIndex, cursor);
    408 
    409         if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
    410             updateIndexer(cursor);
    411         }
    412     }
    413 
    414     public void changeCursor(Cursor cursor) {
    415         changeCursor(0, cursor);
    416     }
    417 
    418     /**
    419      * Updates the indexer, which is used to produce section headers.
    420      */
    421     private void updateIndexer(Cursor cursor) {
    422         if (cursor == null) {
    423             setIndexer(null);
    424             return;
    425         }
    426 
    427         Bundle bundle = cursor.getExtras();
    428         if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
    429             String sections[] =
    430                     bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
    431             int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
    432             setIndexer(new ContactsSectionIndexer(sections, counts));
    433         } else {
    434             setIndexer(null);
    435         }
    436     }
    437 
    438     @Override
    439     public int getViewTypeCount() {
    440         // We need a separate view type for each item type, plus another one for
    441         // each type with header, plus one for "other".
    442         return getItemViewTypeCount() * 2 + 1;
    443     }
    444 
    445     @Override
    446     public int getItemViewType(int partitionIndex, int position) {
    447         int type = super.getItemViewType(partitionIndex, position);
    448         if (!isUserProfile(position)
    449                 && isSectionHeaderDisplayEnabled()
    450                 && partitionIndex == getIndexedPartition()) {
    451             Placement placement = getItemPlacementInSection(position);
    452             return placement.firstInSection ? type : getItemViewTypeCount() + type;
    453         } else {
    454             return type;
    455         }
    456     }
    457 
    458     @Override
    459     public boolean isEmpty() {
    460         // TODO
    461 //        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
    462 //            return true;
    463 //        }
    464 
    465         if (!mEmptyListEnabled) {
    466             return false;
    467         } else if (isSearchMode()) {
    468             return TextUtils.isEmpty(getQueryString());
    469         } else {
    470             return super.isEmpty();
    471         }
    472     }
    473 
    474     public boolean isLoading() {
    475         int count = getPartitionCount();
    476         for (int i = 0; i < count; i++) {
    477             Partition partition = getPartition(i);
    478             if (partition instanceof DirectoryPartition
    479                     && ((DirectoryPartition) partition).isLoading()) {
    480                 return true;
    481             }
    482         }
    483         return false;
    484     }
    485 
    486     public boolean areAllPartitionsEmpty() {
    487         int count = getPartitionCount();
    488         for (int i = 0; i < count; i++) {
    489             if (!isPartitionEmpty(i)) {
    490                 return false;
    491             }
    492         }
    493         return true;
    494     }
    495 
    496     /**
    497      * Changes visibility parameters for the default directory partition.
    498      */
    499     public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
    500         int defaultPartitionIndex = -1;
    501         int count = getPartitionCount();
    502         for (int i = 0; i < count; i++) {
    503             Partition partition = getPartition(i);
    504             if (partition instanceof DirectoryPartition &&
    505                     ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) {
    506                 defaultPartitionIndex = i;
    507                 break;
    508             }
    509         }
    510         if (defaultPartitionIndex != -1) {
    511             setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
    512             setHasHeader(defaultPartitionIndex, hasHeader);
    513         }
    514     }
    515 
    516     @Override
    517     protected View newHeaderView(Context context, int partition, Cursor cursor,
    518             ViewGroup parent) {
    519         LayoutInflater inflater = LayoutInflater.from(context);
    520         return inflater.inflate(R.layout.directory_header, parent, false);
    521     }
    522 
    523     @Override
    524     protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
    525         Partition partition = getPartition(partitionIndex);
    526         if (!(partition instanceof DirectoryPartition)) {
    527             return;
    528         }
    529 
    530         DirectoryPartition directoryPartition = (DirectoryPartition)partition;
    531         long directoryId = directoryPartition.getDirectoryId();
    532         TextView labelTextView = (TextView)view.findViewById(R.id.label);
    533         TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name);
    534         if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
    535             labelTextView.setText(mDefaultFilterHeaderText);
    536             displayNameTextView.setText(null);
    537         } else {
    538             labelTextView.setText(R.string.directory_search_label);
    539             String directoryName = directoryPartition.getDisplayName();
    540             String displayName = !TextUtils.isEmpty(directoryName)
    541                     ? directoryName
    542                     : directoryPartition.getDirectoryType();
    543             displayNameTextView.setText(displayName);
    544         }
    545 
    546         TextView countText = (TextView)view.findViewById(R.id.count);
    547         if (directoryPartition.isLoading()) {
    548             countText.setText(R.string.search_results_searching);
    549         } else {
    550             int count = cursor == null ? 0 : cursor.getCount();
    551             if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE
    552                     && count >= getDirectoryResultLimit()) {
    553                 countText.setText(mContext.getString(
    554                         R.string.foundTooManyContacts, getDirectoryResultLimit()));
    555             } else {
    556                 countText.setText(getQuantityText(
    557                         count, R.string.listFoundAllContactsZero, R.plurals.searchFoundContacts));
    558             }
    559         }
    560     }
    561 
    562     /**
    563      * Checks whether the contact entry at the given position represents the user's profile.
    564      */
    565     protected boolean isUserProfile(int position) {
    566         // The profile only ever appears in the first position if it is present.  So if the position
    567         // is anything beyond 0, it can't be the profile.
    568         boolean isUserProfile = false;
    569         if (position == 0) {
    570             int partition = getPartitionForPosition(position);
    571             if (partition >= 0) {
    572                 // Save the old cursor position - the call to getItem() may modify the cursor
    573                 // position.
    574                 int offset = getCursor(partition).getPosition();
    575                 Cursor cursor = (Cursor) getItem(position);
    576                 if (cursor != null) {
    577                     int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE);
    578                     if (profileColumnIndex != -1) {
    579                         isUserProfile = cursor.getInt(profileColumnIndex) == 1;
    580                     }
    581                     // Restore the old cursor position.
    582                     cursor.moveToPosition(offset);
    583                 }
    584             }
    585         }
    586         return isUserProfile;
    587     }
    588 
    589     // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
    590     public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
    591         if (count == 0) {
    592             return getContext().getString(zeroResourceId);
    593         } else {
    594             String format = getContext().getResources()
    595                     .getQuantityText(pluralResourceId, count).toString();
    596             return String.format(format, count);
    597         }
    598     }
    599 
    600     public boolean isPhotoSupported(int partitionIndex) {
    601         Partition partition = getPartition(partitionIndex);
    602         if (partition instanceof DirectoryPartition) {
    603             return ((DirectoryPartition) partition).isPhotoSupported();
    604         }
    605         return true;
    606     }
    607 
    608     /**
    609      * Returns the currently selected filter.
    610      */
    611     public ContactListFilter getFilter() {
    612         return mFilter;
    613     }
    614 
    615     public void setFilter(ContactListFilter filter) {
    616         mFilter = filter;
    617     }
    618 
    619     // TODO: move sharable logic (bindXX() methods) to here with extra arguments
    620 
    621     /**
    622      * Loads the photo for the quick contact view and assigns the contact uri.
    623      * @param photoIdColumn Index of the photo id column
    624      * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
    625      * @param contactIdColumn Index of the contact id column
    626      * @param lookUpKeyColumn Index of the lookup key column
    627      */
    628     protected void bindQuickContact(final ContactListItemView view, int partitionIndex,
    629             Cursor cursor, int photoIdColumn, int photoUriColumn, int contactIdColumn,
    630             int lookUpKeyColumn) {
    631         long photoId = 0;
    632         if (!cursor.isNull(photoIdColumn)) {
    633             photoId = cursor.getLong(photoIdColumn);
    634         }
    635 
    636         QuickContactBadge quickContact = view.getQuickContact();
    637         quickContact.assignContactUri(
    638                 getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
    639 
    640         if (photoId != 0 || photoUriColumn == -1) {
    641             getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme);
    642         } else {
    643             final String photoUriString = cursor.getString(photoUriColumn);
    644             final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
    645             getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme);
    646         }
    647 
    648     }
    649 
    650     protected Uri getContactUri(int partitionIndex, Cursor cursor,
    651             int contactIdColumn, int lookUpKeyColumn) {
    652         long contactId = cursor.getLong(contactIdColumn);
    653         String lookupKey = cursor.getString(lookUpKeyColumn);
    654         Uri uri = Contacts.getLookupUri(contactId, lookupKey);
    655         long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
    656         if (directoryId != Directory.DEFAULT) {
    657             uri = uri.buildUpon().appendQueryParameter(
    658                     ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
    659         }
    660         return uri;
    661     }
    662 
    663     public void setContactsCount(String count) {
    664         mContactsCount = count;
    665     }
    666 
    667     public String getContactsCount() {
    668         return mContactsCount;
    669     }
    670 }
    671