blob: 62483a07c01edb12e06a7873980817181c998688 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.datamodel;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.v4.util.SimpleArrayMap;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContactUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
/**
* A cursor builder that takes the frequent contacts cursor and aggregate it with the all contacts
* cursor to fill in contact details such as phone numbers and strip away invalid contacts.
*
* Because the frequent contact list depends on the loading of two cursors, it needs to temporarily
* store the cursor that it receives with setFrequents() and setAllContacts() calls. Because it
* doesn't know which one will be finished first, it always checks whether both cursors are ready
* to pull data from and construct the aggregate cursor when it's ready to do so. Note that
* this cursor builder doesn't assume ownership of the cursors passed in - it merely references
* them and always does a isClosed() check before consuming them. The ownership still belongs to
* the loader framework and the cursor may be closed when the UI is torn down.
*/
public class FrequentContactsCursorBuilder {
private Cursor mAllContactsCursor;
private Cursor mFrequentContactsCursor;
/**
* Sets the frequent contacts cursor as soon as it is loaded, or null if it's reset.
* @return this builder instance for chained operations
*/
public FrequentContactsCursorBuilder setFrequents(final Cursor frequentContactsCursor) {
mFrequentContactsCursor = frequentContactsCursor;
return this;
}
/**
* Sets the all contacts cursor as soon as it is loaded, or null if it's reset.
* @return this builder instance for chained operations
*/
public FrequentContactsCursorBuilder setAllContacts(final Cursor allContactsCursor) {
mAllContactsCursor = allContactsCursor;
return this;
}
/**
* Reset this builder. Must be called when the consumer resets its data.
*/
public void resetBuilder() {
mAllContactsCursor = null;
mFrequentContactsCursor = null;
}
/**
* Attempt to build the cursor records from the frequent and all contacts cursor if they
* are both ready to be consumed.
* @return the frequent contact cursor if built successfully, or null if it can't be built yet.
*/
public Cursor build() {
if (mFrequentContactsCursor != null && mAllContactsCursor != null) {
Assert.isTrue(!mFrequentContactsCursor.isClosed());
Assert.isTrue(!mAllContactsCursor.isClosed());
// Frequent contacts cursor has one record per contact, plus it doesn't contain info
// such as phone number and type. In order for the records to be usable by Bugle, we
// would like to populate it with information from the all contacts cursor.
final MatrixCursor retCursor = new MatrixCursor(ContactUtil.PhoneQuery.PROJECTION);
// First, go through the frequents cursor and take note of all lookup keys and their
// corresponding rank in the frequents list.
final SimpleArrayMap<String, Integer> lookupKeyToRankMap =
new SimpleArrayMap<String, Integer>();
int oldPosition = mFrequentContactsCursor.getPosition();
int rank = 0;
mFrequentContactsCursor.moveToPosition(-1);
while (mFrequentContactsCursor.moveToNext()) {
final String lookupKey = mFrequentContactsCursor.getString(
ContactUtil.INDEX_LOOKUP_KEY_FREQUENT);
lookupKeyToRankMap.put(lookupKey, rank++);
}
mFrequentContactsCursor.moveToPosition(oldPosition);
// Second, go through the all contacts cursor once and retrieve all information
// (multiple phone numbers etc.) and store that in an array list. Since the all
// contacts list only contains phone contacts, this step will ensure that we filter
// out any invalid/email contacts in the frequents list.
final ArrayList<Object[]> rows =
new ArrayList<Object[]>(mFrequentContactsCursor.getCount());
oldPosition = mAllContactsCursor.getPosition();
mAllContactsCursor.moveToPosition(-1);
while (mAllContactsCursor.moveToNext()) {
final String lookupKey = mAllContactsCursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
if (lookupKeyToRankMap.containsKey(lookupKey)) {
final Object[] row = new Object[ContactUtil.PhoneQuery.PROJECTION.length];
row[ContactUtil.INDEX_DATA_ID] =
mAllContactsCursor.getLong(ContactUtil.INDEX_DATA_ID);
row[ContactUtil.INDEX_CONTACT_ID] =
mAllContactsCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
row[ContactUtil.INDEX_LOOKUP_KEY] =
mAllContactsCursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
row[ContactUtil.INDEX_DISPLAY_NAME] =
mAllContactsCursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
row[ContactUtil.INDEX_PHOTO_URI] =
mAllContactsCursor.getString(ContactUtil.INDEX_PHOTO_URI);
row[ContactUtil.INDEX_PHONE_EMAIL] =
mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL);
row[ContactUtil.INDEX_PHONE_EMAIL_TYPE] =
mAllContactsCursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
row[ContactUtil.INDEX_PHONE_EMAIL_LABEL] =
mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL);
rows.add(row);
}
}
mAllContactsCursor.moveToPosition(oldPosition);
// Now we have a list of rows containing frequent contacts in alphabetical order.
// Therefore, sort all the rows according to their actual ranks in the frequents list.
Collections.sort(rows, new Comparator<Object[]>() {
@Override
public int compare(final Object[] lhs, final Object[] rhs) {
final String lookupKeyLhs = (String) lhs[ContactUtil.INDEX_LOOKUP_KEY];
final String lookupKeyRhs = (String) rhs[ContactUtil.INDEX_LOOKUP_KEY];
Assert.isTrue(lookupKeyToRankMap.containsKey(lookupKeyLhs) &&
lookupKeyToRankMap.containsKey(lookupKeyRhs));
final int rankLhs = lookupKeyToRankMap.get(lookupKeyLhs);
final int rankRhs = lookupKeyToRankMap.get(lookupKeyRhs);
if (rankLhs < rankRhs) {
return -1;
} else if (rankLhs > rankRhs) {
return 1;
} else {
// Same rank, so it's two contact records for the same contact.
// Perform secondary sorting on the phone type. Always place
// mobile before everything else.
final int phoneTypeLhs = (int) lhs[ContactUtil.INDEX_PHONE_EMAIL_TYPE];
final int phoneTypeRhs = (int) rhs[ContactUtil.INDEX_PHONE_EMAIL_TYPE];
if (phoneTypeLhs == Phone.TYPE_MOBILE &&
phoneTypeRhs == Phone.TYPE_MOBILE) {
return 0;
} else if (phoneTypeLhs == Phone.TYPE_MOBILE) {
return -1;
} else if (phoneTypeRhs == Phone.TYPE_MOBILE) {
return 1;
} else {
// Use the default sort order, i.e. sort by phoneType value.
return phoneTypeLhs < phoneTypeRhs ? -1 :
(phoneTypeLhs == phoneTypeRhs ? 0 : 1);
}
}
}
});
// Finally, add all the rows to this cursor.
for (final Object[] row : rows) {
retCursor.addRow(row);
}
return retCursor;
}
return null;
}
}