Test: Check log. Change-Id: I488fc6c4288bc3843a2be35a8e2988738441263e Signed-off-by: Taesu Lee <taesu82.lee@samsung.com>
298 lines
13 KiB
Java
298 lines
13 KiB
Java
/*
|
|
* 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.graphics.Rect;
|
|
import android.os.AsyncTask;
|
|
import androidx.appcompat.R;
|
|
import android.text.Editable;
|
|
import android.text.TextPaint;
|
|
import android.text.TextWatcher;
|
|
import android.text.util.Rfc822Tokenizer;
|
|
import android.util.AttributeSet;
|
|
import android.view.ContextThemeWrapper;
|
|
import android.view.KeyEvent;
|
|
import android.view.inputmethod.EditorInfo;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.ex.chips.RecipientEditTextView;
|
|
import com.android.ex.chips.RecipientEntry;
|
|
import com.android.ex.chips.recipientchip.DrawableRecipientChip;
|
|
import com.android.messaging.datamodel.data.ParticipantData;
|
|
import com.android.messaging.util.ContactRecipientEntryUtils;
|
|
import com.android.messaging.util.ContactUtil;
|
|
import com.android.messaging.util.PhoneUtils;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.Executors;
|
|
|
|
/**
|
|
* An extension for {@link RecipientEditTextView} which shows a list of Materialized contact chips.
|
|
* It uses Bugle's ContactUtil to perform contact lookup, and is able to return the list of
|
|
* recipients in the form of a ParticipantData list.
|
|
*/
|
|
public class ContactRecipientAutoCompleteView extends RecipientEditTextView {
|
|
public interface ContactChipsChangeListener {
|
|
void onContactChipsChanged(int oldCount, int newCount);
|
|
void onInvalidContactChipsPruned(int prunedCount);
|
|
void onEntryComplete();
|
|
}
|
|
|
|
private final int mTextHeight;
|
|
private ContactChipsChangeListener mChipsChangeListener;
|
|
|
|
/**
|
|
* Watches changes in contact chips to determine possible state transitions.
|
|
*/
|
|
private class ContactChipsWatcher implements TextWatcher {
|
|
/**
|
|
* Tracks the old chips count before text changes. Note that we currently don't compare
|
|
* the entire chip sets but just the cheaper-to-do before and after counts, because
|
|
* the chips view don't allow for replacing chips.
|
|
*/
|
|
private int mLastChipsCount = 0;
|
|
|
|
@Override
|
|
public void onTextChanged(final CharSequence s, final int start, final int before,
|
|
final int count) {
|
|
}
|
|
|
|
@Override
|
|
public void beforeTextChanged(final CharSequence s, final int start, final int count,
|
|
final int after) {
|
|
// We don't take mLastChipsCount from here but from the last afterTextChanged() run.
|
|
// The reason is because at this point, any chip spans to be removed is already removed
|
|
// from s in the chips text view.
|
|
}
|
|
|
|
@Override
|
|
public void afterTextChanged(final Editable s) {
|
|
final int currentChipsCount = s.getSpans(0, s.length(),
|
|
DrawableRecipientChip.class).length;
|
|
if (currentChipsCount != mLastChipsCount) {
|
|
// When a sanitizing task is running, we don't want to notify any chips count
|
|
// change, but we do want to track the last chip count.
|
|
if (mChipsChangeListener != null && mCurrentSanitizeTask == null) {
|
|
mChipsChangeListener.onContactChipsChanged(mLastChipsCount, currentChipsCount);
|
|
}
|
|
mLastChipsCount = currentChipsCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static final String TEXT_HEIGHT_SAMPLE = "a";
|
|
|
|
public ContactRecipientAutoCompleteView(final Context context, final AttributeSet attrs) {
|
|
super(new ContextThemeWrapper(context, R.style.ColorAccentGrayOverrideStyle), attrs);
|
|
|
|
// Get the height of the text, given the currently set font face and size.
|
|
final Rect textBounds = new Rect(0, 0, 0, 0);
|
|
final TextPaint paint = getPaint();
|
|
paint.getTextBounds(TEXT_HEIGHT_SAMPLE, 0, TEXT_HEIGHT_SAMPLE.length(), textBounds);
|
|
mTextHeight = textBounds.height();
|
|
|
|
setTokenizer(new Rfc822Tokenizer());
|
|
addTextChangedListener(new ContactChipsWatcher());
|
|
setOnFocusListShrinkRecipients(false);
|
|
|
|
setBackground(context.getResources().getDrawable(
|
|
R.drawable.abc_textfield_search_default_mtrl_alpha));
|
|
}
|
|
|
|
public void setContactChipsListener(final ContactChipsChangeListener listener) {
|
|
mChipsChangeListener = listener;
|
|
}
|
|
|
|
/**
|
|
* A tuple of chips which AsyncContactChipSanitizeTask reports as progress to have the
|
|
* chip actually replaced/removed on the UI thread.
|
|
*/
|
|
private class ChipReplacementTuple {
|
|
public final DrawableRecipientChip removedChip;
|
|
public final RecipientEntry replacedChipEntry;
|
|
|
|
public ChipReplacementTuple(final DrawableRecipientChip removedChip,
|
|
final RecipientEntry replacedChipEntry) {
|
|
this.removedChip = removedChip;
|
|
this.replacedChipEntry = replacedChipEntry;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An AsyncTask that cleans up contact chips on every chips commit (i.e. get or create a new
|
|
* conversation with the given chips).
|
|
*/
|
|
private class AsyncContactChipSanitizeTask extends
|
|
AsyncTask<Void, ChipReplacementTuple, Integer> {
|
|
|
|
@Override
|
|
protected Integer doInBackground(final Void... params) {
|
|
final DrawableRecipientChip[] recips = getText()
|
|
.getSpans(0, getText().length(), DrawableRecipientChip.class);
|
|
int invalidChipsRemoved = 0;
|
|
for (final DrawableRecipientChip recipient : recips) {
|
|
final RecipientEntry entry = recipient.getEntry();
|
|
if (entry != null) {
|
|
if (entry.isValid()) {
|
|
if (RecipientEntry.isCreatedRecipient(entry.getContactId()) ||
|
|
ContactRecipientEntryUtils.isSendToDestinationContact(entry)) {
|
|
// This is a generated/send-to contact chip, try to look it up and
|
|
// display a chip for the corresponding local contact.
|
|
try (final Cursor lookupResult =
|
|
ContactUtil.lookupDestination(
|
|
getContext(), entry.getDestination())
|
|
.performSynchronousQuery()) {
|
|
if (lookupResult != null && lookupResult.moveToNext()) {
|
|
// Found a match, remove the generated entry and replace with a
|
|
// better local entry.
|
|
publishProgress(
|
|
new ChipReplacementTuple(
|
|
recipient,
|
|
ContactUtil.createRecipientEntryForPhoneQuery(
|
|
lookupResult, true)));
|
|
} else if (PhoneUtils.isValidSmsMmsDestination(
|
|
entry.getDestination())) {
|
|
// No match was found, but we have a valid destination so let's
|
|
// at least create an entry that shows an avatar.
|
|
publishProgress(
|
|
new ChipReplacementTuple(
|
|
recipient,
|
|
ContactRecipientEntryUtils
|
|
.constructNumberWithAvatarEntry(
|
|
entry.getDestination())));
|
|
} else {
|
|
// Not a valid contact. Remove and show an error.
|
|
publishProgress(new ChipReplacementTuple(recipient, null));
|
|
invalidChipsRemoved++;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
publishProgress(new ChipReplacementTuple(recipient, null));
|
|
invalidChipsRemoved++;
|
|
}
|
|
}
|
|
}
|
|
return invalidChipsRemoved;
|
|
}
|
|
|
|
@Override
|
|
protected void onProgressUpdate(final ChipReplacementTuple... values) {
|
|
for (final ChipReplacementTuple tuple : values) {
|
|
if (tuple.removedChip != null) {
|
|
final Editable text = getText();
|
|
final int chipStart = text.getSpanStart(tuple.removedChip);
|
|
final int chipEnd = text.getSpanEnd(tuple.removedChip);
|
|
if (chipStart >= 0 && chipEnd >= 0) {
|
|
text.delete(chipStart, chipEnd);
|
|
}
|
|
|
|
if (tuple.replacedChipEntry != null) {
|
|
appendRecipientEntry(tuple.replacedChipEntry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(final Integer invalidChipsRemoved) {
|
|
mCurrentSanitizeTask = null;
|
|
if (invalidChipsRemoved > 0) {
|
|
mChipsChangeListener.onInvalidContactChipsPruned(invalidChipsRemoved);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* We don't use SafeAsyncTask but instead use a single threaded executor to ensure that
|
|
* all sanitization tasks are serially executed so as not to interfere with each other.
|
|
*/
|
|
private static final Executor SANITIZE_EXECUTOR = Executors.newSingleThreadExecutor();
|
|
|
|
private AsyncContactChipSanitizeTask mCurrentSanitizeTask;
|
|
|
|
/**
|
|
* Whenever the caller wants to start a new conversation with the list of chips we have,
|
|
* make sure we asynchronously:
|
|
* 1. Remove invalid chips.
|
|
* 2. Attempt to resolve unknown contacts to known local contacts.
|
|
* 3. Convert still unknown chips to chips with generated avatar.
|
|
*
|
|
* Note that we don't need to perform this synchronously since we can
|
|
* resolve any unknown contacts to local contacts when needed.
|
|
*/
|
|
private void sanitizeContactChips() {
|
|
if (mCurrentSanitizeTask != null && !mCurrentSanitizeTask.isCancelled()) {
|
|
mCurrentSanitizeTask.cancel(false);
|
|
mCurrentSanitizeTask = null;
|
|
}
|
|
mCurrentSanitizeTask = new AsyncContactChipSanitizeTask();
|
|
mCurrentSanitizeTask.executeOnExecutor(SANITIZE_EXECUTOR);
|
|
}
|
|
|
|
/**
|
|
* Returns a list of ParticipantData from the entered chips in order to create
|
|
* new conversation.
|
|
*/
|
|
public ArrayList<ParticipantData> getRecipientParticipantDataForConversationCreation() {
|
|
final DrawableRecipientChip[] recips = getText()
|
|
.getSpans(0, getText().length(), DrawableRecipientChip.class);
|
|
final ArrayList<ParticipantData> contacts =
|
|
new ArrayList<ParticipantData>(recips.length);
|
|
for (final DrawableRecipientChip recipient : recips) {
|
|
final RecipientEntry entry = recipient.getEntry();
|
|
if (entry != null && entry.isValid() && entry.getDestination() != null &&
|
|
PhoneUtils.isValidSmsMmsDestination(entry.getDestination())) {
|
|
contacts.add(ParticipantData.getFromRecipientEntry(recipient.getEntry()));
|
|
}
|
|
}
|
|
sanitizeContactChips();
|
|
return contacts;
|
|
}
|
|
|
|
/**c
|
|
* Gets a set of currently selected chips' emails/phone numbers. This will facilitate the
|
|
* consumer with determining quickly whether a contact is currently selected.
|
|
*/
|
|
public Set<String> getSelectedDestinations() {
|
|
Set<String> set = new HashSet<String>();
|
|
final DrawableRecipientChip[] recips = getText()
|
|
.getSpans(0, getText().length(), DrawableRecipientChip.class);
|
|
|
|
for (final DrawableRecipientChip recipient : recips) {
|
|
final RecipientEntry entry = recipient.getEntry();
|
|
if (entry != null && entry.isValid() && entry.getDestination() != null) {
|
|
set.add(PhoneUtils.getDefault().getCanonicalBySystemLocale(
|
|
entry.getDestination()));
|
|
}
|
|
}
|
|
return set;
|
|
}
|
|
|
|
@Override
|
|
public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) {
|
|
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
|
mChipsChangeListener.onEntryComplete();
|
|
}
|
|
return super.onEditorAction(view, actionId, event);
|
|
}
|
|
}
|