Home | History | Annotate | Download | only in provider
      1 /* Copyright (C) 2010 The Android Open Source Project.
      2  *
      3  * Licensed under the Apache License, Version 2.0 (the "License");
      4  * you may not use this file except in compliance with the License.
      5  * You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software
     10  * distributed under the License is distributed on an "AS IS" BASIS,
     11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12  * See the License for the specific language governing permissions and
     13  * limitations under the License.
     14  */
     15 
     16 package com.android.exchange.provider;
     17 
     18 import com.android.email.Email;
     19 import com.android.email.EmailAddressAdapter;
     20 import com.android.email.R;
     21 import com.android.email.provider.EmailContent.Account;
     22 import com.android.email.provider.EmailContent.HostAuth;
     23 
     24 import android.app.Activity;
     25 import android.content.Context;
     26 import android.database.Cursor;
     27 import android.database.MatrixCursor;
     28 import android.database.MergeCursor;
     29 import android.net.Uri;
     30 import android.util.Log;
     31 import android.view.LayoutInflater;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.widget.ListView;
     35 import android.widget.TextView;
     36 
     37 /**
     38  * Email Address adapter that performs asynchronous GAL lookups.
     39  */
     40 public class GalEmailAddressAdapter extends EmailAddressAdapter {
     41     // DO NOT CHECK IN SET TO TRUE
     42     private static final boolean DEBUG_GAL_LOG = false;
     43 
     44     // Don't run GAL query until there are 3 characters typed
     45     private static final int MINIMUM_GAL_CONSTRAINT_LENGTH = 3;
     46 
     47     private Activity mActivity;
     48     private Account mAccount;
     49     private boolean mAccountHasGal;
     50     private String mAccountEmailDomain;
     51     private LayoutInflater mInflater;
     52 
     53     // Local variables to track status of the search
     54     private int mSeparatorDisplayCount;
     55     private int mSeparatorTotalCount;
     56 
     57     public GalEmailAddressAdapter(Activity activity) {
     58         super(activity);
     59         mActivity = activity;
     60         mAccount = null;
     61         mAccountHasGal = false;
     62         mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     63     }
     64 
     65     /**
     66      * Set the account ID when known.  Not used for generic contacts lookup;  Use when
     67      * linking lookup to specific account.
     68      */
     69     @Override
     70     public void setAccount(Account account) {
     71         mAccount = account;
     72         mAccountHasGal = false;
     73         int finalSplit = mAccount.mEmailAddress.lastIndexOf('@');
     74         mAccountEmailDomain = mAccount.mEmailAddress.substring(finalSplit + 1);
     75     }
     76 
     77     /**
     78      * Sniff the provided account and if it's EAS, record "mAccounthHasGal".  If not,
     79      * clear mAccount so we just ignore it.
     80      */
     81     private void checkGalAccount(Account account) {
     82         HostAuth ha = HostAuth.restoreHostAuthWithId(mActivity, account.mHostAuthKeyRecv);
     83         if (ha != null) {
     84             if ("eas".equalsIgnoreCase(ha.mProtocol)) {
     85                 mAccountHasGal = true;
     86                 return;
     87             }
     88         }
     89         // for any reason, we could not identify a GAL account, so clear mAccount
     90         // and we'll never check this again
     91         mAccount = null;
     92         mAccountHasGal = false;
     93     }
     94 
     95     @Override
     96     public Cursor runQueryOnBackgroundThread(final CharSequence constraint) {
     97         // One time (and not in the UI thread) - check the account and see if it support GAL
     98         // If not, clear it so we never bother again
     99         if (mAccount != null && mAccountHasGal == false) {
    100             checkGalAccount(mAccount);
    101         }
    102 
    103         // Get the cursor from ContactsProvider, and set up to exit immediately, returning it
    104         Cursor contactsCursor = super.runQueryOnBackgroundThread(constraint);
    105         // If we don't have a GAL  account or we don't have a constraint that's long enough,
    106         // just return the raw contactsCursor
    107         if (!mAccountHasGal || constraint == null) {
    108             return contactsCursor;
    109         }
    110         final String constraintString = constraint.toString().trim();
    111         if (constraintString.length() < MINIMUM_GAL_CONSTRAINT_LENGTH) {
    112             return contactsCursor;
    113         }
    114 
    115         // Strategy for handling dynamic GAL lookup.
    116         //  1. Create cursor that we can use now (and update later)
    117         //  2. Return it immediately
    118         //  3. Spawn a thread that will update the cursor when results arrive or search fails
    119 
    120         final MatrixCursor matrixCursor = new MatrixCursor(ExchangeProvider.GAL_PROJECTION);
    121         final MyMergeCursor mergedResultCursor =
    122             new MyMergeCursor(new Cursor[] {contactsCursor, matrixCursor});
    123         mergedResultCursor.setSeparatorPosition(contactsCursor.getCount());
    124         mSeparatorDisplayCount = -1;
    125         mSeparatorTotalCount = -1;
    126         new Thread(new Runnable() {
    127             public void run() {
    128                 // Uri format is account/constraint
    129                 Uri galUri =
    130                     ExchangeProvider.GAL_URI.buildUpon()
    131                         .appendPath(Long.toString(mAccount.mId))
    132                         .appendPath(constraintString).build();
    133                 if (DEBUG_GAL_LOG) {
    134                     Log.d(Email.LOG_TAG, "Query: " + galUri);
    135                 }
    136                 // Use ExchangeProvider to get the results of the GAL query
    137                 final Cursor galCursor =
    138                     mContentResolver.query(galUri, ExchangeProvider.GAL_PROJECTION,
    139                             null, null, null);
    140                 // There are three result cases to handle here.
    141                 //  1. matrixCursor is closed - this means the UI no longer cares about us
    142                 //  2. gal cursor is null or empty - remove separator and exit
    143                 //  3. gal cursor has results - update separator and add results to matrix cursor
    144 
    145                 // Case 1: The merged cursor has already been dropped, (e.g. results superceded)
    146                 if (mergedResultCursor.isClosed()) {
    147                     if (DEBUG_GAL_LOG) {
    148                         Log.d(Email.LOG_TAG, "Drop result (cursor closed, bg thread)");
    149                     }
    150                     return;
    151                 }
    152 
    153                 // Cases 2 & 3 have UI aspects, so do them in the UI thread
    154                 mActivity.runOnUiThread(new Runnable() {
    155                     public void run() {
    156                         // Case 1:  (final re-check):  Merged cursor already dropped
    157                         if (mergedResultCursor.isClosed()) {
    158                             if (DEBUG_GAL_LOG) {
    159                                 Log.d(Email.LOG_TAG, "Drop result (cursor closed, ui thread)");
    160                             }
    161                             return;
    162                         }
    163 
    164                         // Case 2:  Gal cursor is null or empty
    165                         if (galCursor == null || galCursor.getCount() == 0) {
    166                             if (DEBUG_GAL_LOG) {
    167                                 Log.d(Email.LOG_TAG, "Drop empty result");
    168                             }
    169                             mergedResultCursor.setSeparatorPosition(ListView.INVALID_POSITION);
    170                             GalEmailAddressAdapter.this.notifyDataSetChanged();
    171                             return;
    172                         }
    173 
    174                         // Case 3: Real results
    175                         galCursor.moveToPosition(-1);
    176                         while (galCursor.moveToNext()) {
    177                             MatrixCursor.RowBuilder rb = matrixCursor.newRow();
    178                             rb.add(galCursor.getLong(ExchangeProvider.GAL_COLUMN_ID));
    179                             rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DISPLAYNAME));
    180                             rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DATA));
    181                         }
    182                         // Replace the separator text with "totals"
    183                         mSeparatorDisplayCount = galCursor.getCount();
    184                         mSeparatorTotalCount =
    185                             galCursor.getExtras().getInt(ExchangeProvider.EXTRAS_TOTAL_RESULTS);
    186                         // Notify UI that the cursor changed
    187                         if (DEBUG_GAL_LOG) {
    188                             Log.d(Email.LOG_TAG, "Notify result, added=" + mSeparatorDisplayCount);
    189                         }
    190                         GalEmailAddressAdapter.this.notifyDataSetChanged();
    191                     }});
    192             }}).start();
    193         return mergedResultCursor;
    194     }
    195 
    196     /*
    197      * The following series of overrides insert the separator between contacts & GAL contacts
    198      * TODO: extract most of this into a CursorAdapter superclass, and share with AccountFolderList
    199      */
    200 
    201     /**
    202      * Get the separator position, which is tucked into the cursor to deal with threading.
    203      * Result is invalid for any other cursor types (e.g. the raw contacts cursor)
    204      */
    205     private int getSeparatorPosition() {
    206         Cursor c = this.getCursor();
    207         if (c instanceof MyMergeCursor) {
    208             return ((MyMergeCursor)c).getSeparatorPosition();
    209         } else {
    210             return ListView.INVALID_POSITION;
    211         }
    212     }
    213 
    214     /**
    215      * Prevents the separator view from recycling into the other views
    216      */
    217     @Override
    218     public int getItemViewType(int position) {
    219         if (position == getSeparatorPosition()) {
    220             return IGNORE_ITEM_VIEW_TYPE;
    221         }
    222         return super.getItemViewType(position);
    223     }
    224 
    225     /**
    226      * Injects the separator view when required
    227      */
    228     @Override
    229     public View getView(int position, View convertView, ViewGroup parent) {
    230         // The base class's getView() checks for mDataValid at the beginning, but we don't have
    231         // to do that, because if the cursor is invalid getCount() returns 0, in which case this
    232         // method wouldn't get called.
    233 
    234         // Handle the separator here - create & bind
    235         if (position == getSeparatorPosition()) {
    236             View separator;
    237             separator = mInflater.inflate(R.layout.recipient_dropdown_separator, parent, false);
    238             TextView text1 = (TextView) separator.findViewById(R.id.text1);
    239             View progress = separator.findViewById(R.id.progress);
    240             String bannerText;
    241             if (mSeparatorDisplayCount == -1) {
    242                 // Display "Searching <account>..."
    243                 bannerText = mContext.getString(R.string.gal_searching_fmt, mAccountEmailDomain);
    244                 progress.setVisibility(View.VISIBLE);
    245             } else {
    246                 if (mSeparatorDisplayCount == mSeparatorTotalCount) {
    247                     // Display "x results from <account>"
    248                     bannerText = mContext.getResources().getQuantityString(
    249                             R.plurals.gal_completed_fmt, mSeparatorDisplayCount,
    250                             mSeparatorDisplayCount, mAccountEmailDomain);
    251                 } else {
    252                     // Display "First x results from <account>"
    253                     bannerText = mContext.getString(R.string.gal_completed_limited_fmt,
    254                             mSeparatorDisplayCount, mAccountEmailDomain);
    255                 }
    256                 progress.setVisibility(View.GONE);
    257             }
    258             text1.setText(bannerText);
    259             return separator;
    260         }
    261         return super.getView(getRealPosition(position), convertView, parent);
    262     }
    263 
    264     /**
    265      * Forces navigation to skip over the separator
    266      */
    267     @Override
    268     public boolean areAllItemsEnabled() {
    269         return false;
    270     }
    271 
    272     /**
    273      * Forces navigation to skip over the separator
    274      */
    275     @Override
    276     public boolean isEnabled(int position) {
    277         return position != getSeparatorPosition();
    278     }
    279 
    280     /**
    281      * Adjusts list count to include separator
    282      */
    283     @Override
    284     public int getCount() {
    285         int count = super.getCount();
    286         if (getSeparatorPosition() != ListView.INVALID_POSITION) {
    287             // Increment for separator, if we have anything to show.
    288             count += 1;
    289         }
    290         return count;
    291     }
    292 
    293     /**
    294      * Converts list position to cursor position
    295      */
    296     private int getRealPosition(int pos) {
    297         int separatorPosition = getSeparatorPosition();
    298         if (separatorPosition == ListView.INVALID_POSITION) {
    299             // No separator, identity map
    300             return pos;
    301         } else if (pos <= separatorPosition) {
    302             // Before or at the separator, identity map
    303             return pos;
    304         } else {
    305             // After the separator, remove 1 from the pos to get the real underlying pos
    306             return pos - 1;
    307         }
    308     }
    309 
    310     /**
    311      * Returns the item using external position numbering (no separator)
    312      */
    313     @Override
    314     public Object getItem(int pos) {
    315         return super.getItem(getRealPosition(pos));
    316     }
    317 
    318     /**
    319      * Returns the item id using external position numbering (no separator)
    320      */
    321     @Override
    322     public long getItemId(int pos) {
    323         if (pos == getSeparatorPosition()) {
    324             return View.NO_ID;
    325         }
    326         return super.getItemId(getRealPosition(pos));
    327     }
    328 
    329     /**
    330      * Lightweight override of MergeCursor.  Synchronizes "mClosed" / "isClosed()" so we
    331      * can safely check if it has been closed, in the threading jumble of our adapter.
    332      * Also holds the separator position, so it can be tracked with the cursor itself and avoid
    333      * errors when multiple cursors are in flight.
    334      */
    335     private static class MyMergeCursor extends MergeCursor {
    336 
    337         private int mSeparatorPosition;
    338 
    339         public MyMergeCursor(Cursor[] cursors) {
    340             super(cursors);
    341             mClosed = false;
    342             mSeparatorPosition = ListView.INVALID_POSITION;
    343         }
    344 
    345         @Override
    346         public synchronized void close() {
    347             super.close();
    348             if (DEBUG_GAL_LOG) {
    349                 Log.d(Email.LOG_TAG, "Closing MyMergeCursor");
    350             }
    351         }
    352 
    353         @Override
    354         public synchronized boolean isClosed() {
    355             return super.isClosed();
    356         }
    357 
    358         void setSeparatorPosition(int newPos) {
    359             mSeparatorPosition = newPos;
    360         }
    361 
    362         int getSeparatorPosition() {
    363             return mSeparatorPosition;
    364         }
    365     }
    366 }
    367