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 androidx.collection.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 */ setFrequents(final Cursor frequentContactsCursor)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 */ setAllContacts(final Cursor allContactsCursor)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 */ resetBuilder()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 */ build()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