Home | History | Annotate | Download | only in datamodel
      1 /*
      2  * Copyright (C) 2015 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.messaging.datamodel;
     17 
     18 import android.database.Cursor;
     19 import android.database.MatrixCursor;
     20 import android.provider.ContactsContract.CommonDataKinds.Phone;
     21 import android.support.v4.util.SimpleArrayMap;
     22 
     23 import com.android.messaging.util.Assert;
     24 import com.android.messaging.util.ContactUtil;
     25 
     26 import java.util.ArrayList;
     27 import java.util.Collections;
     28 import java.util.Comparator;
     29 
     30 /**
     31  * A cursor builder that takes the frequent contacts cursor and aggregate it with the all contacts
     32  * cursor to fill in contact details such as phone numbers and strip away invalid contacts.
     33  *
     34  * Because the frequent contact list depends on the loading of two cursors, it needs to temporarily
     35  * store the cursor that it receives with setFrequents() and setAllContacts() calls. Because it
     36  * doesn't know which one will be finished first, it always checks whether both cursors are ready
     37  * to pull data from and construct the aggregate cursor when it's ready to do so. Note that
     38  * this cursor builder doesn't assume ownership of the cursors passed in - it merely references
     39  * them and always does a isClosed() check before consuming them. The ownership still belongs to
     40  * the loader framework and the cursor may be closed when the UI is torn down.
     41  */
     42 public class FrequentContactsCursorBuilder {
     43     private Cursor mAllContactsCursor;
     44     private Cursor mFrequentContactsCursor;
     45 
     46     /**
     47      * Sets the frequent contacts cursor as soon as it is loaded, or null if it's reset.
     48      * @return this builder instance for chained operations
     49      */
     50     public FrequentContactsCursorBuilder setFrequents(final Cursor frequentContactsCursor) {
     51         mFrequentContactsCursor = frequentContactsCursor;
     52         return this;
     53     }
     54 
     55     /**
     56      * Sets the all contacts cursor as soon as it is loaded, or null if it's reset.
     57      * @return this builder instance for chained operations
     58      */
     59     public FrequentContactsCursorBuilder setAllContacts(final Cursor allContactsCursor) {
     60         mAllContactsCursor = allContactsCursor;
     61         return this;
     62     }
     63 
     64     /**
     65      * Reset this builder. Must be called when the consumer resets its data.
     66      */
     67     public void resetBuilder() {
     68         mAllContactsCursor = null;
     69         mFrequentContactsCursor = null;
     70     }
     71 
     72     /**
     73      * Attempt to build the cursor records from the frequent and all contacts cursor if they
     74      * are both ready to be consumed.
     75      * @return the frequent contact cursor if built successfully, or null if it can't be built yet.
     76      */
     77     public Cursor build() {
     78         if (mFrequentContactsCursor != null && mAllContactsCursor != null) {
     79             Assert.isTrue(!mFrequentContactsCursor.isClosed());
     80             Assert.isTrue(!mAllContactsCursor.isClosed());
     81 
     82             // Frequent contacts cursor has one record per contact, plus it doesn't contain info
     83             // such as phone number and type. In order for the records to be usable by Bugle, we
     84             // would like to populate it with information from the all contacts cursor.
     85             final MatrixCursor retCursor = new MatrixCursor(ContactUtil.PhoneQuery.PROJECTION);
     86 
     87             // First, go through the frequents cursor and take note of all lookup keys and their
     88             // corresponding rank in the frequents list.
     89             final SimpleArrayMap<String, Integer> lookupKeyToRankMap =
     90                     new SimpleArrayMap<String, Integer>();
     91             int oldPosition = mFrequentContactsCursor.getPosition();
     92             int rank = 0;
     93             mFrequentContactsCursor.moveToPosition(-1);
     94             while (mFrequentContactsCursor.moveToNext()) {
     95                 final String lookupKey = mFrequentContactsCursor.getString(
     96                         ContactUtil.INDEX_LOOKUP_KEY_FREQUENT);
     97                 lookupKeyToRankMap.put(lookupKey, rank++);
     98             }
     99             mFrequentContactsCursor.moveToPosition(oldPosition);
    100 
    101             // Second, go through the all contacts cursor once and retrieve all information
    102             // (multiple phone numbers etc.) and store that in an array list. Since the all
    103             // contacts list only contains phone contacts, this step will ensure that we filter
    104             // out any invalid/email contacts in the frequents list.
    105             final ArrayList<Object[]> rows =
    106                     new ArrayList<Object[]>(mFrequentContactsCursor.getCount());
    107             oldPosition = mAllContactsCursor.getPosition();
    108             mAllContactsCursor.moveToPosition(-1);
    109             while (mAllContactsCursor.moveToNext()) {
    110                 final String lookupKey = mAllContactsCursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
    111                 if (lookupKeyToRankMap.containsKey(lookupKey)) {
    112                     final Object[] row = new Object[ContactUtil.PhoneQuery.PROJECTION.length];
    113                     row[ContactUtil.INDEX_DATA_ID] =
    114                             mAllContactsCursor.getLong(ContactUtil.INDEX_DATA_ID);
    115                     row[ContactUtil.INDEX_CONTACT_ID] =
    116                             mAllContactsCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
    117                     row[ContactUtil.INDEX_LOOKUP_KEY] =
    118                             mAllContactsCursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
    119                     row[ContactUtil.INDEX_DISPLAY_NAME] =
    120                             mAllContactsCursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
    121                     row[ContactUtil.INDEX_PHOTO_URI] =
    122                             mAllContactsCursor.getString(ContactUtil.INDEX_PHOTO_URI);
    123                     row[ContactUtil.INDEX_PHONE_EMAIL] =
    124                             mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL);
    125                     row[ContactUtil.INDEX_PHONE_EMAIL_TYPE] =
    126                             mAllContactsCursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
    127                     row[ContactUtil.INDEX_PHONE_EMAIL_LABEL] =
    128                             mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL);
    129                     rows.add(row);
    130                 }
    131             }
    132             mAllContactsCursor.moveToPosition(oldPosition);
    133 
    134             // Now we have a list of rows containing frequent contacts in alphabetical order.
    135             // Therefore, sort all the rows according to their actual ranks in the frequents list.
    136             Collections.sort(rows, new Comparator<Object[]>() {
    137                 @Override
    138                 public int compare(final Object[] lhs, final Object[] rhs) {
    139                     final String lookupKeyLhs = (String) lhs[ContactUtil.INDEX_LOOKUP_KEY];
    140                     final String lookupKeyRhs = (String) rhs[ContactUtil.INDEX_LOOKUP_KEY];
    141                     Assert.isTrue(lookupKeyToRankMap.containsKey(lookupKeyLhs) &&
    142                             lookupKeyToRankMap.containsKey(lookupKeyRhs));
    143                     final int rankLhs = lookupKeyToRankMap.get(lookupKeyLhs);
    144                     final int rankRhs = lookupKeyToRankMap.get(lookupKeyRhs);
    145                     if (rankLhs < rankRhs) {
    146                         return -1;
    147                     } else if (rankLhs > rankRhs) {
    148                         return 1;
    149                     } else {
    150                         // Same rank, so it's two contact records for the same contact.
    151                         // Perform secondary sorting on the phone type. Always place
    152                         // mobile before everything else.
    153                         final int phoneTypeLhs = (int) lhs[ContactUtil.INDEX_PHONE_EMAIL_TYPE];
    154                         final int phoneTypeRhs = (int) rhs[ContactUtil.INDEX_PHONE_EMAIL_TYPE];
    155                         if (phoneTypeLhs == Phone.TYPE_MOBILE &&
    156                                 phoneTypeRhs == Phone.TYPE_MOBILE) {
    157                             return 0;
    158                         } else if (phoneTypeLhs == Phone.TYPE_MOBILE) {
    159                             return -1;
    160                         } else if (phoneTypeRhs == Phone.TYPE_MOBILE) {
    161                             return 1;
    162                         } else {
    163                             // Use the default sort order, i.e. sort by phoneType value.
    164                             return phoneTypeLhs < phoneTypeRhs ? -1 :
    165                                     (phoneTypeLhs == phoneTypeRhs ? 0 : 1);
    166                         }
    167                     }
    168                 }
    169             });
    170 
    171             // Finally, add all the rows to this cursor.
    172             for (final Object[] row : rows) {
    173                 retCursor.addRow(row);
    174             }
    175             return retCursor;
    176         }
    177         return null;
    178     }
    179 }
    180