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.event.EditEventHelper.AttendeeItem; 23 import com.android.common.Rfc822Validator; 24 25 import android.content.AsyncQueryHandler; 26 import android.content.ContentResolver; 27 import android.content.ContentUris; 28 import android.content.Context; 29 import android.content.res.Resources; 30 import android.database.Cursor; 31 import android.graphics.ColorMatrix; 32 import android.graphics.ColorMatrixColorFilter; 33 import android.graphics.Paint; 34 import android.graphics.drawable.Drawable; 35 import android.net.Uri; 36 import android.provider.CalendarContract.Attendees; 37 import android.provider.ContactsContract.CommonDataKinds.Email; 38 import android.provider.ContactsContract.Contacts; 39 import android.provider.ContactsContract.Data; 40 import android.provider.ContactsContract.StatusUpdates; 41 import android.text.TextUtils; 42 import android.text.util.Rfc822Token; 43 import android.util.AttributeSet; 44 import android.util.Log; 45 import android.view.LayoutInflater; 46 import android.view.View; 47 import android.widget.ImageButton; 48 import android.widget.ImageView; 49 import android.widget.LinearLayout; 50 import android.widget.QuickContactBadge; 51 import android.widget.TextView; 52 53 import java.util.ArrayList; 54 import java.util.HashMap; 55 import java.util.LinkedHashSet; 56 57 public 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_EMAIL_INDEX = 1; 63 private static final int PRESENCE_PROJECTION_PHOTO_ID_INDEX = 2; 64 65 private static final String[] PRESENCE_PROJECTION = new String[] { 66 Email.CONTACT_ID, // 0 67 Email.DATA, // 1 68 Email.PHOTO_ID, // 2 69 }; 70 71 private static final Uri CONTACT_DATA_WITH_PRESENCE_URI = Data.CONTENT_URI; 72 private static final String CONTACT_DATA_SELECTION = Email.DATA + " IN (?)"; 73 74 private final Context mContext; 75 private final LayoutInflater mInflater; 76 private final PresenceQueryHandler mPresenceQueryHandler; 77 private final Drawable mDefaultBadge; 78 private final ColorMatrixColorFilter mGrayscaleFilter; 79 80 // TextView shown at the top of each type of attendees 81 // e.g. 82 // Yes <-- divider 83 // example_for_yes <exampleyes (at) example.com> 84 // No <-- divider 85 // example_for_no <exampleno (at) example.com> 86 private final CharSequence[] mEntries; 87 private final View mDividerForYes; 88 private final View mDividerForNo; 89 private final View mDividerForMaybe; 90 private final View mDividerForNoResponse; 91 private final int mNoResponsePhotoAlpha; 92 private final int mDefaultPhotoAlpha; 93 private Rfc822Validator mValidator; 94 95 // Number of attendees responding or not responding. 96 private int mYes; 97 private int mNo; 98 private int mMaybe; 99 private int mNoResponse; 100 101 public AttendeesView(Context context, AttributeSet attrs) { 102 super(context, attrs); 103 mContext = context; 104 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 105 mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver()); 106 107 final Resources resources = context.getResources(); 108 mDefaultBadge = resources.getDrawable(R.drawable.ic_contact_picture); 109 mNoResponsePhotoAlpha = 110 resources.getInteger(R.integer.noresponse_attendee_photo_alpha_level); 111 mDefaultPhotoAlpha = resources.getInteger(R.integer.default_attendee_photo_alpha_level); 112 113 // Create dividers between groups of attendees (accepted, declined, etc...) 114 mEntries = resources.getTextArray(R.array.response_labels1); 115 mDividerForYes = constructDividerView(mEntries[1]); 116 mDividerForNo = constructDividerView(mEntries[3]); 117 mDividerForMaybe = constructDividerView(mEntries[2]); 118 mDividerForNoResponse = constructDividerView(mEntries[0]); 119 120 // Create a filter to convert photos of declined attendees to grayscale. 121 ColorMatrix matrix = new ColorMatrix(); 122 matrix.setSaturation(0); 123 mGrayscaleFilter = new ColorMatrixColorFilter(matrix); 124 125 } 126 127 // Disable/enable removal of attendings 128 @Override 129 public void setEnabled(boolean enabled) { 130 super.setEnabled(enabled); 131 int visibility = isEnabled() ? View.VISIBLE : View.GONE; 132 int count = getChildCount(); 133 for (int i = 0; i < count; i++) { 134 View child = getChildAt(i); 135 View minusButton = child.findViewById(R.id.contact_remove); 136 if (minusButton != null) { 137 minusButton.setVisibility(visibility); 138 } 139 } 140 } 141 142 public void setRfc822Validator(Rfc822Validator validator) { 143 mValidator = validator; 144 } 145 146 private View constructDividerView(CharSequence label) { 147 final TextView textView = 148 (TextView)mInflater.inflate(R.layout.event_info_label, this, false); 149 textView.setText(label); 150 textView.setClickable(false); 151 return textView; 152 } 153 154 // Add the number of attendees in the specific status (corresponding to the divider) in 155 // parenthesis next to the label 156 private void updateDividerViewLabel(View divider, CharSequence label, int count) { 157 if (count <= 0) { 158 ((TextView)divider).setText(label); 159 } 160 else { 161 ((TextView)divider).setText(label + " (" + count + ")"); 162 } 163 } 164 165 166 /** 167 * Inflates a layout for a given attendee view and set up each element in it, and returns 168 * the constructed View object. The object is also stored in {@link AttendeeItem#mView}. 169 */ 170 private View constructAttendeeView(AttendeeItem item) { 171 item.mView = mInflater.inflate(R.layout.contact_item, null); 172 return updateAttendeeView(item); 173 } 174 175 /** 176 * Set up each element in {@link AttendeeItem#mView} using the latest information. View 177 * object is reused. 178 */ 179 private View updateAttendeeView(AttendeeItem item) { 180 final Attendee attendee = item.mAttendee; 181 final View view = item.mView; 182 final TextView nameView = (TextView) view.findViewById(R.id.name); 183 nameView.setText(TextUtils.isEmpty(attendee.mName) ? attendee.mEmail : attendee.mName); 184 if (item.mRemoved) { 185 nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags()); 186 } else { 187 nameView.setPaintFlags((~Paint.STRIKE_THRU_TEXT_FLAG) & nameView.getPaintFlags()); 188 } 189 190 // Set up the Image button even if the view is disabled 191 // Everything will be ready when the view is enabled later 192 final ImageButton button = (ImageButton) view.findViewById(R.id.contact_remove); 193 button.setVisibility(isEnabled() ? View.VISIBLE : View.GONE); 194 button.setTag(item); 195 if (item.mRemoved) { 196 button.setImageResource(R.drawable.ic_menu_add_field_holo_light); 197 button.setContentDescription(mContext.getString(R.string.accessibility_add_attendee)); 198 } else { 199 button.setImageResource(R.drawable.ic_menu_remove_field_holo_light); 200 button.setContentDescription(mContext. 201 getString(R.string.accessibility_remove_attendee)); 202 } 203 button.setOnClickListener(this); 204 205 final QuickContactBadge badge = (QuickContactBadge) view.findViewById(R.id.badge); 206 if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) { 207 item.mBadge.setAlpha(mNoResponsePhotoAlpha); 208 } else { 209 item.mBadge.setAlpha(mDefaultPhotoAlpha); 210 } 211 if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) { 212 item.mBadge.setColorFilter(mGrayscaleFilter); 213 } else { 214 item.mBadge.setColorFilter(null); 215 } 216 badge.setImageDrawable(item.mBadge); 217 badge.assignContactFromEmail(item.mAttendee.mEmail, true); 218 badge.setMaxHeight(60); 219 220 return view; 221 } 222 223 public boolean contains(Attendee attendee) { 224 final int size = getChildCount(); 225 for (int i = 0; i < size; i++) { 226 final View view = getChildAt(i); 227 if (view instanceof TextView) { // divider 228 continue; 229 } 230 AttendeeItem attendeeItem = (AttendeeItem) view.getTag(); 231 if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) { 232 return true; 233 } 234 } 235 return false; 236 } 237 238 public void clearAttendees() { 239 removeAllViews(); 240 mYes = 0; 241 mNo = 0; 242 mMaybe = 0; 243 mNoResponse = 0; 244 } 245 246 private void addOneAttendee(Attendee attendee) { 247 if (contains(attendee)) { 248 return; 249 } 250 final AttendeeItem item = new AttendeeItem(attendee, 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 while (cursor.moveToNext()) { 385 String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX); 386 int temp = 0; 387 temp = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX); 388 // A photo id must be > 0 and we only care about the contact 389 // ID if there's a photo 390 if (temp > 0) { 391 photoId = temp; 392 contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX); 393 } 394 395 found = true; 396 if (DEBUG) { 397 Log.d(TAG, "onQueryComplete Id: " + contactId + " PhotoId: " + photoId 398 + " Email: " + email + " updateCount:" + item.mUpdateCounts); 399 } 400 } 401 if (found) { 402 403 if (photoId > 0 && item.mUpdateCounts < queryIndex) { 404 item.mUpdateCounts = queryIndex; 405 final Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, 406 contactId); 407 // Query for this contacts picture 408 ContactsAsyncHelper.retrieveContactPhotoAsync( 409 mContext, item, new Runnable() { 410 public void run() { 411 updateAttendeeView(item); 412 } 413 }, personUri); 414 } 415 } 416 } finally { 417 cursor.close(); 418 } 419 } 420 } 421 422 public Attendee getItem(int index) { 423 final View view = getChildAt(index); 424 if (view instanceof TextView) { // divider 425 return null; 426 } 427 return ((AttendeeItem) view.getTag()).mAttendee; 428 } 429 430 @Override 431 public void onClick(View view) { 432 // Button corresponding to R.id.contact_remove. 433 final AttendeeItem item = (AttendeeItem) view.getTag(); 434 item.mRemoved = !item.mRemoved; 435 updateAttendeeView(item); 436 } 437 } 438