367 lines
14 KiB
Java
367 lines
14 KiB
Java
/* 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;
|
|
|
|
/**
|
|
* Email Address adapter that performs asynchronous GAL lookups.
|
|
*/
|
|
public class GalEmailAddressAdapter extends EmailAddressAdapter {
|
|
// DO NOT CHECK IN SET TO TRUE
|
|
private static final boolean DEBUG_GAL_LOG = false;
|
|
|
|
// 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);
|
|
String bannerText;
|
|
if (mSeparatorDisplayCount == -1) {
|
|
// Display "Searching <account>..."
|
|
bannerText = mContext.getString(R.string.gal_searching_fmt, mAccountEmailDomain);
|
|
progress.setVisibility(View.VISIBLE);
|
|
} else {
|
|
if (mSeparatorDisplayCount == mSeparatorTotalCount) {
|
|
// Display "x results from <account>"
|
|
bannerText = mContext.getResources().getQuantityString(
|
|
R.plurals.gal_completed_fmt, mSeparatorDisplayCount,
|
|
mSeparatorDisplayCount, mAccountEmailDomain);
|
|
} else {
|
|
// Display "First x results from <account>"
|
|
bannerText = mContext.getString(R.string.gal_completed_limited_fmt,
|
|
mSeparatorDisplayCount, mAccountEmailDomain);
|
|
}
|
|
progress.setVisibility(View.GONE);
|
|
}
|
|
text1.setText(bannerText);
|
|
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;
|
|
}
|
|
}
|
|
}
|