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