| /* Copyright (C) 2010 The Android Open Source Project. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.exchange.provider; |
| |
| import com.android.email.Email; |
| import com.android.email.EmailAddressAdapter; |
| import com.android.email.R; |
| import com.android.email.provider.EmailContent.Account; |
| import com.android.email.provider.EmailContent.HostAuth; |
| |
| import android.app.Activity; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.database.MergeCursor; |
| import android.net.Uri; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ListView; |
| import android.widget.TextView; |
| |
| /** |
| * TODO: Use real format strings, get rid of all hardcoded strings |
| */ |
| public class GalEmailAddressAdapter extends EmailAddressAdapter { |
| // STOPSHIP - DO NOT RELEASE AS 'TRUE' |
| private static final boolean DEBUG_GAL_LOG = true; |
| |
| // Don't run GAL query until there are 3 characters typed |
| private static final int MINIMUM_GAL_CONSTRAINT_LENGTH = 3; |
| |
| private Activity mActivity; |
| private Account mAccount; |
| private boolean mAccountHasGal; |
| private String mAccountEmailDomain; |
| private LayoutInflater mInflater; |
| |
| // Local variables to track status of the search |
| private int mSeparatorDisplayCount; |
| private int mSeparatorTotalCount; |
| |
| public GalEmailAddressAdapter(Activity activity) { |
| super(activity); |
| mActivity = activity; |
| mAccount = null; |
| mAccountHasGal = false; |
| mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| } |
| |
| /** |
| * Set the account ID when known. Not used for generic contacts lookup; Use when |
| * linking lookup to specific account. |
| */ |
| @Override |
| public void setAccount(Account account) { |
| mAccount = account; |
| mAccountHasGal = false; |
| int finalSplit = mAccount.mEmailAddress.lastIndexOf('@'); |
| mAccountEmailDomain = mAccount.mEmailAddress.substring(finalSplit + 1); |
| } |
| |
| /** |
| * Sniff the provided account and if it's EAS, record "mAccounthHasGal". If not, |
| * clear mAccount so we just ignore it. |
| */ |
| private void checkGalAccount(Account account) { |
| HostAuth ha = HostAuth.restoreHostAuthWithId(mActivity, account.mHostAuthKeyRecv); |
| if (ha != null) { |
| if ("eas".equalsIgnoreCase(ha.mProtocol)) { |
| mAccountHasGal = true; |
| return; |
| } |
| } |
| // for any reason, we could not identify a GAL account, so clear mAccount |
| // and we'll never check this again |
| mAccount = null; |
| mAccountHasGal = false; |
| } |
| |
| @Override |
| public Cursor runQueryOnBackgroundThread(final CharSequence constraint) { |
| // One time (and not in the UI thread) - check the account and see if it support GAL |
| // If not, clear it so we never bother again |
| if (mAccount != null && mAccountHasGal == false) { |
| checkGalAccount(mAccount); |
| } |
| |
| // Get the cursor from ContactsProvider, and set up to exit immediately, returning it |
| Cursor contactsCursor = super.runQueryOnBackgroundThread(constraint); |
| // If we don't have a GAL account or we don't have a constraint that's long enough, |
| // just return the raw contactsCursor |
| if (!mAccountHasGal || constraint == null) { |
| return contactsCursor; |
| } |
| final String constraintString = constraint.toString().trim(); |
| if (constraintString.length() < MINIMUM_GAL_CONSTRAINT_LENGTH) { |
| return contactsCursor; |
| } |
| |
| // Strategy for handling dynamic GAL lookup. |
| // 1. Create cursor that we can use now (and update later) |
| // 2. Return it immediately |
| // 3. Spawn a thread that will update the cursor when results arrive or search fails |
| |
| final MatrixCursor matrixCursor = new MatrixCursor(ExchangeProvider.GAL_PROJECTION); |
| final MyMergeCursor mergedResultCursor = |
| new MyMergeCursor(new Cursor[] {contactsCursor, matrixCursor}); |
| mergedResultCursor.setSeparatorPosition(contactsCursor.getCount()); |
| mSeparatorDisplayCount = -1; |
| mSeparatorTotalCount = -1; |
| new Thread(new Runnable() { |
| public void run() { |
| // Uri format is account/constraint |
| Uri galUri = |
| ExchangeProvider.GAL_URI.buildUpon() |
| .appendPath(Long.toString(mAccount.mId)) |
| .appendPath(constraintString).build(); |
| if (DEBUG_GAL_LOG) { |
| Log.d(Email.LOG_TAG, "Query: " + galUri); |
| } |
| // Use ExchangeProvider to get the results of the GAL query |
| final Cursor galCursor = |
| mContentResolver.query(galUri, ExchangeProvider.GAL_PROJECTION, |
| null, null, null); |
| // There are three result cases to handle here. |
| // 1. matrixCursor is closed - this means the UI no longer cares about us |
| // 2. gal cursor is null or empty - remove separator and exit |
| // 3. gal cursor has results - update separator and add results to matrix cursor |
| |
| // Case 1: The merged cursor has already been dropped, (e.g. results superceded) |
| if (mergedResultCursor.isClosed()) { |
| if (DEBUG_GAL_LOG) { |
| Log.d(Email.LOG_TAG, "Drop result (cursor closed, bg thread)"); |
| } |
| return; |
| } |
| |
| // Cases 2 & 3 have UI aspects, so do them in the UI thread |
| mActivity.runOnUiThread(new Runnable() { |
| public void run() { |
| // Case 1: (final re-check): Merged cursor already dropped |
| if (mergedResultCursor.isClosed()) { |
| if (DEBUG_GAL_LOG) { |
| Log.d(Email.LOG_TAG, "Drop result (cursor closed, ui thread)"); |
| } |
| return; |
| } |
| |
| // Case 2: Gal cursor is null or empty |
| if (galCursor == null || galCursor.getCount() == 0) { |
| if (DEBUG_GAL_LOG) { |
| Log.d(Email.LOG_TAG, "Drop empty result"); |
| } |
| mergedResultCursor.setSeparatorPosition(ListView.INVALID_POSITION); |
| GalEmailAddressAdapter.this.notifyDataSetChanged(); |
| return; |
| } |
| |
| // Case 3: Real results |
| galCursor.moveToPosition(-1); |
| while (galCursor.moveToNext()) { |
| MatrixCursor.RowBuilder rb = matrixCursor.newRow(); |
| rb.add(galCursor.getLong(ExchangeProvider.GAL_COLUMN_ID)); |
| rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DISPLAYNAME)); |
| rb.add(galCursor.getString(ExchangeProvider.GAL_COLUMN_DATA)); |
| } |
| // Replace the separator text with "totals" |
| mSeparatorDisplayCount = galCursor.getCount(); |
| mSeparatorTotalCount = |
| galCursor.getExtras().getInt(ExchangeProvider.EXTRAS_TOTAL_RESULTS); |
| // Notify UI that the cursor changed |
| if (DEBUG_GAL_LOG) { |
| Log.d(Email.LOG_TAG, "Notify result, added=" + mSeparatorDisplayCount); |
| } |
| GalEmailAddressAdapter.this.notifyDataSetChanged(); |
| }}); |
| }}).start(); |
| return mergedResultCursor; |
| } |
| |
| /* |
| * The following series of overrides insert the separator between contacts & GAL contacts |
| * TODO: extract most of this into a CursorAdapter superclass, and share with AccountFolderList |
| */ |
| |
| /** |
| * Get the separator position, which is tucked into the cursor to deal with threading. |
| * Result is invalid for any other cursor types (e.g. the raw contacts cursor) |
| */ |
| private int getSeparatorPosition() { |
| Cursor c = this.getCursor(); |
| if (c instanceof MyMergeCursor) { |
| return ((MyMergeCursor)c).getSeparatorPosition(); |
| } else { |
| return ListView.INVALID_POSITION; |
| } |
| } |
| |
| /** |
| * Prevents the separator view from recycling into the other views |
| */ |
| @Override |
| public int getItemViewType(int position) { |
| if (position == getSeparatorPosition()) { |
| return IGNORE_ITEM_VIEW_TYPE; |
| } |
| return super.getItemViewType(position); |
| } |
| |
| /** |
| * Injects the separator view when required |
| */ |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| // The base class's getView() checks for mDataValid at the beginning, but we don't have |
| // to do that, because if the cursor is invalid getCount() returns 0, in which case this |
| // method wouldn't get called. |
| |
| // Handle the separator here - create & bind |
| if (position == getSeparatorPosition()) { |
| View separator; |
| separator = mInflater.inflate(R.layout.recipient_dropdown_separator, parent, false); |
| TextView text1 = (TextView) separator.findViewById(R.id.text1); |
| View progress = separator.findViewById(R.id.progress); |
| // TODO replace this logic with proper formatting |
| if (mSeparatorDisplayCount == -1) { |
| text1.setText("Searching " + mAccountEmailDomain); |
| progress.setVisibility(View.VISIBLE); |
| } else { |
| if (mSeparatorDisplayCount == mSeparatorTotalCount) { |
| text1.setText(mSeparatorDisplayCount + " results from " + mAccountEmailDomain); |
| } else { |
| text1.setText("First " + mSeparatorDisplayCount + " results from " + |
| mAccountEmailDomain); |
| } |
| progress.setVisibility(View.GONE); |
| } |
| return separator; |
| } |
| return super.getView(getRealPosition(position), convertView, parent); |
| } |
| |
| /** |
| * Forces navigation to skip over the separator |
| */ |
| @Override |
| public boolean areAllItemsEnabled() { |
| return false; |
| } |
| |
| /** |
| * Forces navigation to skip over the separator |
| */ |
| @Override |
| public boolean isEnabled(int position) { |
| return position != getSeparatorPosition(); |
| } |
| |
| /** |
| * Adjusts list count to include separator |
| */ |
| @Override |
| public int getCount() { |
| int count = super.getCount(); |
| if (getSeparatorPosition() != ListView.INVALID_POSITION) { |
| // Increment for separator, if we have anything to show. |
| count += 1; |
| } |
| return count; |
| } |
| |
| /** |
| * Converts list position to cursor position |
| */ |
| private int getRealPosition(int pos) { |
| int separatorPosition = getSeparatorPosition(); |
| if (separatorPosition == ListView.INVALID_POSITION) { |
| // No separator, identity map |
| return pos; |
| } else if (pos <= separatorPosition) { |
| // Before or at the separator, identity map |
| return pos; |
| } else { |
| // After the separator, remove 1 from the pos to get the real underlying pos |
| return pos - 1; |
| } |
| } |
| |
| /** |
| * Returns the item using external position numbering (no separator) |
| */ |
| @Override |
| public Object getItem(int pos) { |
| return super.getItem(getRealPosition(pos)); |
| } |
| |
| /** |
| * Returns the item id using external position numbering (no separator) |
| */ |
| @Override |
| public long getItemId(int pos) { |
| if (pos == getSeparatorPosition()) { |
| return View.NO_ID; |
| } |
| return super.getItemId(getRealPosition(pos)); |
| } |
| |
| /** |
| * Lightweight override of MergeCursor. Synchronizes "mClosed" / "isClosed()" so we |
| * can safely check if it has been closed, in the threading jumble of our adapter. |
| * Also holds the separator position, so it can be tracked with the cursor itself and avoid |
| * errors when multiple cursors are in flight. |
| */ |
| private static class MyMergeCursor extends MergeCursor { |
| |
| private int mSeparatorPosition; |
| |
| public MyMergeCursor(Cursor[] cursors) { |
| super(cursors); |
| mClosed = false; |
| mSeparatorPosition = ListView.INVALID_POSITION; |
| } |
| |
| @Override |
| public synchronized void close() { |
| super.close(); |
| if (DEBUG_GAL_LOG) { |
| Log.d(Email.LOG_TAG, "Closing MyMergeCursor"); |
| } |
| } |
| |
| @Override |
| public synchronized boolean isClosed() { |
| return super.isClosed(); |
| } |
| |
| void setSeparatorPosition(int newPos) { |
| mSeparatorPosition = newPos; |
| } |
| |
| int getSeparatorPosition() { |
| return mSeparatorPosition; |
| } |
| } |
| } |