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 17 package com.android.calendar.event; 18 19 import com.android.calendar.CalendarEventModel.Attendee; 20 import com.android.calendar.ContactsAsyncHelper; 21 import com.android.calendar.R; 22 import com.android.calendar.Utils; 23 import com.android.calendar.event.EditEventHelper.AttendeeItem; 24 import com.android.common.Rfc822Validator; 25 26 import android.content.AsyncQueryHandler; 27 import android.content.ContentResolver; 28 import android.content.ContentUris; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.database.Cursor; 32 import android.graphics.ColorMatrix; 33 import android.graphics.ColorMatrixColorFilter; 34 import android.graphics.Paint; 35 import android.graphics.drawable.Drawable; 36 import android.net.Uri; 37 import android.provider.CalendarContract.Attendees; 38 import android.provider.ContactsContract.CommonDataKinds.Email; 39 import android.provider.ContactsContract.CommonDataKinds.Identity; 40 import android.provider.ContactsContract.Contacts; 41 import android.provider.ContactsContract.Data; 42 import android.provider.ContactsContract.RawContacts; 43 import android.text.TextUtils; 44 import android.text.util.Rfc822Token; 45 import android.util.AttributeSet; 46 import android.util.Log; 47 import android.view.LayoutInflater; 48 import android.view.View; 49 import android.widget.ImageButton; 50 import android.widget.LinearLayout; 51 import android.widget.QuickContactBadge; 52 import android.widget.TextView; 53 54 import java.util.ArrayList; 55 import java.util.HashMap; 56 import java.util.LinkedHashSet; 57 58 public 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 EMAIL_PROJECTION_CONTACT_ID_INDEX = 0; 63 private static final int EMAIL_PROJECTION_CONTACT_LOOKUP_INDEX = 1; 64 private static final int EMAIL_PROJECTION_PHOTO_ID_INDEX = 2; 65 66 private static final String[] PROJECTION = new String[] { 67 RawContacts.CONTACT_ID, // 0 68 Contacts.LOOKUP_KEY, // 1 69 Contacts.PHOTO_ID, // 2 70 }; 71 72 private final Context mContext; 73 private final LayoutInflater mInflater; 74 private final PresenceQueryHandler mPresenceQueryHandler; 75 private final Drawable mDefaultBadge; 76 private final ColorMatrixColorFilter mGrayscaleFilter; 77 78 // TextView shown at the top of each type of attendees 79 // e.g. 80 // Yes <-- divider 81 // example_for_yes <exampleyes (at) example.com> 82 // No <-- divider 83 // example_for_no <exampleno (at) example.com> 84 private final CharSequence[] mEntries; 85 private final View mDividerForYes; 86 private final View mDividerForNo; 87 private final View mDividerForMaybe; 88 private final View mDividerForNoResponse; 89 private final int mNoResponsePhotoAlpha; 90 private final int mDefaultPhotoAlpha; 91 private Rfc822Validator mValidator; 92 93 // Number of attendees responding or not responding. 94 private int mYes; 95 private int mNo; 96 private int mMaybe; 97 private int mNoResponse; 98 99 // Cache for loaded photos 100 HashMap<String, Drawable> mRecycledPhotos; 101 102 public AttendeesView(Context context, AttributeSet attrs) { 103 super(context, attrs); 104 mContext = context; 105 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 106 mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver()); 107 108 final Resources resources = context.getResources(); 109 mDefaultBadge = resources.getDrawable(R.drawable.ic_contact_picture); 110 mNoResponsePhotoAlpha = 111 resources.getInteger(R.integer.noresponse_attendee_photo_alpha_level); 112 mDefaultPhotoAlpha = resources.getInteger(R.integer.default_attendee_photo_alpha_level); 113 114 // Create dividers between groups of attendees (accepted, declined, etc...) 115 mEntries = resources.getTextArray(R.array.response_labels1); 116 mDividerForYes = constructDividerView(mEntries[1]); 117 mDividerForNo = constructDividerView(mEntries[3]); 118 mDividerForMaybe = constructDividerView(mEntries[2]); 119 mDividerForNoResponse = constructDividerView(mEntries[0]); 120 121 // Create a filter to convert photos of declined attendees to grayscale. 122 ColorMatrix matrix = new ColorMatrix(); 123 matrix.setSaturation(0); 124 mGrayscaleFilter = new ColorMatrixColorFilter(matrix); 125 126 } 127 128 // Disable/enable removal of attendings 129 @Override 130 public void setEnabled(boolean enabled) { 131 super.setEnabled(enabled); 132 int visibility = isEnabled() ? View.VISIBLE : View.GONE; 133 int count = getChildCount(); 134 for (int i = 0; i < count; i++) { 135 View child = getChildAt(i); 136 View minusButton = child.findViewById(R.id.contact_remove); 137 if (minusButton != null) { 138 minusButton.setVisibility(visibility); 139 } 140 } 141 } 142 143 public void setRfc822Validator(Rfc822Validator validator) { 144 mValidator = validator; 145 } 146 147 private View constructDividerView(CharSequence label) { 148 final TextView textView = 149 (TextView)mInflater.inflate(R.layout.event_info_label, this, false); 150 textView.setText(label); 151 textView.setClickable(false); 152 return textView; 153 } 154 155 // Add the number of attendees in the specific status (corresponding to the divider) in 156 // parenthesis next to the label 157 private void updateDividerViewLabel(View divider, CharSequence label, int count) { 158 if (count <= 0) { 159 ((TextView)divider).setText(label); 160 } 161 else { 162 ((TextView)divider).setText(label + " (" + count + ")"); 163 } 164 } 165 166 167 /** 168 * Inflates a layout for a given attendee view and set up each element in it, and returns 169 * the constructed View object. The object is also stored in {@link AttendeeItem#mView}. 170 */ 171 private View constructAttendeeView(AttendeeItem item) { 172 item.mView = mInflater.inflate(R.layout.contact_item, null); 173 return updateAttendeeView(item); 174 } 175 176 /** 177 * Set up each element in {@link AttendeeItem#mView} using the latest information. View 178 * object is reused. 179 */ 180 private View updateAttendeeView(AttendeeItem item) { 181 final Attendee attendee = item.mAttendee; 182 final View view = item.mView; 183 final TextView nameView = (TextView) view.findViewById(R.id.name); 184 nameView.setText(TextUtils.isEmpty(attendee.mName) ? attendee.mEmail : attendee.mName); 185 if (item.mRemoved) { 186 nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags()); 187 } else { 188 nameView.setPaintFlags((~Paint.STRIKE_THRU_TEXT_FLAG) & nameView.getPaintFlags()); 189 } 190 191 // Set up the Image button even if the view is disabled 192 // Everything will be ready when the view is enabled later 193 final ImageButton button = (ImageButton) view.findViewById(R.id.contact_remove); 194 button.setVisibility(isEnabled() ? View.VISIBLE : View.GONE); 195 button.setTag(item); 196 if (item.mRemoved) { 197 button.setImageResource(R.drawable.ic_menu_add_field_holo_light); 198 button.setContentDescription(mContext.getString(R.string.accessibility_add_attendee)); 199 } else { 200 button.setImageResource(R.drawable.ic_menu_remove_field_holo_light); 201 button.setContentDescription(mContext. 202 getString(R.string.accessibility_remove_attendee)); 203 } 204 button.setOnClickListener(this); 205 206 final QuickContactBadge badgeView = (QuickContactBadge) view.findViewById(R.id.badge); 207 208 Drawable badge = null; 209 // Search for photo in recycled photos 210 if (mRecycledPhotos != null) { 211 badge = mRecycledPhotos.get(item.mAttendee.mEmail); 212 } 213 if (badge != null) { 214 item.mBadge = badge; 215 } 216 badgeView.setImageDrawable(item.mBadge); 217 218 if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) { 219 item.mBadge.setAlpha(mNoResponsePhotoAlpha); 220 } else { 221 item.mBadge.setAlpha(mDefaultPhotoAlpha); 222 } 223 if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) { 224 item.mBadge.setColorFilter(mGrayscaleFilter); 225 } else { 226 item.mBadge.setColorFilter(null); 227 } 228 229 // If we know the lookup-uri of the contact, it is a good idea to set this here. This 230 // allows QuickContact to be started without an extra database lookup. If we don't know 231 // the lookup uri (yet), we can set Email and QuickContact will lookup once tapped. 232 if (item.mContactLookupUri != null) { 233 badgeView.assignContactUri(item.mContactLookupUri); 234 } else { 235 badgeView.assignContactFromEmail(item.mAttendee.mEmail, true); 236 } 237 badgeView.setMaxHeight(60); 238 239 return view; 240 } 241 242 public boolean contains(Attendee attendee) { 243 final int size = getChildCount(); 244 for (int i = 0; i < size; i++) { 245 final View view = getChildAt(i); 246 if (view instanceof TextView) { // divider 247 continue; 248 } 249 AttendeeItem attendeeItem = (AttendeeItem) view.getTag(); 250 if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) { 251 return true; 252 } 253 } 254 return false; 255 } 256 257 public void clearAttendees() { 258 259 // Before clearing the views, save all the badges. The updateAtendeeView will use the saved 260 // photo instead of the default badge thus prevent switching between the two while the 261 // most current photo is loaded in the background. 262 mRecycledPhotos = new HashMap<String, Drawable> (); 263 final int size = getChildCount(); 264 for (int i = 0; i < size; i++) { 265 final View view = getChildAt(i); 266 if (view instanceof TextView) { // divider 267 continue; 268 } 269 AttendeeItem attendeeItem = (AttendeeItem) view.getTag(); 270 mRecycledPhotos.put(attendeeItem.mAttendee.mEmail, attendeeItem.mBadge); 271 } 272 273 removeAllViews(); 274 mYes = 0; 275 mNo = 0; 276 mMaybe = 0; 277 mNoResponse = 0; 278 } 279 280 private void addOneAttendee(Attendee attendee) { 281 if (contains(attendee)) { 282 return; 283 } 284 final AttendeeItem item = new AttendeeItem(attendee, mDefaultBadge); 285 final int status = attendee.mStatus; 286 final int index; 287 boolean firstAttendeeInCategory = false; 288 switch (status) { 289 case Attendees.ATTENDEE_STATUS_ACCEPTED: { 290 final int startIndex = 0; 291 updateDividerViewLabel(mDividerForYes, mEntries[1], mYes + 1); 292 if (mYes == 0) { 293 addView(mDividerForYes, startIndex); 294 firstAttendeeInCategory = true; 295 } 296 mYes++; 297 index = startIndex + mYes; 298 break; 299 } 300 case Attendees.ATTENDEE_STATUS_DECLINED: { 301 final int startIndex = (mYes == 0 ? 0 : 1 + mYes); 302 updateDividerViewLabel(mDividerForNo, mEntries[3], mNo + 1); 303 if (mNo == 0) { 304 addView(mDividerForNo, startIndex); 305 firstAttendeeInCategory = true; 306 } 307 mNo++; 308 index = startIndex + mNo; 309 break; 310 } 311 case Attendees.ATTENDEE_STATUS_TENTATIVE: { 312 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo); 313 updateDividerViewLabel(mDividerForMaybe, mEntries[2], mMaybe + 1); 314 if (mMaybe == 0) { 315 addView(mDividerForMaybe, startIndex); 316 firstAttendeeInCategory = true; 317 } 318 mMaybe++; 319 index = startIndex + mMaybe; 320 break; 321 } 322 default: { 323 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) 324 + (mMaybe == 0 ? 0 : 1 + mMaybe); 325 updateDividerViewLabel(mDividerForNoResponse, mEntries[0], mNoResponse + 1); 326 if (mNoResponse == 0) { 327 addView(mDividerForNoResponse, startIndex); 328 firstAttendeeInCategory = true; 329 } 330 mNoResponse++; 331 index = startIndex + mNoResponse; 332 break; 333 } 334 } 335 336 final View view = constructAttendeeView(item); 337 view.setTag(item); 338 addView(view, index); 339 // Show separator between Attendees 340 if (!firstAttendeeInCategory) { 341 View prevItem = getChildAt(index - 1); 342 if (prevItem != null) { 343 View Separator = prevItem.findViewById(R.id.contact_separator); 344 if (Separator != null) { 345 Separator.setVisibility(View.VISIBLE); 346 } 347 } 348 } 349 350 Uri uri; 351 String selection = null; 352 String[] selectionArgs = null; 353 if (attendee.mIdentity != null && attendee.mIdNamespace != null) { 354 // Query by identity + namespace 355 uri = Data.CONTENT_URI; 356 selection = Data.MIMETYPE + "=? AND " + Identity.IDENTITY + "=? AND " + 357 Identity.NAMESPACE + "=?"; 358 selectionArgs = new String[] {Identity.CONTENT_ITEM_TYPE, attendee.mIdentity, 359 attendee.mIdNamespace}; 360 } else { 361 // Query by email 362 uri = Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(attendee.mEmail)); 363 } 364 365 mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item, uri, PROJECTION, selection, 366 selectionArgs, null); 367 } 368 369 public void addAttendees(ArrayList<Attendee> attendees) { 370 synchronized (this) { 371 for (final Attendee attendee : attendees) { 372 addOneAttendee(attendee); 373 } 374 } 375 } 376 377 public void addAttendees(HashMap<String, Attendee> attendees) { 378 synchronized (this) { 379 for (final Attendee attendee : attendees.values()) { 380 addOneAttendee(attendee); 381 } 382 } 383 } 384 385 public void addAttendees(String attendees) { 386 final LinkedHashSet<Rfc822Token> addresses = 387 EditEventHelper.getAddressesFromList(attendees, mValidator); 388 synchronized (this) { 389 for (final Rfc822Token address : addresses) { 390 final Attendee attendee = new Attendee(address.getName(), address.getAddress()); 391 if (TextUtils.isEmpty(attendee.mName)) { 392 attendee.mName = attendee.mEmail; 393 } 394 addOneAttendee(attendee); 395 } 396 } 397 } 398 399 /** 400 * Returns true when the attendee at that index is marked as "removed" (the name of 401 * the attendee is shown with a strike through line). 402 */ 403 public boolean isMarkAsRemoved(int index) { 404 final View view = getChildAt(index); 405 if (view instanceof TextView) { // divider 406 return false; 407 } 408 return ((AttendeeItem) view.getTag()).mRemoved; 409 } 410 411 // TODO put this into a Loader for auto-requeries 412 private class PresenceQueryHandler extends AsyncQueryHandler { 413 public PresenceQueryHandler(ContentResolver cr) { 414 super(cr); 415 } 416 417 @Override 418 protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) { 419 if (cursor == null || cookie == null) { 420 if (DEBUG) { 421 Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie); 422 } 423 return; 424 } 425 426 final AttendeeItem item = (AttendeeItem)cookie; 427 try { 428 if (item.mUpdateCounts < queryIndex) { 429 item.mUpdateCounts = queryIndex; 430 if (cursor.moveToFirst()) { 431 final long contactId = cursor.getLong(EMAIL_PROJECTION_CONTACT_ID_INDEX); 432 final Uri contactUri = 433 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 434 435 final String lookupKey = 436 cursor.getString(EMAIL_PROJECTION_CONTACT_LOOKUP_INDEX); 437 item.mContactLookupUri = Contacts.getLookupUri(contactId, lookupKey); 438 439 final long photoId = cursor.getLong(EMAIL_PROJECTION_PHOTO_ID_INDEX); 440 // If we found a picture, start the async loading 441 if (photoId > 0) { 442 // Query for this contacts picture 443 ContactsAsyncHelper.retrieveContactPhotoAsync( 444 mContext, item, new Runnable() { 445 @Override 446 public void run() { 447 updateAttendeeView(item); 448 } 449 }, contactUri); 450 } else { 451 // call update view to make sure that the lookup key gets set in 452 // the QuickContactBadge 453 updateAttendeeView(item); 454 } 455 } else { 456 // Contact not found. For real emails, keep the QuickContactBadge with 457 // its Email address set, so that the user can create a contact by tapping. 458 item.mContactLookupUri = null; 459 if (!Utils.isValidEmail(item.mAttendee.mEmail)) { 460 item.mAttendee.mEmail = null; 461 updateAttendeeView(item); 462 } 463 } 464 } 465 } finally { 466 cursor.close(); 467 } 468 } 469 } 470 471 public Attendee getItem(int index) { 472 final View view = getChildAt(index); 473 if (view instanceof TextView) { // divider 474 return null; 475 } 476 return ((AttendeeItem) view.getTag()).mAttendee; 477 } 478 479 @Override 480 public void onClick(View view) { 481 // Button corresponding to R.id.contact_remove. 482 final AttendeeItem item = (AttendeeItem) view.getTag(); 483 item.mRemoved = !item.mRemoved; 484 updateAttendeeView(item); 485 } 486 } 487