AttendeesView.java revision 4c5475e6d27497be020d3098c4554fe353d19d38
1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.calendar.event; 18 19import com.android.calendar.CalendarEventModel.Attendee; 20import com.android.calendar.ContactsAsyncHelper; 21import com.android.calendar.R; 22import com.android.calendar.event.EditEventHelper.AttendeeItem; 23import com.android.common.Rfc822Validator; 24 25import android.content.AsyncQueryHandler; 26import android.content.ContentResolver; 27import android.content.ContentUris; 28import android.content.Context; 29import android.content.res.Resources; 30import android.database.Cursor; 31import android.graphics.Color; 32import android.graphics.ColorMatrix; 33import android.graphics.ColorMatrixColorFilter; 34import android.graphics.Paint; 35import android.graphics.drawable.Drawable; 36import android.net.Uri; 37import android.provider.Calendar.Attendees; 38import android.provider.ContactsContract.CommonDataKinds.Email; 39import android.provider.ContactsContract.Contacts; 40import android.provider.ContactsContract.Data; 41import android.provider.ContactsContract.StatusUpdates; 42import android.text.TextUtils; 43import android.text.util.Rfc822Token; 44import android.util.AttributeSet; 45import android.util.Log; 46import android.view.LayoutInflater; 47import android.view.View; 48import android.widget.ImageButton; 49import android.widget.ImageView; 50import android.widget.LinearLayout; 51import android.widget.QuickContactBadge; 52import android.widget.TextView; 53 54import java.util.ArrayList; 55import java.util.HashMap; 56import java.util.LinkedHashSet; 57 58public class AttendeesView extends LinearLayout implements View.OnClickListener { 59 private static final String TAG = "AttendeesView"; 60 private static final boolean DEBUG = false; 61 62 private static final int PRESENCE_PROJECTION_CONTACT_ID_INDEX = 0; 63 private static final int PRESENCE_PROJECTION_PRESENCE_INDEX = 1; 64 private static final int PRESENCE_PROJECTION_EMAIL_INDEX = 2; 65 private static final int PRESENCE_PROJECTION_PHOTO_ID_INDEX = 3; 66 67 private static final String[] PRESENCE_PROJECTION = new String[] { 68 Email.CONTACT_ID, // 0 69 Email.CONTACT_PRESENCE, // 1 70 Email.DATA, // 2 71 Email.PHOTO_ID, // 3 72 }; 73 74 private static final Uri CONTACT_DATA_WITH_PRESENCE_URI = Data.CONTENT_URI; 75 private static final String CONTACT_DATA_SELECTION = Email.DATA + " IN (?)"; 76 77 private final Context mContext; 78 private final LayoutInflater mInflater; 79 private final PresenceQueryHandler mPresenceQueryHandler; 80 private final Drawable mDefaultBadge; 81 private final ColorMatrixColorFilter mGrayscaleFilter; 82 83 // TextView shown at the top of each type of attendees 84 // e.g. 85 // Yes <-- divider 86 // example_for_yes <exampleyes@example.com> 87 // No <-- divider 88 // example_for_no <exampleno@example.com> 89 private final CharSequence[] mEntries; 90 private final View mDividerForYes; 91 private final View mDividerForNo; 92 private final View mDividerForMaybe; 93 private final View mDividerForNoResponse; 94 private final int mNoResponsePhotoAlpha; 95 private final int mDefaultPhotoAlpha; 96 private Rfc822Validator mValidator; 97 98 // Number of attendees responding or not responding. 99 private int mYes; 100 private int mNo; 101 private int mMaybe; 102 private int mNoResponse; 103 104 public AttendeesView(Context context, AttributeSet attrs) { 105 super(context, attrs); 106 mContext = context; 107 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 108 mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver()); 109 110 final Resources resources = context.getResources(); 111 mDefaultBadge = resources.getDrawable(R.drawable.ic_contact_picture); 112 mNoResponsePhotoAlpha = 113 resources.getInteger(R.integer.noresponse_attendee_photo_alpha_level); 114 mDefaultPhotoAlpha = resources.getInteger(R.integer.default_attendee_photo_alpha_level); 115 116 // Create dividers between groups of attendees (accepted, declined, etc...) 117 mEntries = resources.getTextArray(R.array.response_labels1); 118 mDividerForYes = constructDividerView(mEntries[1]); 119 mDividerForNo = constructDividerView(mEntries[3]); 120 mDividerForMaybe = constructDividerView(mEntries[2]); 121 mDividerForNoResponse = constructDividerView(mEntries[0]); 122 123 // Create a filter to convert photos of declined attendees to grayscale. 124 ColorMatrix matrix = new ColorMatrix(); 125 matrix.setSaturation(0); 126 mGrayscaleFilter = new ColorMatrixColorFilter(matrix); 127 128 } 129 130 // Disable/enable removal of attendings 131 @Override 132 public void setEnabled(boolean enabled) { 133 super.setEnabled(enabled); 134 int visibility = isEnabled() ? View.VISIBLE : View.GONE; 135 int count = getChildCount(); 136 for (int i = 0; i < count; i++) { 137 View child = getChildAt(i); 138 View minusButton = child.findViewById(R.id.contact_remove); 139 if (minusButton != null) { 140 minusButton.setVisibility(visibility); 141 } 142 } 143 } 144 145 public void setRfc822Validator(Rfc822Validator validator) { 146 mValidator = validator; 147 } 148 149 private View constructDividerView(CharSequence label) { 150 final TextView textView = new TextView(mContext); 151 textView.setText(label); 152 textView.setTextAppearance(mContext, R.style.TextAppearance_EventInfo_Label); 153 textView.setTextColor(Color.BLACK); 154 textView.setClickable(false); 155 return textView; 156 } 157 158 // Add the number of attendees in the specific status (corresponding to the divider) in 159 // parenthesis next to the label 160 private void updateDividerViewLabel(View divider, CharSequence label, int count) { 161 if (count <= 0) { 162 ((TextView)divider).setText(label); 163 } 164 else { 165 ((TextView)divider).setText(label + " (" + count + ")"); 166 } 167 } 168 169 170 /** 171 * Inflates a layout for a given attendee view and set up each element in it, and returns 172 * the constructed View object. The object is also stored in {@link AttendeeItem#mView}. 173 */ 174 private View constructAttendeeView(AttendeeItem item) { 175 item.mView = mInflater.inflate(R.layout.contact_item, null); 176 return updateAttendeeView(item); 177 } 178 179 /** 180 * Set up each element in {@link AttendeeItem#mView} using the latest information. View 181 * object is reused. 182 */ 183 private View updateAttendeeView(AttendeeItem item) { 184 final Attendee attendee = item.mAttendee; 185 final View view = item.mView; 186 final TextView nameView = (TextView) view.findViewById(R.id.name); 187 nameView.setText(TextUtils.isEmpty(attendee.mName) ? attendee.mEmail : attendee.mName); 188 if (item.mRemoved) { 189 nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags()); 190 } else { 191 nameView.setPaintFlags((~Paint.STRIKE_THRU_TEXT_FLAG) & nameView.getPaintFlags()); 192 } 193 194 // Set up the Image button even if the view is disabled 195 // Everything will be ready when the view is enabled later 196 final ImageButton button = (ImageButton) view.findViewById(R.id.contact_remove); 197 button.setVisibility(isEnabled() ? View.VISIBLE : View.GONE); 198 button.setTag(item); 199 if (item.mRemoved) { 200 button.setImageResource(R.drawable.ic_menu_add_field_holo_light); 201 } else { 202 button.setImageResource(R.drawable.ic_menu_remove_field_holo_light); 203 } 204 button.setOnClickListener(this); 205 206 final QuickContactBadge badge = (QuickContactBadge) view.findViewById(R.id.badge); 207 if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) { 208 item.mBadge.setAlpha(mNoResponsePhotoAlpha); 209 } else { 210 item.mBadge.setAlpha(mDefaultPhotoAlpha); 211 } 212 if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) { 213 item.mBadge.setColorFilter(mGrayscaleFilter); 214 } else { 215 item.mBadge.setColorFilter(null); 216 } 217 badge.setImageDrawable(item.mBadge); 218 badge.assignContactFromEmail(item.mAttendee.mEmail, true); 219 badge.setMaxHeight(60); 220 if (item.mPresence != -1) { 221 final ImageView presence = (ImageView) view.findViewById(R.id.presence); 222 presence.setImageResource(StatusUpdates.getPresenceIconResourceId(item.mPresence)); 223 presence.setVisibility(View.VISIBLE); 224 } 225 226 return view; 227 } 228 229 public boolean contains(Attendee attendee) { 230 final int size = getChildCount(); 231 for (int i = 0; i < size; i++) { 232 final View view = getChildAt(i); 233 if (view instanceof TextView) { // divider 234 continue; 235 } 236 AttendeeItem attendeeItem = (AttendeeItem) view.getTag(); 237 if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) { 238 return true; 239 } 240 } 241 return false; 242 } 243 244 245 private void addOneAttendee(Attendee attendee) { 246 if (contains(attendee)) { 247 return; 248 } 249 final AttendeeItem item = new AttendeeItem(attendee, -1 /* presence */, mDefaultBadge); 250 final int status = attendee.mStatus; 251 final String name = attendee.mName == null ? "" : attendee.mName; 252 final int index; 253 switch (status) { 254 case Attendees.ATTENDEE_STATUS_ACCEPTED: { 255 final int startIndex = 0; 256 updateDividerViewLabel(mDividerForYes, mEntries[1], mYes + 1); 257 if (mYes == 0) { 258 addView(mDividerForYes, startIndex); 259 } 260 mYes++; 261 index = startIndex + mYes; 262 break; 263 } 264 case Attendees.ATTENDEE_STATUS_DECLINED: { 265 final int startIndex = (mYes == 0 ? 0 : 1 + mYes); 266 updateDividerViewLabel(mDividerForNo, mEntries[3], mNo + 1); 267 if (mNo == 0) { 268 addView(mDividerForNo, startIndex); 269 } 270 mNo++; 271 index = startIndex + mNo; 272 break; 273 } 274 case Attendees.ATTENDEE_STATUS_TENTATIVE: { 275 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo); 276 updateDividerViewLabel(mDividerForMaybe, mEntries[2], mMaybe + 1); 277 if (mMaybe == 0) { 278 addView(mDividerForMaybe, startIndex); 279 } 280 mMaybe++; 281 index = startIndex + mMaybe; 282 break; 283 } 284 default: { 285 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) 286 + (mMaybe == 0 ? 0 : 1 + mMaybe); 287 // We delay adding the divider for "No response". 288 index = startIndex + mNoResponse; 289 mNoResponse++; 290 updateDividerViewLabel(mDividerForNoResponse, mEntries[0], mNoResponse); 291 break; 292 } 293 } 294 295 final View view = constructAttendeeView(item); 296 view.setTag(item); 297 addView(view, index); 298 299 // We want "No Response" divider only when 300 // - someone already answered in some way, 301 // - there is attendees not responding yet, and 302 // - divider isn't in the list yet 303 if (mYes + mNo + mMaybe > 0 && mNoResponse > 0 && 304 mDividerForNoResponse.getParent() == null) { 305 final int dividerIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) + 306 (mMaybe == 0 ? 0 : 1 + mMaybe); 307 addView(mDividerForNoResponse, dividerIndex); 308 } 309 310 mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item, 311 CONTACT_DATA_WITH_PRESENCE_URI, PRESENCE_PROJECTION, CONTACT_DATA_SELECTION, 312 new String[] { attendee.mEmail }, null); 313 } 314 315 public void addAttendees(ArrayList<Attendee> attendees) { 316 synchronized (this) { 317 for (final Attendee attendee : attendees) { 318 addOneAttendee(attendee); 319 } 320 } 321 } 322 323 public void addAttendees(HashMap<String, Attendee> attendees) { 324 synchronized (this) { 325 for (final Attendee attendee : attendees.values()) { 326 addOneAttendee(attendee); 327 } 328 } 329 } 330 331 public void addAttendees(String attendees) { 332 final LinkedHashSet<Rfc822Token> addresses = 333 EditEventHelper.getAddressesFromList(attendees, mValidator); 334 synchronized (this) { 335 for (final Rfc822Token address : addresses) { 336 final Attendee attendee = new Attendee(address.getName(), address.getAddress()); 337 if (TextUtils.isEmpty(attendee.mName)) { 338 attendee.mName = attendee.mEmail; 339 } 340 addOneAttendee(attendee); 341 } 342 } 343 } 344 345 /** 346 * Returns true when the attendee at that index is marked as "removed" (the name of 347 * the attendee is shown with a strike through line). 348 */ 349 public boolean isMarkAsRemoved(int index) { 350 final View view = getChildAt(index); 351 if (view instanceof TextView) { // divider 352 return false; 353 } 354 return ((AttendeeItem) view.getTag()).mRemoved; 355 } 356 357 // TODO put this into a Loader for auto-requeries 358 private class PresenceQueryHandler extends AsyncQueryHandler { 359 public PresenceQueryHandler(ContentResolver cr) { 360 super(cr); 361 } 362 363 @Override 364 protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) { 365 if (cursor == null || cookie == null) { 366 if (DEBUG) { 367 Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie); 368 } 369 return; 370 } 371 372 final AttendeeItem item = (AttendeeItem)cookie; 373 try { 374 cursor.moveToPosition(-1); 375 boolean found = false; 376 int contactId = 0; 377 int photoId = 0; 378 int presence = 0; 379 while (cursor.moveToNext()) { 380 String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX); 381 int temp = 0; 382 temp = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX); 383 // A photo id must be > 0 and we only care about the contact 384 // ID if there's a photo 385 if (temp > 0) { 386 photoId = temp; 387 contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX); 388 } 389 // Take the most available status we can find. 390 presence = Math.max( 391 cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX), presence); 392 393 found = true; 394 if (DEBUG) { 395 Log.d(TAG, 396 "onQueryComplete Id: " + contactId + " PhotoId: " + photoId 397 + " Email: " + email + " updateCount:" + item.mUpdateCounts 398 + " Presence:" + item.mPresence); 399 } 400 } 401 if (found) { 402 item.mPresence = presence; 403 404 if (photoId > 0 && item.mUpdateCounts < queryIndex) { 405 item.mUpdateCounts = queryIndex; 406 final Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, 407 contactId); 408 // Query for this contacts picture 409 ContactsAsyncHelper.retrieveContactPhotoAsync( 410 mContext, item, new Runnable() { 411 public void run() { 412 updateAttendeeView(item); 413 } 414 }, personUri); 415 } 416 } 417 } finally { 418 cursor.close(); 419 } 420 } 421 } 422 423 public Attendee getItem(int index) { 424 final View view = getChildAt(index); 425 if (view instanceof TextView) { // divider 426 return null; 427 } 428 return ((AttendeeItem) view.getTag()).mAttendee; 429 } 430 431 @Override 432 public void onClick(View view) { 433 // Button corresponding to R.id.contact_remove. 434 final AttendeeItem item = (AttendeeItem) view.getTag(); 435 item.mRemoved = !item.mRemoved; 436 updateAttendeeView(item); 437 } 438} 439