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.Utils;
     23 import com.android.calendar.event.EditEventHelper.AttendeeItem;
     24 import com.android.common.Rfc822Validator;
     25 
     26 import android.content.AsyncQueryHandler;
     27 import android.content.ContentResolver;
     28 import android.content.ContentUris;
     29 import android.content.Context;
     30 import android.content.res.Resources;
     31 import android.database.Cursor;
     32 import android.graphics.ColorMatrix;
     33 import android.graphics.ColorMatrixColorFilter;
     34 import android.graphics.Paint;
     35 import android.graphics.drawable.Drawable;
     36 import android.net.Uri;
     37 import android.provider.CalendarContract.Attendees;
     38 import android.provider.ContactsContract.CommonDataKinds.Email;
     39 import android.provider.ContactsContract.CommonDataKinds.Identity;
     40 import android.provider.ContactsContract.Contacts;
     41 import android.provider.ContactsContract.Data;
     42 import android.provider.ContactsContract.RawContacts;
     43 import android.text.TextUtils;
     44 import android.text.util.Rfc822Token;
     45 import android.util.AttributeSet;
     46 import android.util.Log;
     47 import android.view.LayoutInflater;
     48 import android.view.View;
     49 import android.widget.ImageButton;
     50 import android.widget.LinearLayout;
     51 import android.widget.QuickContactBadge;
     52 import android.widget.TextView;
     53 
     54 import java.util.ArrayList;
     55 import java.util.HashMap;
     56 import java.util.LinkedHashSet;
     57 
     58 public 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 EMAIL_PROJECTION_CONTACT_ID_INDEX = 0;
     63     private static final int EMAIL_PROJECTION_CONTACT_LOOKUP_INDEX = 1;
     64     private static final int EMAIL_PROJECTION_PHOTO_ID_INDEX = 2;
     65 
     66     private static final String[] PROJECTION = new String[] {
     67         RawContacts.CONTACT_ID,     // 0
     68         Contacts.LOOKUP_KEY,        // 1
     69         Contacts.PHOTO_ID,          // 2
     70     };
     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 (at) example.com>
     82     // No <-- divider
     83     // example_for_no <exampleno (at) 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     // Cache for loaded photos
    100     HashMap<String, Drawable> mRecycledPhotos;
    101 
    102     public AttendeesView(Context context, AttributeSet attrs) {
    103         super(context, attrs);
    104         mContext = context;
    105         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    106         mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver());
    107 
    108         final Resources resources = context.getResources();
    109         mDefaultBadge = resources.getDrawable(R.drawable.ic_contact_picture);
    110         mNoResponsePhotoAlpha =
    111             resources.getInteger(R.integer.noresponse_attendee_photo_alpha_level);
    112         mDefaultPhotoAlpha = resources.getInteger(R.integer.default_attendee_photo_alpha_level);
    113 
    114         // Create dividers between groups of attendees (accepted, declined, etc...)
    115         mEntries = resources.getTextArray(R.array.response_labels1);
    116         mDividerForYes = constructDividerView(mEntries[1]);
    117         mDividerForNo = constructDividerView(mEntries[3]);
    118         mDividerForMaybe = constructDividerView(mEntries[2]);
    119         mDividerForNoResponse = constructDividerView(mEntries[0]);
    120 
    121         // Create a filter to convert photos of declined attendees to grayscale.
    122         ColorMatrix matrix = new ColorMatrix();
    123         matrix.setSaturation(0);
    124         mGrayscaleFilter = new ColorMatrixColorFilter(matrix);
    125 
    126     }
    127 
    128     // Disable/enable removal of attendings
    129     @Override
    130     public void setEnabled(boolean enabled) {
    131         super.setEnabled(enabled);
    132         int visibility = isEnabled() ? View.VISIBLE : View.GONE;
    133         int count = getChildCount();
    134         for (int i = 0; i < count; i++) {
    135             View child = getChildAt(i);
    136             View minusButton = child.findViewById(R.id.contact_remove);
    137             if (minusButton != null) {
    138                 minusButton.setVisibility(visibility);
    139             }
    140         }
    141     }
    142 
    143     public void setRfc822Validator(Rfc822Validator validator) {
    144         mValidator = validator;
    145     }
    146 
    147     private View constructDividerView(CharSequence label) {
    148         final TextView textView =
    149             (TextView)mInflater.inflate(R.layout.event_info_label, this, false);
    150         textView.setText(label);
    151         textView.setClickable(false);
    152         return textView;
    153     }
    154 
    155     // Add the number of attendees in the specific status (corresponding to the divider) in
    156     // parenthesis next to the label
    157     private void updateDividerViewLabel(View divider, CharSequence label, int count) {
    158         if (count <= 0) {
    159             ((TextView)divider).setText(label);
    160         }
    161         else {
    162             ((TextView)divider).setText(label + " (" + count + ")");
    163         }
    164     }
    165 
    166 
    167     /**
    168      * Inflates a layout for a given attendee view and set up each element in it, and returns
    169      * the constructed View object. The object is also stored in {@link AttendeeItem#mView}.
    170      */
    171     private View constructAttendeeView(AttendeeItem item) {
    172         item.mView = mInflater.inflate(R.layout.contact_item, null);
    173         return updateAttendeeView(item);
    174     }
    175 
    176     /**
    177      * Set up each element in {@link AttendeeItem#mView} using the latest information. View
    178      * object is reused.
    179      */
    180     private View updateAttendeeView(AttendeeItem item) {
    181         final Attendee attendee = item.mAttendee;
    182         final View view = item.mView;
    183         final TextView nameView = (TextView) view.findViewById(R.id.name);
    184         nameView.setText(TextUtils.isEmpty(attendee.mName) ? attendee.mEmail : attendee.mName);
    185         if (item.mRemoved) {
    186             nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags());
    187         } else {
    188             nameView.setPaintFlags((~Paint.STRIKE_THRU_TEXT_FLAG) & nameView.getPaintFlags());
    189         }
    190 
    191         // Set up the Image button even if the view is disabled
    192         // Everything will be ready when the view is enabled later
    193         final ImageButton button = (ImageButton) view.findViewById(R.id.contact_remove);
    194         button.setVisibility(isEnabled() ? View.VISIBLE : View.GONE);
    195         button.setTag(item);
    196         if (item.mRemoved) {
    197             button.setImageResource(R.drawable.ic_menu_add_field_holo_light);
    198             button.setContentDescription(mContext.getString(R.string.accessibility_add_attendee));
    199         } else {
    200             button.setImageResource(R.drawable.ic_menu_remove_field_holo_light);
    201             button.setContentDescription(mContext.
    202                     getString(R.string.accessibility_remove_attendee));
    203         }
    204         button.setOnClickListener(this);
    205 
    206         final QuickContactBadge badgeView = (QuickContactBadge) view.findViewById(R.id.badge);
    207 
    208         Drawable badge = null;
    209         // Search for photo in recycled photos
    210         if (mRecycledPhotos != null) {
    211             badge = mRecycledPhotos.get(item.mAttendee.mEmail);
    212         }
    213         if (badge != null) {
    214             item.mBadge = badge;
    215         }
    216         badgeView.setImageDrawable(item.mBadge);
    217 
    218         if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) {
    219             item.mBadge.setAlpha(mNoResponsePhotoAlpha);
    220         } else {
    221             item.mBadge.setAlpha(mDefaultPhotoAlpha);
    222         }
    223         if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) {
    224             item.mBadge.setColorFilter(mGrayscaleFilter);
    225         } else {
    226             item.mBadge.setColorFilter(null);
    227         }
    228 
    229         // If we know the lookup-uri of the contact, it is a good idea to set this here. This
    230         // allows QuickContact to be started without an extra database lookup. If we don't know
    231         // the lookup uri (yet), we can set Email and QuickContact will lookup once tapped.
    232         if (item.mContactLookupUri != null) {
    233             badgeView.assignContactUri(item.mContactLookupUri);
    234         } else {
    235             badgeView.assignContactFromEmail(item.mAttendee.mEmail, true);
    236         }
    237         badgeView.setMaxHeight(60);
    238 
    239         return view;
    240     }
    241 
    242     public boolean contains(Attendee attendee) {
    243         final int size = getChildCount();
    244         for (int i = 0; i < size; i++) {
    245             final View view = getChildAt(i);
    246             if (view instanceof TextView) { // divider
    247                 continue;
    248             }
    249             AttendeeItem attendeeItem = (AttendeeItem) view.getTag();
    250             if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) {
    251                 return true;
    252             }
    253         }
    254         return false;
    255     }
    256 
    257     public void clearAttendees() {
    258 
    259         // Before clearing the views, save all the badges. The updateAtendeeView will use the saved
    260         // photo instead of the default badge thus prevent switching between the two while the
    261         // most current photo is loaded in the background.
    262         mRecycledPhotos = new HashMap<String, Drawable>  ();
    263         final int size = getChildCount();
    264         for (int i = 0; i < size; i++) {
    265             final View view = getChildAt(i);
    266             if (view instanceof TextView) { // divider
    267                 continue;
    268             }
    269             AttendeeItem attendeeItem = (AttendeeItem) view.getTag();
    270             mRecycledPhotos.put(attendeeItem.mAttendee.mEmail, attendeeItem.mBadge);
    271         }
    272 
    273         removeAllViews();
    274         mYes = 0;
    275         mNo = 0;
    276         mMaybe = 0;
    277         mNoResponse = 0;
    278     }
    279 
    280     private void addOneAttendee(Attendee attendee) {
    281         if (contains(attendee)) {
    282             return;
    283         }
    284         final AttendeeItem item = new AttendeeItem(attendee, mDefaultBadge);
    285         final int status = attendee.mStatus;
    286         final int index;
    287         boolean firstAttendeeInCategory = false;
    288         switch (status) {
    289             case Attendees.ATTENDEE_STATUS_ACCEPTED: {
    290                 final int startIndex = 0;
    291                 updateDividerViewLabel(mDividerForYes, mEntries[1], mYes + 1);
    292                 if (mYes == 0) {
    293                     addView(mDividerForYes, startIndex);
    294                     firstAttendeeInCategory = true;
    295                 }
    296                 mYes++;
    297                 index = startIndex + mYes;
    298                 break;
    299             }
    300             case Attendees.ATTENDEE_STATUS_DECLINED: {
    301                 final int startIndex = (mYes == 0 ? 0 : 1 + mYes);
    302                 updateDividerViewLabel(mDividerForNo, mEntries[3], mNo + 1);
    303                 if (mNo == 0) {
    304                     addView(mDividerForNo, startIndex);
    305                     firstAttendeeInCategory = true;
    306                 }
    307                 mNo++;
    308                 index = startIndex + mNo;
    309                 break;
    310             }
    311             case Attendees.ATTENDEE_STATUS_TENTATIVE: {
    312                 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo);
    313                 updateDividerViewLabel(mDividerForMaybe, mEntries[2], mMaybe + 1);
    314                 if (mMaybe == 0) {
    315                     addView(mDividerForMaybe, startIndex);
    316                     firstAttendeeInCategory = true;
    317                 }
    318                 mMaybe++;
    319                 index = startIndex + mMaybe;
    320                 break;
    321             }
    322             default: {
    323                 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo)
    324                         + (mMaybe == 0 ? 0 : 1 + mMaybe);
    325                 updateDividerViewLabel(mDividerForNoResponse, mEntries[0], mNoResponse + 1);
    326                 if (mNoResponse == 0) {
    327                     addView(mDividerForNoResponse, startIndex);
    328                     firstAttendeeInCategory = true;
    329                 }
    330                 mNoResponse++;
    331                 index = startIndex + mNoResponse;
    332                 break;
    333             }
    334         }
    335 
    336         final View view = constructAttendeeView(item);
    337         view.setTag(item);
    338         addView(view, index);
    339         // Show separator between Attendees
    340         if (!firstAttendeeInCategory) {
    341             View prevItem = getChildAt(index - 1);
    342             if (prevItem != null) {
    343                 View Separator = prevItem.findViewById(R.id.contact_separator);
    344                 if (Separator != null) {
    345                     Separator.setVisibility(View.VISIBLE);
    346                 }
    347             }
    348         }
    349 
    350         Uri uri;
    351         String selection = null;
    352         String[] selectionArgs = null;
    353         if (attendee.mIdentity != null && attendee.mIdNamespace != null) {
    354             // Query by identity + namespace
    355             uri = Data.CONTENT_URI;
    356             selection = Data.MIMETYPE + "=? AND " + Identity.IDENTITY + "=? AND " +
    357                     Identity.NAMESPACE + "=?";
    358             selectionArgs = new String[] {Identity.CONTENT_ITEM_TYPE, attendee.mIdentity,
    359                     attendee.mIdNamespace};
    360         } else {
    361             // Query by email
    362             uri = Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(attendee.mEmail));
    363         }
    364 
    365         mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item, uri, PROJECTION, selection,
    366                 selectionArgs, null);
    367     }
    368 
    369     public void addAttendees(ArrayList<Attendee> attendees) {
    370         synchronized (this) {
    371             for (final Attendee attendee : attendees) {
    372                 addOneAttendee(attendee);
    373             }
    374         }
    375     }
    376 
    377     public void addAttendees(HashMap<String, Attendee> attendees) {
    378         synchronized (this) {
    379             for (final Attendee attendee : attendees.values()) {
    380                 addOneAttendee(attendee);
    381             }
    382         }
    383     }
    384 
    385     public void addAttendees(String attendees) {
    386         final LinkedHashSet<Rfc822Token> addresses =
    387                 EditEventHelper.getAddressesFromList(attendees, mValidator);
    388         synchronized (this) {
    389             for (final Rfc822Token address : addresses) {
    390                 final Attendee attendee = new Attendee(address.getName(), address.getAddress());
    391                 if (TextUtils.isEmpty(attendee.mName)) {
    392                     attendee.mName = attendee.mEmail;
    393                 }
    394                 addOneAttendee(attendee);
    395             }
    396         }
    397     }
    398 
    399     /**
    400      * Returns true when the attendee at that index is marked as "removed" (the name of
    401      * the attendee is shown with a strike through line).
    402      */
    403     public boolean isMarkAsRemoved(int index) {
    404         final View view = getChildAt(index);
    405         if (view instanceof TextView) { // divider
    406             return false;
    407         }
    408         return ((AttendeeItem) view.getTag()).mRemoved;
    409     }
    410 
    411     // TODO put this into a Loader for auto-requeries
    412     private class PresenceQueryHandler extends AsyncQueryHandler {
    413         public PresenceQueryHandler(ContentResolver cr) {
    414             super(cr);
    415         }
    416 
    417         @Override
    418         protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
    419             if (cursor == null || cookie == null) {
    420                 if (DEBUG) {
    421                     Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie);
    422                 }
    423                 return;
    424             }
    425 
    426             final AttendeeItem item = (AttendeeItem)cookie;
    427             try {
    428                 if (item.mUpdateCounts < queryIndex) {
    429                     item.mUpdateCounts = queryIndex;
    430                     if (cursor.moveToFirst()) {
    431                         final long contactId = cursor.getLong(EMAIL_PROJECTION_CONTACT_ID_INDEX);
    432                         final Uri contactUri =
    433                                 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
    434 
    435                         final String lookupKey =
    436                                 cursor.getString(EMAIL_PROJECTION_CONTACT_LOOKUP_INDEX);
    437                         item.mContactLookupUri = Contacts.getLookupUri(contactId, lookupKey);
    438 
    439                         final long photoId = cursor.getLong(EMAIL_PROJECTION_PHOTO_ID_INDEX);
    440                         // If we found a picture, start the async loading
    441                         if (photoId > 0) {
    442                             // Query for this contacts picture
    443                             ContactsAsyncHelper.retrieveContactPhotoAsync(
    444                                     mContext, item, new Runnable() {
    445                                         @Override
    446                                         public void run() {
    447                                             updateAttendeeView(item);
    448                                         }
    449                                     }, contactUri);
    450                         } else {
    451                             // call update view to make sure that the lookup key gets set in
    452                             // the QuickContactBadge
    453                             updateAttendeeView(item);
    454                         }
    455                     } else {
    456                         // Contact not found.  For real emails, keep the QuickContactBadge with
    457                         // its Email address set, so that the user can create a contact by tapping.
    458                         item.mContactLookupUri = null;
    459                         if (!Utils.isValidEmail(item.mAttendee.mEmail)) {
    460                             item.mAttendee.mEmail = null;
    461                             updateAttendeeView(item);
    462                         }
    463                     }
    464                 }
    465             } finally {
    466                 cursor.close();
    467             }
    468         }
    469     }
    470 
    471     public Attendee getItem(int index) {
    472         final View view = getChildAt(index);
    473         if (view instanceof TextView) { // divider
    474             return null;
    475         }
    476         return ((AttendeeItem) view.getTag()).mAttendee;
    477     }
    478 
    479     @Override
    480     public void onClick(View view) {
    481         // Button corresponding to R.id.contact_remove.
    482         final AttendeeItem item = (AttendeeItem) view.getTag();
    483         item.mRemoved = !item.mRemoved;
    484         updateAttendeeView(item);
    485     }
    486 }
    487