GAL UI cleanup work (in progress)

Provider:
  * Move UI (e.g. separator) out of provider, create it entirely in adapter
  * Remove timestamp based disambiguation (TODO throttling some other way)
  * Add cursor "extras" to report total # of results on server
UI:
  * Rewrite background-foreground threading to be more efficient and
    signal changes back to UI by calling notifyDataSetChanged())
  * Separator strings per Rich's UI (not using real strings yet)
  * Remove link back to AutoCompleteTextView

NOTE:  There's a bug in ACTV that is causing the window size not to
change properly on notifyDataSetChanged().  Framework team to investigate.

Bug: 2249514
Change-Id: I183b200dcdaa268f0612d2de6442db28e1491d40
This commit is contained in:
Andrew Stadler 2010-03-15 19:17:03 -07:00
parent e2c56fc88c
commit 26b4b47fb3
5 changed files with 331 additions and 170 deletions

View File

@ -14,69 +14,34 @@
limitations under the License.
-->
<!-- TODO: Remove separator, return to original simple layout -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_centerVertical="true">
<RelativeLayout android:id="@+id/address"
android:layout_width="match_parent"
android:layout_height="50dip"
android:orientation="horizontal"
android:layout_height="50dip"
android:orientation="horizontal"
android:layout_centerVertical="true"
android:layout_gravity="center_vertical">
<TextView android:id="@+id/text1"
android:textColor="?android:attr/textColorPrimaryInverse"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_gravity="center_vertical">
<TextView android:id="@+id/text1"
android:textColor="?android:attr/textColorPrimaryInverse"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
<TextView android:id="@+id/text2"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/text1"
android:layout_centerVertical="true"
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
</RelativeLayout>
<RelativeLayout android:id="@+id/status_divider"
android:layout_width="match_parent"
android:layout_height="20dip"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
<TextView android:id="@+id/text2"
android:textColor="?android:attr/textColorSecondaryInverse"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/text1"
android:layout_centerVertical="true"
android:background="#FF777777">
<TextView android:id="@+id/account"
android:textColor="#FFFFFFFF"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
<ProgressBar android:id="@+id/progress"
style="?android:attr/progressBarStyleSmallTitle"
android:minWidth="10dip"
android:paddingRight="4dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
/>
</RelativeLayout>
android:gravity="center_vertical"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
</RelativeLayout>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<!-- TODO: proper style/theme based layout -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="20dip"
android:orientation="horizontal"
android:layout_centerVertical="true"
android:background="#FF777777" >
<TextView android:id="@+id/text1"
android:textColor="#FFFFFFFF"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:paddingLeft="6dip"
android:singleLine="true"
android:ellipsize="end"
/>
<ProgressBar android:id="@+id/progress"
style="?android:attr/progressBarStyleSmallTitle"
android:minWidth="10dip"
android:paddingRight="4dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
/>
</RelativeLayout>

View File

