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