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