@ -580,9 +580,9 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus
private void setupAddressAdapters() {
/* EXCHANGE-REMOVE-SECTION-START */
if (true) {
mAddressAdapterTo = new GalEmailAddressAdapter(this, mToView);
mAddressAdapterCc = new GalEmailAddressAdapter(this, mCcView);
mAddressAdapterBcc = new GalEmailAddressAdapter(this, mBccView);
mAddressAdapterTo = new GalEmailAddressAdapter(this);
mAddressAdapterCc = new GalEmailAddressAdapter(this);
mAddressAdapterBcc = new GalEmailAddressAdapter(this);
} else {
/* EXCHANGE-REMOVE-SECTION-END */
mAddressAdapterTo = new EmailAddressAdapter(this);

View File

@ -25,6 +25,7 @@ import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
/**
@ -37,16 +38,17 @@ public class ExchangeProvider extends ContentProvider {
public static final String EXCHANGE_AUTHORITY = "com.android.exchange.provider";
public static final Uri GAL_URI = Uri.parse("content://" + EXCHANGE_AUTHORITY + "/gal/");
public static final long GAL_START_ID = 0x1000000L;
private static final int GAL_BASE = 0;
private static final int GAL_FILTER = GAL_BASE;
public static final String[] GAL_PROJECTION = new String[] {"_id", "displayName", "data"};
public static final int GAL_COLUMN_ID = 0;
public static final int GAL_COLUMN_DISPLAYNAME = 1;
public static final int GAL_COLUMN_DATA = 2;
public static final String EXTRAS_TOTAL_RESULTS = "com.android.exchange.provider.TOTAL_RESULTS";
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// We use the time stamp to suppress GAL results for queries that have been superceded by newer
// ones (i.e. we only respond to the most recent query)
public static long sQueryTimeStamp = 0;
static {
// Exchange URI matching table
@ -54,8 +56,7 @@ public class ExchangeProvider extends ContentProvider {
// The URI for GAL lookup contains three user-supplied parameters in the path:
// 1) the account id of the Exchange account
// 2) the constraint (filter) text
// 3) a time stamp for the request
matcher.addURI(EXCHANGE_AUTHORITY, "gal/*/*/*", GAL_FILTER);
matcher.addURI(EXCHANGE_AUTHORITY, "gal/*/*", GAL_FILTER);
}
private static void addGalDataRow(MatrixCursor mc, long id, String name, String address) {
@ -70,42 +71,27 @@ public class ExchangeProvider extends ContentProvider {
case GAL_FILTER:
long accountId = -1;
// Pull out our parameters
MatrixCursor c = new MatrixCursor(GAL_PROJECTION);
MatrixCursorExtras c = new MatrixCursorExtras(GAL_PROJECTION);
String accountIdString = uri.getPathSegments().get(1);
String filter = uri.getPathSegments().get(2);
String time = uri.getPathSegments().get(3);
// Make sure we get a valid time; otherwise throw an exception
// Make sure we get a valid id; otherwise throw an exception
try {
accountId = Long.parseLong(accountIdString);
sQueryTimeStamp = Long.parseLong(time);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Illegal value in URI");
}
// Get results from the Exchange account
long timeStamp = sQueryTimeStamp;
GalResult galResult = EasSyncService.searchGal(getContext(), accountId, filter);
// If we have a more recent query in process, ignore the result
if (timeStamp != sQueryTimeStamp) {
Log.d(TAG, "Ignoring result from query: " + uri);
return null;
} else if (galResult != null) {
// TODO: None of the UI row should be communicated here- use
// cursor metadata or other method.
if (galResult != null) {
int count = galResult.galData.size();
Log.d(TAG, "Query returned " + count + " result(s)");
String header = (count == 0) ? "No results" : (count == 1) ? "1 result" :
count + " results";
if (galResult.total != count) {
header += " (of " + galResult.total + ")";
}
addGalDataRow(c, GAL_START_ID, header, "");
int i = 1;
for (GalData data : galResult.galData) {
// TODO Don't get the constant from Email app...
addGalDataRow(c, data._id | GAL_START_ID + i, data.displayName,
data.emailAddress);
i++;
addGalDataRow(c, data._id, data.displayName, data.emailAddress);
}
// Use cursor side channel to report metadata
final Bundle bundle = new Bundle();
bundle.putInt(EXTRAS_TOTAL_RESULTS, galResult.total);
c.setExtras(bundle);
}
return c;
default:
@ -137,4 +123,26 @@ public class ExchangeProvider extends ContentProvider {
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return -1;
}
/**
* A simple extension to MatrixCursor that supports extras
*/
private static class MatrixCursorExtras extends MatrixCursor {
private Bundle mExtras;
public MatrixCursorExtras(String[] columnNames) {
super(columnNames);
mExtras = null;
}
public void setExtras(Bundle extras) {
mExtras = extras;
}
@Override
public Bundle getExtras() {
return mExtras;
}
}
}

View File

@ -27,28 +27,35 @@ 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.widget.AutoCompleteTextView;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
public class GalEmailAddressAdapter extends EmailAddressAdapter {
private static final String TAG = "GalAdapter";
// Don't run GAL query until there are 3 characters typed
private static final int MINIMUM_GAL_CONSTRAINT_LENGTH = 3;
// Tag in the placeholder
public static final String SEARCHING_TAG = "_SEARCHING_";
Activity mActivity;
AutoCompleteTextView mAutoCompleteTextView;
Account mAccount;
boolean mAccountHasGal;
private Activity mActivity;
private Account mAccount;
private boolean mAccountHasGal;
private String mAccountEmailDomain;
private LayoutInflater mInflater;
public GalEmailAddressAdapter(Activity activity, AutoCompleteTextView actv) {
// Local variables to track status of the search
private int mSeparatorPosition;
private int mSeparatorDisplayCount;
private int mSeparatorTotalCount;
public GalEmailAddressAdapter(Activity activity) {
super(activity);
mActivity = activity;
mAutoCompleteTextView = actv;
mAccount = null;
mAccountHasGal = false;
mSeparatorPosition = ListView.INVALID_POSITION;
mInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
/**
@ -59,16 +66,8 @@ public class GalEmailAddressAdapter extends EmailAddressAdapter {
public void setAccount(Account account) {
mAccount = account;
mAccountHasGal = false;
}
/**
* TODO: This should be the general purpose placeholder
* TODO: String (possibly with account name)
*/
public static Cursor getProgressCursor() {
MatrixCursor c = new MatrixCursor(ExchangeProvider.GAL_PROJECTION);
c.newRow().add(ExchangeProvider.GAL_START_ID).add("Searching ").add(SEARCHING_TAG);
return c;
int finalSplit = mAccount.mEmailAddress.lastIndexOf('@');
mAccountEmailDomain = mAccount.mEmailAddress.substring(finalSplit + 1);
}
/**
@ -97,70 +96,214 @@ public class GalEmailAddressAdapter extends EmailAddressAdapter {
checkGalAccount(mAccount);
}
// Get the cursor from ContactsProvider
Cursor initialCursor = super.runQueryOnBackgroundThread(constraint);
// If we don't have an account or we don't have a constraint that's long enough, just return
if (mAccountHasGal &&
constraint != null && constraint.length() >= MINIMUM_GAL_CONSTRAINT_LENGTH) {
// Note that the "progress" line could (should) be implemented as a header rather than
// as a row in the list. The current implementation is placeholder.
MergeCursor mc =
new MergeCursor(new Cursor[] {initialCursor, getProgressCursor()});
// We need another copy of the original cursor for our MergeCursor
// because changeCursor closes the original!
// TODO: Avoid this - getting the contacts cursor twice is bad news.
// We're probably not handling the filter UI properly.
final Cursor contactsCursor = super.runQueryOnBackgroundThread(constraint);
new Thread(new Runnable() {
public void run() {
// Uri format is account/constraint/timestamp
Uri galUri =
ExchangeProvider.GAL_URI.buildUpon()
.appendPath(Long.toString(mAccount.mId))
.appendPath(constraint.toString())
.appendPath(((Long)System.currentTimeMillis()).toString()).build();
Log.d(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);
// A null cursor means that our query has been superceded by a later one
if (galCursor == null) return;
// We need to change cursors on the UI thread
mActivity.runOnUiThread(new Runnable() {
public void run() {
// Create a new cursor putting together local and GAL results and
// use it in the adapter
MergeCursor mergeCursor =
new MergeCursor(new Cursor[] {contactsCursor, galCursor});
changeCursor(mergeCursor);
// Call AutoCompleteTextView's onFilterComplete method with count
mAutoCompleteTextView.onFilterComplete(mergeCursor.getCount());
}});
}}).start();
return mc;
Cursor contactsCursor = super.runQueryOnBackgroundThread(constraint);
mSeparatorPosition = ListView.INVALID_POSITION;
// 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 || constraint.length() < MINIMUM_GAL_CONSTRAINT_LENGTH) {
return contactsCursor;
}
// Return right away with the ContactsProvider result
return initialCursor;
// 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});
mSeparatorPosition = 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(constraint.toString()).build();
Log.d(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()) {
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()) {
return;
}
// Case 2: Gal cursor is null or empty
if (galCursor == null || galCursor.getCount() == 0) {
mSeparatorPosition = 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
GalEmailAddressAdapter.this.notifyDataSetChanged();
}});
}}).start();
return mergedResultCursor;
}
// TODO - we cannot assume that contacts ID's will not overlap with GAL_START_ID
// need to do this in a different way, based on more direct knowledge of our cursor
/*
* 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
*/
/**
* Prevents the separator view from recycling into the other views
*/
@Override
public final void bindView(View view, Context context, Cursor cursor) {
if (cursor.getLong(ID_INDEX) == ExchangeProvider.GAL_START_ID) {
((TextView)view.findViewById(R.id.account)).setText(cursor.getString(NAME_INDEX));
view.findViewById(R.id.status_divider).setVisibility(View.VISIBLE);
view.findViewById(R.id.address).setVisibility(View.GONE);
view.findViewById(R.id.progress).setVisibility(
cursor.getString(DATA_INDEX).equals(SEARCHING_TAG) ?
View.VISIBLE : View.GONE);
public int getItemViewType(int position) {
if (position == mSeparatorPosition) {
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 == mSeparatorPosition) {
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 " + mSeparatorTotalCount + " 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 != mSeparatorPosition;
}
/**
* Adjusts list count to include separator
*/
@Override
public int getCount() {
int count = super.getCount();
if (mSeparatorPosition != 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) {
if (mSeparatorPosition == ListView.INVALID_POSITION) {
// No separator, identity map
return pos;
} else if (pos <= mSeparatorPosition) {
// Before or at the separator, identity map
return pos;
} else {
((TextView)view.findViewById(R.id.text1)).setText(cursor.getString(NAME_INDEX));
((TextView)view.findViewById(R.id.text2)).setText(cursor.getString(DATA_INDEX));
view.findViewById(R.id.address).setVisibility(View.VISIBLE);
view.findViewById(R.id.status_divider).setVisibility(View.GONE);
// 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 == mSeparatorPosition) {
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.
*/
private static class MyMergeCursor extends MergeCursor {
public MyMergeCursor(Cursor[] cursors) {
super(cursors);
mClosed = false;
}
@Override
public synchronized void close() {
super.close();
}
@Override
public synchronized boolean isClosed() {
return super.isClosed();
}
}
}