Home | History | Annotate | Download | only in event
      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