Initial checkin of AOSP Messaging app.

b/23110861

Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
This commit is contained in:
Mike Dodd
2015-08-12 12:47:26 -07:00
parent ef8c7abbcf
commit d3b009ae55
1645 changed files with 186271 additions and 0 deletions
@@ -0,0 +1,286 @@
/*
* 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.ui.contact;
import android.content.Context;
import android.database.Cursor;
import android.database.MergeCursor;
import android.support.v4.util.Pair;
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.widget.Filter;
import com.android.ex.chips.BaseRecipientAdapter;
import com.android.ex.chips.RecipientAlternatesAdapter;
import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
import com.android.ex.chips.RecipientEntry;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.ContactRecipientEntryUtils;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.PhoneUtils;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* An extension on the base {@link BaseRecipientAdapter} that uses data layer from Bugle,
* such as the ContactRecipientPhotoManager that uses our own MediaResourceManager, and
* contact lookup that relies on ContactUtil. It provides data source and filtering ability
* for {@link ContactRecipientAutoCompleteView}
*/
public final class ContactRecipientAdapter extends BaseRecipientAdapter {
public ContactRecipientAdapter(final Context context,
final ContactListItemView.HostInterface clivHost) {
this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost);
}
public ContactRecipientAdapter(final Context context, final int preferredMaxResultCount,
final int queryMode, final ContactListItemView.HostInterface clivHost) {
super(context, preferredMaxResultCount, queryMode);
setPhotoManager(new ContactRecipientPhotoManager(context, clivHost));
}
@Override
public boolean forceShowAddress() {
// We should always use the SingleRecipientAddressAdapter
// And never use the RecipientAlternatesAdapter
return true;
}
@Override
public Filter getFilter() {
return new ContactFilter();
}
/**
* A Filter for a RecipientEditTextView that queries Bugle's ContactUtil for auto-complete
* results.
*/
public class ContactFilter extends Filter {
// Used to sort filtered contacts when it has combined results from email and phone.
private final RecipientEntryComparator mComparator = new RecipientEntryComparator();
/**
* Returns a cursor containing the filtered results in contacts given the search text,
* and a boolean indicating whether the results are sorted.
*
* The queries are synchronously performed since this is not run on the main thread.
*
* Some locales (e.g. JPN) expect email addresses to be auto-completed for MMS.
* If this is the case, perform two queries on phone number followed by email and
* return the merged results.
*/
@DoesNotRunOnMainThread
private Pair<Cursor, Boolean> getFilteredResultsCursor(final Context context,
final String searchText) {
Assert.isNotMainThread();
if (BugleGservices.get().getBoolean(
BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS,
BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) {
return Pair.create((Cursor) new MergeCursor(new Cursor[] {
ContactUtil.filterPhones(getContext(), searchText)
.performSynchronousQuery(),
ContactUtil.filterEmails(getContext(), searchText)
.performSynchronousQuery()
}), false /* the merged cursor is not sorted */);
} else {
return Pair.create(ContactUtil.filterDestination(getContext(), searchText)
.performSynchronousQuery(), true);
}
}
@Override
protected FilterResults performFiltering(final CharSequence constraint) {
Assert.isNotMainThread();
final FilterResults results = new FilterResults();
// No query, return empty results.
if (TextUtils.isEmpty(constraint)) {
clearTempEntries();
return results;
}
final String searchText = constraint.toString();
// Query for auto-complete results, since performFiltering() is not done on the
// main thread, perform the cursor loader queries directly.
final Pair<Cursor, Boolean> filteredResults = getFilteredResultsCursor(getContext(),
searchText);
final Cursor cursor = filteredResults.first;
final boolean sorted = filteredResults.second;
if (cursor != null) {
try {
final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
// First check if the constraint is a valid SMS destination. If so, add the
// destination as a suggestion item to the drop down.
if (PhoneUtils.isValidSmsMmsDestination(searchText)) {
entries.add(ContactRecipientEntryUtils
.constructSendToDestinationEntry(searchText));
}
HashSet<Long> existingContactIds = new HashSet<Long>();
while (cursor.moveToNext()) {
// Make sure there's only one first-level contact (i.e. contact for which
// we show the avatar picture and name) for every contact id.
final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
final boolean isFirstLevel = !existingContactIds.contains(contactId);
if (isFirstLevel) {
existingContactIds.add(contactId);
}
entries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor,
isFirstLevel));
}
if (!sorted) {
Collections.sort(entries, mComparator);
}
results.values = entries;
results.count = 1;
} finally {
cursor.close();
}
}
return results;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
mCurrentConstraint = constraint;
clearTempEntries();
if (results.values != null) {
@SuppressWarnings("unchecked")
final List<RecipientEntry> entries = (List<RecipientEntry>) results.values;
updateEntries(entries);
} else {
updateEntries(Collections.<RecipientEntry>emptyList());
}
}
private class RecipientEntryComparator implements Comparator<RecipientEntry> {
private final Collator mCollator;
public RecipientEntryComparator() {
mCollator = Collator.getInstance(Locale.getDefault());
mCollator.setStrength(Collator.PRIMARY);
}
/**
* Compare two RecipientEntry's, first by locale-aware display name comparison, then by
* contact id comparison, finally by first-level-ness comparison.
*/
@Override
public int compare(RecipientEntry lhs, RecipientEntry rhs) {
// Send-to-destinations always appear before everything else.
final boolean sendToLhs = ContactRecipientEntryUtils
.isSendToDestinationContact(lhs);
final boolean sendToRhs = ContactRecipientEntryUtils
.isSendToDestinationContact(lhs);
if (sendToLhs != sendToRhs) {
if (sendToLhs) {
return -1;
} else if (sendToRhs) {
return 1;
}
}
final int displayNameCompare = mCollator.compare(lhs.getDisplayName(),
rhs.getDisplayName());
if (displayNameCompare != 0) {
return displayNameCompare;
}
// Long.compare could accomplish the following three lines, but this is only
// available in API 19+
final long lhsContactId = lhs.getContactId();
final long rhsContactId = rhs.getContactId();
final int contactCompare = lhsContactId < rhsContactId ? -1 :
(lhsContactId == rhsContactId ? 0 : 1);
if (contactCompare != 0) {
return contactCompare;
}
// These are the same contact. Make sure first-level contacts always
// appear at the front.
if (lhs.isFirstLevel()) {
return -1;
} else if (rhs.isFirstLevel()) {
return 1;
} else {
return 0;
}
}
}
}
/**
* Called when we need to substitute temporary recipient chips with better alternatives.
* For example, if a list of comma-delimited phone numbers are pasted into the edit box,
* we want to be able to look up in the ContactUtil for exact matches and get contact
* details such as name and photo thumbnail for the contact to display a better chip.
*/
@Override
public void getMatchingRecipients(final ArrayList<String> inAddresses,
final RecipientMatchCallback callback) {
final int addressesSize = Math.min(
RecipientAlternatesAdapter.MAX_LOOKUPS, inAddresses.size());
final HashSet<String> addresses = new HashSet<String>();
for (int i = 0; i < addressesSize; i++) {
final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase());
addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i));
}
final Map<String, RecipientEntry> recipientEntries =
new HashMap<String, RecipientEntry>();
// query for each address
for (final String address : addresses) {
final Cursor cursor = ContactUtil.lookupDestination(getContext(), address)
.performSynchronousQuery();
if (cursor != null) {
try {
if (cursor.moveToNext()) {
// There may be multiple matches to the same number, always take the
// first match.
// TODO: May need to consider if there's an existing conversation
// that matches this particular contact and prioritize that contact.
final RecipientEntry entry =
ContactUtil.createRecipientEntryForPhoneQuery(cursor, true);
recipientEntries.put(address, entry);
}
} finally {
cursor.close();
}
}
}
// report matches
callback.matchesFound(recipientEntries);
}
}