blob: a54d40b62a779ccaef5b567983fb9c654471eabf [file] [log] [blame]
/*
* Copyright (C) 2010 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.calendar.event;
import com.android.calendar.CalendarEventModel.Attendee;
import com.android.calendar.ContactsAsyncHelper;
import com.android.calendar.R;
import com.android.calendar.Utils;
import com.android.calendar.event.EditEventHelper.AttendeeItem;
import com.android.common.Rfc822Validator;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.CalendarContract.Attendees;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Identity;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.QuickContactBadge;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
public class AttendeesView extends LinearLayout implements View.OnClickListener {
private static final String TAG = "AttendeesView";
private static final boolean DEBUG = false;
private static final int EMAIL_PROJECTION_CONTACT_ID_INDEX = 0;
private static final int EMAIL_PROJECTION_CONTACT_LOOKUP_INDEX = 1;
private static final int EMAIL_PROJECTION_PHOTO_ID_INDEX = 2;
private static final String[] PROJECTION = new String[] {
RawContacts.CONTACT_ID, // 0
Contacts.LOOKUP_KEY, // 1
Contacts.PHOTO_ID, // 2
};
private final Context mContext;
private final LayoutInflater mInflater;
private final PresenceQueryHandler mPresenceQueryHandler;
private final Drawable mDefaultBadge;
private final ColorMatrixColorFilter mGrayscaleFilter;
// TextView shown at the top of each type of attendees
// e.g.
// Yes <-- divider
// example_for_yes <exampleyes@example.com>
// No <-- divider
// example_for_no <exampleno@example.com>
private final CharSequence[] mEntries;
private final View mDividerForYes;
private final View mDividerForNo;
private final View mDividerForMaybe;
private final View mDividerForNoResponse;
private final int mNoResponsePhotoAlpha;
private final int mDefaultPhotoAlpha;
private Rfc822Validator mValidator;
// Number of attendees responding or not responding.
private int mYes;
private int mNo;
private int mMaybe;
private int mNoResponse;
// Cache for loaded photos
HashMap<String, Drawable> mRecycledPhotos;
public AttendeesView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver());
final Resources resources = context.getResources();
mDefaultBadge = resources.getDrawable(R.drawable.ic_contact_picture);
mNoResponsePhotoAlpha =
resources.getInteger(R.integer.noresponse_attendee_photo_alpha_level);
mDefaultPhotoAlpha = resources.getInteger(R.integer.default_attendee_photo_alpha_level);
// Create dividers between groups of attendees (accepted, declined, etc...)
mEntries = resources.getTextArray(R.array.response_labels1);
mDividerForYes = constructDividerView(mEntries[1]);
mDividerForNo = constructDividerView(mEntries[3]);
mDividerForMaybe = constructDividerView(mEntries[2]);
mDividerForNoResponse = constructDividerView(mEntries[0]);
// Create a filter to convert photos of declined attendees to grayscale.
ColorMatrix matrix = new ColorMatrix();
matrix.setSaturation(0);
mGrayscaleFilter = new ColorMatrixColorFilter(matrix);
}
// Disable/enable removal of attendings
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
int visibility = isEnabled() ? View.VISIBLE : View.GONE;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
View minusButton = child.findViewById(R.id.contact_remove);
if (minusButton != null) {
minusButton.setVisibility(visibility);
}
}
}
public void setRfc822Validator(Rfc822Validator validator) {
mValidator = validator;
}
private View constructDividerView(CharSequence label) {
final TextView textView =
(TextView)mInflater.inflate(R.layout.event_info_label, this, false);
textView.setText(label);
textView.setClickable(false);
return textView;
}
// Add the number of attendees in the specific status (corresponding to the divider) in
// parenthesis next to the label
private void updateDividerViewLabel(View divider, CharSequence label, int count) {
if (count <= 0) {
((TextView)divider).setText(label);
}
else {
((TextView)divider).setText(label + " (" + count + ")");
}
}
/**
* Inflates a layout for a given attendee view and set up each element in it, and returns
* the constructed View object. The object is also stored in {@link AttendeeItem#mView}.
*/
private View constructAttendeeView(AttendeeItem item) {
item.mView = mInflater.inflate(R.layout.contact_item, null);
return updateAttendeeView(item);
}
/**
* Set up each element in {@link AttendeeItem#mView} using the latest information. View
* object is reused.
*/
private View updateAttendeeView(AttendeeItem item) {
final Attendee attendee = item.mAttendee;
final View view = item.mView;
final TextView nameView = (TextView) view.findViewById(R.id.name);
nameView.setText(TextUtils.isEmpty(attendee.mName) ? attendee.mEmail : attendee.mName);
if (item.mRemoved) {
nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags());
} else {
nameView.setPaintFlags((~Paint.STRIKE_THRU_TEXT_FLAG) & nameView.getPaintFlags());
}
// Set up the Image button even if the view is disabled
// Everything will be ready when the view is enabled later
final ImageButton button = (ImageButton) view.findViewById(R.id.contact_remove);
button.setVisibility(isEnabled() ? View.VISIBLE : View.GONE);
button.setTag(item);
if (item.mRemoved) {
button.setImageResource(R.drawable.ic_menu_add_field_holo_light);
button.setContentDescription(mContext.getString(R.string.accessibility_add_attendee));
} else {
button.setImageResource(R.drawable.ic_menu_remove_field_holo_light);
button.setContentDescription(mContext.
getString(R.string.accessibility_remove_attendee));
}
button.setOnClickListener(this);
final QuickContactBadge badgeView = (QuickContactBadge) view.findViewById(R.id.badge);
Drawable badge = null;
// Search for photo in recycled photos
if (mRecycledPhotos != null) {
badge = mRecycledPhotos.get(item.mAttendee.mEmail);
}
if (badge != null) {
item.mBadge = badge;
}
badgeView.setImageDrawable(item.mBadge);
if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) {
item.mBadge.setAlpha(mNoResponsePhotoAlpha);
} else {
item.mBadge.setAlpha(mDefaultPhotoAlpha);
}
if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) {
item.mBadge.setColorFilter(mGrayscaleFilter);
} else {
item.mBadge.setColorFilter(null);
}
// If we know the lookup-uri of the contact, it is a good idea to set this here. This
// allows QuickContact to be started without an extra database lookup. If we don't know
// the lookup uri (yet), we can set Email and QuickContact will lookup once tapped.
if (item.mContactLookupUri != null) {
badgeView.assignContactUri(item.mContactLookupUri);
} else {
badgeView.assignContactFromEmail(item.mAttendee.mEmail, true);
}
badgeView.setMaxHeight(60);
return view;
}
public boolean contains(Attendee attendee) {
final int size = getChildCount();
for (int i = 0; i < size; i++) {
final View view = getChildAt(i);
if (view instanceof TextView) { // divider
continue;
}
AttendeeItem attendeeItem = (AttendeeItem) view.getTag();
if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) {
return true;
}
}
return false;
}
public void clearAttendees() {
// Before clearing the views, save all the badges. The updateAtendeeView will use the saved
// photo instead of the default badge thus prevent switching between the two while the
// most current photo is loaded in the background.
mRecycledPhotos = new HashMap<String, Drawable> ();
final int size = getChildCount();
for (int i = 0; i < size; i++) {
final View view = getChildAt(i);
if (view instanceof TextView) { // divider
continue;
}
AttendeeItem attendeeItem = (AttendeeItem) view.getTag();
mRecycledPhotos.put(attendeeItem.mAttendee.mEmail, attendeeItem.mBadge);
}
removeAllViews();
mYes = 0;
mNo = 0;
mMaybe = 0;
mNoResponse = 0;
}
private void addOneAttendee(Attendee attendee) {
if (contains(attendee)) {
return;
}
final AttendeeItem item = new AttendeeItem(attendee, mDefaultBadge);
final int status = attendee.mStatus;
final int index;
boolean firstAttendeeInCategory = false;
switch (status) {
case Attendees.ATTENDEE_STATUS_ACCEPTED: {
final int startIndex = 0;
updateDividerViewLabel(mDividerForYes, mEntries[1], mYes + 1);
if (mYes == 0) {
addView(mDividerForYes, startIndex);
firstAttendeeInCategory = true;
}
mYes++;
index = startIndex + mYes;
break;
}
case Attendees.ATTENDEE_STATUS_DECLINED: {
final int startIndex = (mYes == 0 ? 0 : 1 + mYes);
updateDividerViewLabel(mDividerForNo, mEntries[3], mNo + 1);
if (mNo == 0) {
addView(mDividerForNo, startIndex);
firstAttendeeInCategory = true;
}
mNo++;
index = startIndex + mNo;
break;
}
case Attendees.ATTENDEE_STATUS_TENTATIVE: {
final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo);
updateDividerViewLabel(mDividerForMaybe, mEntries[2], mMaybe + 1);
if (mMaybe == 0) {
addView(mDividerForMaybe, startIndex);
firstAttendeeInCategory = true;
}
mMaybe++;
index = startIndex + mMaybe;
break;
}
default: {
final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo)
+ (mMaybe == 0 ? 0 : 1 + mMaybe);
updateDividerViewLabel(mDividerForNoResponse, mEntries[0], mNoResponse + 1);
if (mNoResponse == 0) {
addView(mDividerForNoResponse, startIndex);
firstAttendeeInCategory = true;
}
mNoResponse++;
index = startIndex + mNoResponse;
break;
}
}
final View view = constructAttendeeView(item);
view.setTag(item);
addView(view, index);
// Show separator between Attendees
if (!firstAttendeeInCategory) {
View prevItem = getChildAt(index - 1);
if (prevItem != null) {
View Separator = prevItem.findViewById(R.id.contact_separator);
if (Separator != null) {
Separator.setVisibility(View.VISIBLE);
}
}
}
Uri uri;
String selection = null;
String[] selectionArgs = null;
if (attendee.mIdentity != null && attendee.mIdNamespace != null) {
// Query by identity + namespace
uri = Data.CONTENT_URI;
selection = Data.MIMETYPE + "=? AND " + Identity.IDENTITY + "=? AND " +
Identity.NAMESPACE + "=?";
selectionArgs = new String[] {Identity.CONTENT_ITEM_TYPE, attendee.mIdentity,
attendee.mIdNamespace};
} else {
// Query by email
uri = Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(attendee.mEmail));
}
mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item, uri, PROJECTION, selection,
selectionArgs, null);
}
public void addAttendees(ArrayList<Attendee> attendees) {
synchronized (this) {
for (final Attendee attendee : attendees) {
addOneAttendee(attendee);
}
}
}
public void addAttendees(HashMap<String, Attendee> attendees) {
synchronized (this) {
for (final Attendee attendee : attendees.values()) {
addOneAttendee(attendee);
}
}
}
public void addAttendees(String attendees) {
final LinkedHashSet<Rfc822Token> addresses =
EditEventHelper.getAddressesFromList(attendees, mValidator);
synchronized (this) {
for (final Rfc822Token address : addresses) {
final Attendee attendee = new Attendee(address.getName(), address.getAddress());
if (TextUtils.isEmpty(attendee.mName)) {
attendee.mName = attendee.mEmail;
}
addOneAttendee(attendee);
}
}
}
/**
* Returns true when the attendee at that index is marked as "removed" (the name of
* the attendee is shown with a strike through line).
*/
public boolean isMarkAsRemoved(int index) {
final View view = getChildAt(index);
if (view instanceof TextView) { // divider
return false;
}
return ((AttendeeItem) view.getTag()).mRemoved;
}
// TODO put this into a Loader for auto-requeries
private class PresenceQueryHandler extends AsyncQueryHandler {
public PresenceQueryHandler(ContentResolver cr) {
super(cr);
}
@Override
protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
if (cursor == null || cookie == null) {
if (DEBUG) {
Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie);
}
return;
}
final AttendeeItem item = (AttendeeItem)cookie;
try {
if (item.mUpdateCounts < queryIndex) {
item.mUpdateCounts = queryIndex;
if (cursor.moveToFirst()) {
final long contactId = cursor.getLong(EMAIL_PROJECTION_CONTACT_ID_INDEX);
final Uri contactUri =
ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
final String lookupKey =
cursor.getString(EMAIL_PROJECTION_CONTACT_LOOKUP_INDEX);
item.mContactLookupUri = Contacts.getLookupUri(contactId, lookupKey);
final long photoId = cursor.getLong(EMAIL_PROJECTION_PHOTO_ID_INDEX);
// If we found a picture, start the async loading
if (photoId > 0) {
// Query for this contacts picture
ContactsAsyncHelper.retrieveContactPhotoAsync(
mContext, item, new Runnable() {
@Override
public void run() {
updateAttendeeView(item);
}
}, contactUri);
} else {
// call update view to make sure that the lookup key gets set in
// the QuickContactBadge
updateAttendeeView(item);
}
} else {
// Contact not found. For real emails, keep the QuickContactBadge with
// its Email address set, so that the user can create a contact by tapping.
item.mContactLookupUri = null;
if (!Utils.isValidEmail(item.mAttendee.mEmail)) {
item.mAttendee.mEmail = null;
updateAttendeeView(item);
}
}
}
} finally {
cursor.close();
}
}
}
public Attendee getItem(int index) {
final View view = getChildAt(index);
if (view instanceof TextView) { // divider
return null;
}
return ((AttendeeItem) view.getTag()).mAttendee;
}
@Override
public void onClick(View view) {
// Button corresponding to R.id.contact_remove.
final AttendeeItem item = (AttendeeItem) view.getTag();
item.mRemoved = !item.mRemoved;
updateAttendeeView(item);
}
}