/** * Copyright (C) 2014 Open Whisper Systems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.securecomcode.text.contacts; import android.content.Context; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.provider.ContactsContract; import android.support.v4.widget.CursorAdapter; import android.text.Spannable; import android.text.SpannableString; import android.text.style.ForegroundColorSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.FilterQueryProvider; import android.widget.ImageView; import android.widget.TextView; import com.securecomcode.text.R; import com.securecomcode.text.util.BitmapUtil; import com.securecomcode.text.util.BitmapWorkerRunnable; import com.securecomcode.text.util.BitmapWorkerRunnable.AsyncDrawable; import com.securecomcode.text.util.TaggedFutureTask; import com.securecomcode.text.util.Util; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.FutureTask; import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; /** * List adapter to display all contacts and their related information * * @author Jake McGinty */ public class ContactSelectionListAdapter extends CursorAdapter implements StickyListHeadersAdapter { private final static String TAG = "ContactListAdapter"; private final static ExecutorService photoResolver = Util.newSingleThreadedLifoExecutor(); private final static int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user, R.attr.contact_selection_lay_user, R.attr.contact_selection_label_text}; private int TYPE_COLUMN = -1; private int NAME_COLUMN = -1; private int NUMBER_COLUMN = -1; private int NUMBER_TYPE_COLUMN = -1; private int LABEL_COLUMN = -1; private int ID_COLUMN = -1; private final Context context; private final boolean multiSelect; private final LayoutInflater li; private final TypedArray drawables; private final Bitmap defaultPhoto; private final Bitmap defaultCroppedPhoto; private final int scaledPhotoSize; private final HashMap<Long, ContactAccessor.ContactData> selectedContacts = new HashMap<Long, ContactAccessor.ContactData>(); public ContactSelectionListAdapter(Context context, Cursor cursor, boolean multiSelect) { super(context, cursor, 0); this.context = context; this.li = LayoutInflater.from(context); this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES); this.multiSelect = multiSelect; this.defaultPhoto = ContactPhotoFactory.getDefaultContactPhoto(context); this.scaledPhotoSize = context.getResources().getDimensionPixelSize(R.dimen.contact_selection_photo_size); this.defaultCroppedPhoto = BitmapUtil.getScaledCircleCroppedBitmap(defaultPhoto, scaledPhotoSize); } public static class ViewHolder { public CheckBox checkBox; public TextView name; public TextView number; public ImageView contactPhoto; public int position; } public static class DataHolder { public int type; public String name; public String number; public int numberType; public String label; public long id; } public static class HeaderViewHolder { TextView text; } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { final View v = li.inflate(R.layout.push_contact_selection_list_item, parent, false); final ViewHolder holder = new ViewHolder(); if (v != null) { holder.name = (TextView) v.findViewById(R.id.name); holder.number = (TextView) v.findViewById(R.id.number); holder.checkBox = (CheckBox) v.findViewById(R.id.check_box); holder.contactPhoto = (ImageView) v.findViewById(R.id.contact_photo_image); if (!multiSelect) holder.checkBox.setVisibility(View.GONE); v.setTag(R.id.holder_tag, holder); v.setTag(R.id.contact_info_tag, new DataHolder()); } return v; } @Override public void bindView(View view, Context context, Cursor cursor) { final DataHolder contactData = (DataHolder) view.getTag(R.id.contact_info_tag); final ViewHolder holder = (ViewHolder) view.getTag(R.id.holder_tag); if (holder == null) { Log.w(TAG, "ViewHolder was null. This should not happen."); return; } if (contactData == null) { Log.w(TAG, "DataHolder was null. This should not happen."); return; } if (ID_COLUMN < 0) { populateColumnIndices(cursor); } contactData.type = cursor.getInt(TYPE_COLUMN); contactData.name = cursor.getString(NAME_COLUMN); contactData.number = cursor.getString(NUMBER_COLUMN); contactData.numberType = cursor.getInt(NUMBER_TYPE_COLUMN); contactData.label = cursor.getString(LABEL_COLUMN); contactData.id = cursor.getLong(ID_COLUMN); if (contactData.type != ContactsDatabase.PUSH_TYPE) { holder.name.setTextColor(drawables.getColor(1, 0xff000000)); holder.number.setTextColor(drawables.getColor(1, 0xff000000)); } else { holder.name.setTextColor(drawables.getColor(0, 0xa0000000)); holder.number.setTextColor(drawables.getColor(0, 0xa0000000)); } if (selectedContacts.containsKey(contactData.id)) { holder.checkBox.setChecked(true); } else { holder.checkBox.setChecked(false); } holder.name.setText(contactData.name); if (contactData.number == null || contactData.number.isEmpty()) { holder.name.setEnabled(false); holder.number.setText(""); } else if (contactData.type == ContactsDatabase.PUSH_TYPE) { holder.number.setText(contactData.number); } else { final CharSequence label = ContactsContract.CommonDataKinds.Phone.getTypeLabel(context.getResources(), contactData.numberType, contactData.label); final CharSequence numberWithLabel = contactData.number + " " + label; final Spannable numberLabelSpan = new SpannableString(numberWithLabel); numberLabelSpan.setSpan(new ForegroundColorSpan(drawables.getColor(2, 0xff444444)), contactData.number.length(), numberWithLabel.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); holder.number.setText(numberLabelSpan); } holder.contactPhoto.setImageBitmap(defaultCroppedPhoto); if (contactData.id > -1) loadBitmap(contactData.number, holder.contactPhoto); } @Override public View getHeaderView(int i, View convertView, ViewGroup viewGroup) { final Cursor c = getCursor(); final HeaderViewHolder holder; if (convertView == null) { holder = new HeaderViewHolder(); convertView = li.inflate(R.layout.push_contact_selection_list_header, viewGroup, false); holder.text = (TextView) convertView.findViewById(R.id.text); convertView.setTag(holder); } else { holder = (HeaderViewHolder) convertView.getTag(); } c.moveToPosition(i); final int type = c.getInt(c.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN)); final int headerTextRes; switch (type) { case 1: headerTextRes = R.string.contact_selection_list__header_textsecure_users; break; default: headerTextRes = R.string.contact_selection_list__header_other; break; } holder.text.setText(headerTextRes); return convertView; } @Override public long getHeaderId(int i) { final Cursor c = getCursor(); c.moveToPosition(i); return c.getInt(c.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN)); } public boolean cancelPotentialWork(String number, ImageView imageView) { final TaggedFutureTask<?> bitmapWorkerTask = AsyncDrawable.getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { final Object tag = bitmapWorkerTask.getTag(); if (tag != null && !tag.equals(number)) { bitmapWorkerTask.cancel(true); } else { return false; } } return true; } public void loadBitmap(String number, ImageView imageView) { if (cancelPotentialWork(number, imageView)) { final BitmapWorkerRunnable runnable = new BitmapWorkerRunnable(context, imageView, defaultPhoto, number, scaledPhotoSize); final TaggedFutureTask<?> task = new TaggedFutureTask<Void>(runnable, null, number); final AsyncDrawable asyncDrawable = new AsyncDrawable(context.getResources(), defaultCroppedPhoto, task); imageView.setImageDrawable(asyncDrawable); if (!task.isCancelled()) photoResolver.execute(new FutureTask<Void>(task, null)); } } public Map<Long,ContactAccessor.ContactData> getSelectedContacts() { return selectedContacts; } private void populateColumnIndices(final Cursor cursor) { this.TYPE_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.TYPE_COLUMN); this.NAME_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN); this.NUMBER_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN); this.NUMBER_TYPE_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN); this.LABEL_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN); this.ID_COLUMN = cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN); } }