AttendeesView.java revision 9ceed1f3df98c5fc85441da0c6e7e5d45cf17a1e
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 button.setContentDescription(mContext.getString(R.string.accessibility_add_attendee)); 202 } else { 203 button.setImageResource(R.drawable.ic_menu_remove_field_holo_light); 204 button.setContentDescription(mContext. 205 getString(R.string.accessibility_remove_attendee)); 206 } 207 button.setOnClickListener(this); 208 209 final QuickContactBadge badge = (QuickContactBadge) view.findViewById(R.id.badge); 210 if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) { 211 item.mBadge.setAlpha(mNoResponsePhotoAlpha); 212 } else { 213 item.mBadge.setAlpha(mDefaultPhotoAlpha); 214 } 215 if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) { 216 item.mBadge.setColorFilter(mGrayscaleFilter); 217 } else { 218 item.mBadge.setColorFilter(null); 219 } 220 badge.setImageDrawable(item.mBadge); 221 badge.assignContactFromEmail(item.mAttendee.mEmail, true); 222 badge.setMaxHeight(60); 223 if (item.mPresence != -1) { 224 final ImageView presence = (ImageView) view.findViewById(R.id.presence); 225 presence.setImageResource(StatusUpdates.getPresenceIconResourceId(item.mPresence)); 226 presence.setVisibility(View.VISIBLE); 227 } 228 229 return view; 230 } 231 232 public boolean contains(Attendee attendee) { 233 final int size = getChildCount(); 234 for (int i = 0; i < size; i++) { 235 final View view = getChildAt(i); 236 if (view instanceof TextView) { // divider 237 continue; 238 } 239 AttendeeItem attendeeItem = (AttendeeItem) view.getTag(); 240 if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) { 241 return true; 242 } 243 } 244 return false; 245 } 246 247 248 private void addOneAttendee(Attendee attendee) { 249 if (contains(attendee)) { 250 return; 251 } 252 final AttendeeItem item = new AttendeeItem(attendee, -1 /* presence */, mDefaultBadge); 253 final int status = attendee.mStatus; 254 final String name = attendee.mName == null ? "" : attendee.mName; 255 final int index; 256 switch (status) { 257 case Attendees.ATTENDEE_STATUS_ACCEPTED: { 258 final int startIndex = 0; 259 updateDividerViewLabel(mDividerForYes, mEntries[1], mYes + 1); 260 if (mYes == 0) { 261 addView(mDividerForYes, startIndex); 262 } 263 mYes++; 264 index = startIndex + mYes; 265 break; 266 } 267 case Attendees.ATTENDEE_STATUS_DECLINED: { 268 final int startIndex = (mYes == 0 ? 0 : 1 + mYes); 269 updateDividerViewLabel(mDividerForNo, mEntries[3], mNo + 1); 270 if (mNo == 0) { 271 addView(mDividerForNo, startIndex); 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 } 283 mMaybe++; 284 index = startIndex + mMaybe; 285 break; 286 } 287 default: { 288 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) 289 + (mMaybe == 0 ? 0 : 1 + mMaybe); 290 // We delay adding the divider for "No response". 291 index = startIndex + mNoResponse; 292 mNoResponse++; 293 updateDividerViewLabel(mDividerForNoResponse, mEntries[0], mNoResponse); 294 break; 295 } 296 } 297 298 final View view = constructAttendeeView(item); 299 view.setTag(item); 300 addView(view, index); 301 302 // We want "No Response" divider only when 303 // - someone already answered in some way, 304 // - there is attendees not responding yet, and 305 // - divider isn't in the list yet 306 if (mYes + mNo + mMaybe > 0 && mNoResponse > 0 && 307 mDividerForNoResponse.getParent() == null) { 308 final int dividerIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) + 309 (mMaybe == 0 ? 0 : 1 + mMaybe); 310 addView(mDividerForNoResponse, dividerIndex); 311 } 312 313 mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item, 314 CONTACT_DATA_WITH_PRESENCE_URI, PRESENCE_PROJECTION, CONTACT_DATA_SELECTION, 315 new String[] { attendee.mEmail }, null); 316 } 317 318 public void addAttendees(ArrayList<Attendee> attendees) { 319 synchronized (this) { 320 for (final Attendee attendee : attendees) { 321 addOneAttendee(attendee); 322 } 323 } 324 } 325 326 public void addAttendees(HashMap<String, Attendee> attendees) { 327 synchronized (this) { 328 for (final Attendee attendee : attendees.values()) { 329 addOneAttendee(attendee); 330 } 331 } 332 } 333 334 public void addAttendees(String attendees) { 335 final LinkedHashSet<Rfc822Token> addresses = 336 EditEventHelper.getAddressesFromList(attendees, mValidator); 337 synchronized (this) { 338 for (final Rfc822Token address : addresses) { 339 final Attendee attendee = new Attendee(address.getName(), address.getAddress()); 340 if (TextUtils.isEmpty(attendee.mName)) { 341 attendee.mName = attendee.mEmail; 342 } 343 addOneAttendee(attendee); 344 } 345 } 346 } 347 348 /** 349 * Returns true when the attendee at that index is marked as "removed" (the name of 350 * the attendee is shown with a strike through line). 351 */ 352 public boolean isMarkAsRemoved(int index) { 353 final View view = getChildAt(index); 354 if (view instanceof TextView) { // divider 355 return false; 356 } 357 return ((AttendeeItem) view.getTag()).mRemoved; 358 } 359 360 // TODO put this into a Loader for auto-requeries 361 private class PresenceQueryHandler extends AsyncQueryHandler { 362 public PresenceQueryHandler(ContentResolver cr) { 363 super(cr); 364 } 365 366 @Override 367 protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) { 368 if (cursor == null || cookie == null) { 369 if (DEBUG) { 370 Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie); 371 } 372 return; 373 } 374 375 final AttendeeItem item = (AttendeeItem)cookie; 376 try { 377 cursor.moveToPosition(-1); 378 boolean found = false; 379 int contactId = 0; 380 int photoId = 0; 381 int presence = 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 // Take the most available status we can find. 393 presence = Math.max( 394 cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX), presence); 395 396 found = true; 397 if (DEBUG) { 398 Log.d(TAG, 399 "onQueryComplete Id: " + contactId + " PhotoId: " + photoId 400 + " Email: " + email + " updateCount:" + item.mUpdateCounts 401 + " Presence:" + item.mPresence); 402 } 403 } 404 if (found) { 405 item.mPresence = presence; 406 407 if (photoId > 0 && item.mUpdateCounts < queryIndex) { 408 item.mUpdateCounts = queryIndex; 409 final Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, 410 contactId); 411 // Query for this contacts picture 412 ContactsAsyncHelper.retrieveContactPhotoAsync( 413 mContext, item, new Runnable() { 414 public void run() { 415 updateAttendeeView(item); 416 } 417 }, personUri); 418 } 419 } 420 } finally { 421 cursor.close(); 422 } 423 } 424 } 425 426 public Attendee getItem(int index) { 427 final View view = getChildAt(index); 428 if (view instanceof TextView) { // divider 429 return null; 430 } 431 return ((AttendeeItem) view.getTag()).mAttendee; 432 } 433 434 @Override 435 public void onClick(View view) { 436 // Button corresponding to R.id.contact_remove. 437 final AttendeeItem item = (AttendeeItem) view.getTag(); 438 item.mRemoved = !item.mRemoved; 439 updateAttendeeView(item); 440 } 441} 442