summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSara Ting <sarating@google.com>2012-11-13 13:52:54 -0800
committerSara Ting <sarating@google.com>2012-11-15 16:13:57 -0800
commit4686b4065af0137f5d6ff7570d867fed562c8120 (patch)
tree5ce9e07131065f461ade41fec662394079cea447
parentddc1b123074b7243f7d39071c8c39788e44d9079 (diff)
downloadandroid_packages_apps_Calendar-4686b4065af0137f5d6ff7570d867fed562c8120.tar.gz
android_packages_apps_Calendar-4686b4065af0137f5d6ff7570d867fed562c8120.tar.bz2
android_packages_apps_Calendar-4686b4065af0137f5d6ff7570d867fed562c8120.zip
Autocomplete location in edit-event with recent locations and contacts' names/addresses.
Bug:6636507 Change-Id: I731edfc453acc8c54b47311bae47cd65a607c763
-rw-r--r--res/drawable-hdpi/ic_history_holo_light.pngbin0 -> 749 bytes
-rw-r--r--res/drawable-mdpi/ic_history_holo_light.pngbin0 -> 507 bytes
-rw-r--r--res/drawable-xhdpi/ic_history_holo_light.pngbin0 -> 916 bytes
-rw-r--r--res/layout-sw600dp/edit_event_1.xml2
-rw-r--r--res/layout/edit_event_1.xml2
-rw-r--r--res/layout/location_dropdown_item.xml56
-rw-r--r--src/com/android/calendar/event/EditEventView.java38
-rw-r--r--src/com/android/calendar/event/EventLocationAdapter.java471
8 files changed, 557 insertions, 12 deletions
diff --git a/res/drawable-hdpi/ic_history_holo_light.png b/res/drawable-hdpi/ic_history_holo_light.png
new file mode 100644
index 00000000..d3feeac2
--- /dev/null
+++ b/res/drawable-hdpi/ic_history_holo_light.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_history_holo_light.png b/res/drawable-mdpi/ic_history_holo_light.png
new file mode 100644
index 00000000..2b6eec4f
--- /dev/null
+++ b/res/drawable-mdpi/ic_history_holo_light.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_history_holo_light.png b/res/drawable-xhdpi/ic_history_holo_light.png
new file mode 100644
index 00000000..83d61fa6
--- /dev/null
+++ b/res/drawable-xhdpi/ic_history_holo_light.png
Binary files differ
diff --git a/res/layout-sw600dp/edit_event_1.xml b/res/layout-sw600dp/edit_event_1.xml
index 88ff3b16..f5689f3f 100644
--- a/res/layout-sw600dp/edit_event_1.xml
+++ b/res/layout-sw600dp/edit_event_1.xml
@@ -88,7 +88,7 @@
<TextView
android:text="@string/where_label"
style="@style/TextAppearance.EditEvent_Label" />
- <EditText
+ <AutoCompleteTextView
android:id="@+id/location"
android:singleLine="false"
style="@style/TextAppearance.EditEvent_Value"
diff --git a/res/layout/edit_event_1.xml b/res/layout/edit_event_1.xml
index 48e6c7f8..be04cff5 100644
--- a/res/layout/edit_event_1.xml
+++ b/res/layout/edit_event_1.xml
@@ -89,7 +89,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="6dip"
android:focusable="true">
- <EditText
+ <AutoCompleteTextView
android:id="@+id/location"
android:singleLine="false"
android:layout_height="wrap_content"
diff --git a/res/layout/location_dropdown_item.xml b/res/layout/location_dropdown_item.xml
new file mode 100644
index 00000000..d0fff1bf
--- /dev/null
+++ b/res/layout/location_dropdown_item.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="48dip"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:background="?android:attr/activatedBackgroundIndicator">
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="48dip"
+ android:layout_height="48dip"
+ android:layout_marginLeft="0dip"
+ android:src="@drawable/ic_contact_picture"
+ android:cropToPadding="true"
+ android:scaleType="centerCrop" />
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:layout_weight="1">
+ <TextView android:id="@+id/location_name"
+ android:textColor="@drawable/list_item_font_primary"
+ android:textSize="18sp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dip"
+ android:singleLine="true"
+ android:ellipsize="end" />
+ <TextView android:id="@+id/location_address"
+ android:textColor="@drawable/list_item_font_secondary"
+ android:textSize="14sp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dip"
+ android:singleLine="true"
+ android:ellipsize="end"
+ android:layout_marginTop="-4dip" />
+ </LinearLayout>
+</LinearLayout>
diff --git a/src/com/android/calendar/event/EditEventView.java b/src/com/android/calendar/event/EditEventView.java
index 3ef527e2..0b25ab9b 100644
--- a/src/com/android/calendar/event/EditEventView.java
+++ b/src/com/android/calendar/event/EditEventView.java
@@ -140,7 +140,8 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa
Spinner mAccessLevelSpinner;
RadioGroup mResponseRadioGroup;
AutoCompleteTextView mTitleTextView;
- TextView mLocationTextView;
+ AutoCompleteTextView mLocationTextView;
+ EventLocationAdapter mLocationAdapter;
TextView mDescriptionTextView;
TextView mWhenView;
TextView mTimezoneTextView;
@@ -695,14 +696,16 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa
Cursor c = uniqueTitlesCursor(tempCursor);
// Log the processing duration.
- long duration = System.currentTimeMillis() - startTime;
- StringBuilder msg = new StringBuilder();
- msg.append("Autocomplete of ");
- msg.append(constraint);
- msg.append(": title query match took ");
- msg.append(duration);
- msg.append("ms.");
- Log.d(TAG, msg.toString());
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ long duration = System.currentTimeMillis() - startTime;
+ StringBuilder msg = new StringBuilder();
+ msg.append("Autocomplete of ");
+ msg.append(constraint);
+ msg.append(": title query match took ");
+ msg.append(duration);
+ msg.append("ms.");
+ Log.d(TAG, msg.toString());
+ }
return c;
} finally {
tempCursor.close();
@@ -948,7 +951,7 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa
// cache all the widgets
mCalendarsSpinner = (Spinner) view.findViewById(R.id.calendars_spinner);
mTitleTextView = (AutoCompleteTextView) view.findViewById(R.id.title);
- mLocationTextView = (TextView) view.findViewById(R.id.location);
+ mLocationTextView = (AutoCompleteTextView) view.findViewById(R.id.location);
mDescriptionTextView = (TextView) view.findViewById(R.id.description);
mTimezoneLabel = (TextView) view.findViewById(R.id.timezone_label);
mStartDateButton = (Button) view.findViewById(R.id.start_date);
@@ -995,6 +998,21 @@ public class EditEventView implements View.OnClickListener, DialogInterface.OnCa
});
mLocationTextView.setTag(mLocationTextView.getBackground());
+ mLocationAdapter = new EventLocationAdapter(activity);
+ mLocationTextView.setAdapter(mLocationAdapter);
+ mLocationTextView.setOnEditorActionListener(new OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ // Dismiss the suggestions dropdown. Return false so the other
+ // side effects still occur (soft keyboard going away, etc.).
+ mLocationTextView.dismissDropDown();
+ }
+ return false;
+ }
+ });
+
+
mDescriptionTextView.setTag(mDescriptionTextView.getBackground());
mRepeatsSpinner.setTag(mRepeatsSpinner.getBackground());
mAttendeesList.setTag(mAttendeesList.getBackground());
diff --git a/src/com/android/calendar/event/EventLocationAdapter.java b/src/com/android/calendar/event/EventLocationAdapter.java
new file mode 100644
index 00000000..f2c9a150
--- /dev/null
+++ b/src/com/android/calendar/event/EventLocationAdapter.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2012 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.calendar.event;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.CalendarContract.Events;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.calendar.R;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.concurrent.ExecutionException;
+
+// TODO: limit length of dropdown to stop at the soft keyboard
+// TODO: history icon resize asset
+
+/**
+ * An adapter for autocomplete of the location field in edit-event view.
+ */
+public class EventLocationAdapter extends ArrayAdapter<EventLocationAdapter.Result>
+ implements Filterable {
+ private static final String TAG = "EventLocationAdapter";
+
+ /**
+ * Internal class for containing info for an item in the auto-complete results.
+ */
+ public static class Result {
+ private final String mName;
+ private final String mAddress;
+
+ // The default image resource for the icon. This will be null if there should
+ // be no icon (if multiple listings for a contact, only the first one should have the
+ // photo icon).
+ private final Integer mDefaultIcon;
+
+ // The contact photo to use for the icon. This will override the default icon.
+ private final Uri mContactPhotoUri;
+
+ public Result(String displayName, String address, Integer defaultIcon,
+ Uri contactPhotoUri) {
+ this.mName = displayName;
+ this.mAddress = address;
+ this.mDefaultIcon = defaultIcon;
+ this.mContactPhotoUri = contactPhotoUri;
+ }
+
+ /**
+ * This is the autocompleted text.
+ */
+ @Override
+ public String toString() {
+ return mAddress;
+ }
+ }
+ private static ArrayList<Result> EMPTY_LIST = new ArrayList<Result>();
+
+ // Constants for contacts query:
+ // SELECT ... FROM view_data data WHERE ((data1 LIKE 'input%' OR data1 LIKE '%input%' OR
+ // display_name LIKE 'input%' OR display_name LIKE '%input%' )) ORDER BY display_name ASC
+ private static final String[] CONTACTS_PROJECTION = new String[] {
+ CommonDataKinds.StructuredPostal._ID,
+ Contacts.DISPLAY_NAME,
+ CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS,
+ RawContacts.CONTACT_ID,
+ Contacts.PHOTO_ID,
+ };
+ private static final int CONTACTS_INDEX_ID = 0;
+ private static final int CONTACTS_INDEX_DISPLAY_NAME = 1;
+ private static final int CONTACTS_INDEX_ADDRESS = 2;
+ private static final int CONTACTS_INDEX_CONTACT_ID = 3;
+ private static final int CONTACTS_INDEX_PHOTO_ID = 4;
+ // TODO: Only query visible contacts?
+ private static final String CONTACTS_WHERE = new StringBuilder()
+ .append("(")
+ .append(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)
+ .append(" LIKE ? OR ")
+ .append(CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS)
+ .append(" LIKE ? OR ")
+ .append(Contacts.DISPLAY_NAME)
+ .append(" LIKE ? OR ")
+ .append(Contacts.DISPLAY_NAME)
+ .append(" LIKE ? )")
+ .toString();
+
+ // Constants for recent locations query (in Events table):
+ // SELECT ... FROM view_events WHERE (eventLocation LIKE 'input%') ORDER BY _id DESC
+ private static final String[] EVENT_PROJECTION = new String[] {
+ Events._ID,
+ Events.EVENT_LOCATION,
+ };
+ private static final int EVENT_INDEX_ID = 0;
+ private static final int EVENT_INDEX_LOCATION = 1;
+ private static final String LOCATION_WHERE = Events.EVENT_LOCATION + " LIKE ?";
+ private static final int MAX_LOCATION_SUGGESTIONS = 4;
+
+ private final ContentResolver mResolver;
+ private final LayoutInflater mInflater;
+ private final ArrayList<Result> mResultList = new ArrayList<Result>();
+
+ // The cache for contacts photos. We don't have to worry about clearing this, as a
+ // new adapter is created for every edit event.
+ private final Map<Uri, Bitmap> mPhotoCache = new HashMap<Uri, Bitmap>();
+
+ /**
+ * Constructor.
+ */
+ public EventLocationAdapter(Context context) {
+ super(context, R.layout.location_dropdown_item, EMPTY_LIST);
+
+ mResolver = context.getContentResolver();
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public int getCount() {
+ return mResultList.size();
+ }
+
+ @Override
+ public Result getItem(int index) {
+ if (index < mResultList.size()) {
+ return mResultList.get(index);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = mInflater.inflate(R.layout.location_dropdown_item, parent, false);
+ }
+ final Result result = getItem(position);
+ if (result == null) {
+ return view;
+ }
+
+ // Update the display name in the item in auto-complete list.
+ TextView nameView = (TextView) view.findViewById(R.id.location_name);
+ if (nameView != null) {
+ if (result.mName == null) {
+ nameView.setVisibility(View.GONE);
+ } else {
+ nameView.setVisibility(View.VISIBLE);
+ nameView.setText(result.mName);
+ }
+ }
+
+ // Update the address line.
+ TextView addressView = (TextView) view.findViewById(R.id.location_address);
+ if (addressView != null) {
+ addressView.setText(result.mAddress);
+ }
+
+ // Update the icon.
+ final ImageView imageView = (ImageView) view.findViewById(R.id.icon);
+ if (imageView != null) {
+ if (result.mDefaultIcon == null) {
+ imageView.setVisibility(View.INVISIBLE);
+ } else {
+ imageView.setVisibility(View.VISIBLE);
+ imageView.setImageResource(result.mDefaultIcon);
+
+ // Save the URI on the view, so we can check against it later when updating
+ // the image. Otherwise the async image update with using 'convertView' above
+ // resulted in the wrong list items being updated.
+ imageView.setTag(result.mContactPhotoUri);
+ if (result.mContactPhotoUri != null) {
+ Bitmap cachedPhoto = mPhotoCache.get(result.mContactPhotoUri);
+ if (cachedPhoto != null) {
+ // Use photo in cache.
+ imageView.setImageBitmap(cachedPhoto);
+ } else {
+ // Asynchronously load photo and update.
+ asyncLoadPhotoAndUpdateView(result.mContactPhotoUri, imageView);
+ }
+ }
+ }
+ }
+ return view;
+ }
+
+ // TODO: Refactor to share code with ContactsAsyncHelper.
+ private void asyncLoadPhotoAndUpdateView(final Uri contactPhotoUri,
+ final ImageView imageView) {
+ AsyncTask<Void, Void, Bitmap> photoUpdaterTask =
+ new AsyncTask<Void, Void, Bitmap>() {
+ @Override
+ protected Bitmap doInBackground(Void... params) {
+ Bitmap photo = null;
+ InputStream imageStream = Contacts.openContactPhotoInputStream(
+ mResolver, contactPhotoUri);
+ if (imageStream != null) {
+ photo = BitmapFactory.decodeStream(imageStream);
+ mPhotoCache.put(contactPhotoUri, photo);
+ }
+ return photo;
+ }
+
+ @Override
+ public void onPostExecute(Bitmap photo) {
+ // The View may have already been reused (because using 'convertView' above), so
+ // we must check the URI is as expected before setting the icon, or we may be
+ // setting the icon in other items.
+ if (photo != null && imageView.getTag() == contactPhotoUri) {
+ imageView.setImageBitmap(photo);
+ }
+ }
+ }.execute();
+ }
+
+ /**
+ * Return filter for matching against contacts info and recent locations.
+ */
+ @Override
+ public Filter getFilter() {
+ return new LocationFilter();
+ }
+
+ /**
+ * Filter implementation for matching the input string against contacts info and
+ * recent locations.
+ */
+ public class LocationFilter extends Filter {
+
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ long startTime = System.currentTimeMillis();
+ final String filter = constraint == null ? "" : constraint.toString();
+ if (filter.isEmpty()) {
+ return null;
+ }
+
+ // Start the recent locations query (async).
+ AsyncTask<Void, Void, List<Result>> locationsQueryTask =
+ new AsyncTask<Void, Void, List<Result>>() {
+ @Override
+ protected List<Result> doInBackground(Void... params) {
+ return queryRecentLocations(mResolver, filter);
+ }
+ }.execute();
+
+ // Perform the contacts query (sync).
+ HashSet<String> contactsAddresses = new HashSet<String>();
+ List<Result> contacts = queryContacts(mResolver, filter, contactsAddresses);
+
+ ArrayList<Result> resultList = new ArrayList<Result>();
+ try {
+ // Wait for the locations query.
+ List<Result> recentLocations = locationsQueryTask.get();
+
+ // Add the matched recent locations to returned results. If a match exists in
+ // both the recent locations query and the contacts addresses, only display it
+ // as a contacts match.
+ for (Result recentLocation : recentLocations) {
+ if (recentLocation.mAddress != null &&
+ !contactsAddresses.contains(recentLocation.mAddress)) {
+ resultList.add(recentLocation);
+ }
+ }
+ } catch (ExecutionException e) {
+ Log.e(TAG, "Failed waiting for locations query results.", e);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Failed waiting for locations query results.", e);
+ }
+
+ // Add all the contacts matches to returned results.
+ if (contacts != null) {
+ resultList.addAll(contacts);
+ }
+
+ // Log the processing duration.
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ long duration = System.currentTimeMillis() - startTime;
+ StringBuilder msg = new StringBuilder();
+ msg.append("Autocomplete of ").append(constraint);
+ msg.append(": location query match took ").append(duration).append("ms ");
+ msg.append("(").append(resultList.size()).append(" results)");
+ Log.d(TAG, msg.toString());
+ }
+
+ final FilterResults filterResults = new FilterResults();
+ filterResults.values = resultList;
+ filterResults.count = resultList.size();
+ return filterResults;
+ }
+
+ @Override
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ mResultList.clear();
+ if (results != null && results.count > 0) {
+ mResultList.addAll((ArrayList<Result>) results.values);
+ notifyDataSetChanged();
+ } else {
+ notifyDataSetInvalidated();
+ }
+ }
+ }
+
+ /**
+ * Matches the input string against contacts names and addresses.
+ *
+ * @param resolver The content resolver.
+ * @param input The user-typed input string.
+ * @param addressesRetVal The addresses in the returned result are also returned here
+ * for faster lookup. Pass in an empty set.
+ * @return Ordered list of all the matched results. If there are multiple address matches
+ * for the same contact, they will be listed together in individual items, with only
+ * the first item containing a name/icon.
+ */
+ private static List<Result> queryContacts(ContentResolver resolver, String input,
+ HashSet<String> addressesRetVal) {
+ String where = null;
+ String[] whereArgs = null;
+
+ // Match any word in contact name or address.
+ if (!TextUtils.isEmpty(input)) {
+ where = CONTACTS_WHERE;
+ String param1 = input + "%";
+ String param2 = "% " + input + "%";
+ whereArgs = new String[] {param1, param2, param1, param2};
+ }
+
+ // Perform the query.
+ Cursor c = resolver.query(CommonDataKinds.StructuredPostal.CONTENT_URI,
+ CONTACTS_PROJECTION, where, whereArgs, Contacts.DISPLAY_NAME + " ASC");
+
+ // Process results. Group together addresses for the same contact.
+ try {
+ Map<String, List<Result>> nameToAddresses = new HashMap<String, List<Result>>();
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ String name = c.getString(CONTACTS_INDEX_DISPLAY_NAME);
+ String address = c.getString(CONTACTS_INDEX_ADDRESS);
+ if (name != null) {
+
+ List<Result> addressesForName = nameToAddresses.get(name);
+ Result result;
+ if (addressesForName == null) {
+ // Determine if there is a photo for the icon.
+ Uri contactPhotoUri = null;
+ if (c.getLong(CONTACTS_INDEX_PHOTO_ID) > 0) {
+ contactPhotoUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
+ c.getLong(CONTACTS_INDEX_CONTACT_ID));
+ }
+
+ // First listing for a distinct contact should have the name/icon.
+ addressesForName = new ArrayList<Result>();
+ nameToAddresses.put(name, addressesForName);
+ result = new Result(name, address, R.drawable.ic_contact_picture,
+ contactPhotoUri);
+ } else {
+ // Do not include name/icon in subsequent listings for the same contact.
+ result = new Result(null, address, null, null);
+ }
+
+ addressesForName.add(result);
+ addressesRetVal.add(address);
+ }
+ }
+
+ // Return the list of results.
+ List<Result> allResults = new ArrayList<Result>();
+ for (List<Result> result : nameToAddresses.values()) {
+ allResults.addAll(result);
+ }
+ return allResults;
+
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * Matches the input string against recent locations.
+ */
+ private static List<Result> queryRecentLocations(ContentResolver resolver, String input) {
+ // TODO: also match each word in the address?
+ String filter = input == null ? "" : input + "%";
+ if (filter.isEmpty()) {
+ return null;
+ }
+
+ // Query all locations prefixed with the constraint. There is no way to insert
+ // 'DISTINCT' or 'GROUP BY' to get rid of dupes, so use post-processing to
+ // remove dupes. We will order query results by descending event ID to show
+ // results that were most recently inputed.
+ Cursor c = resolver.query(Events.CONTENT_URI, EVENT_PROJECTION, LOCATION_WHERE,
+ new String[] { filter }, Events._ID + " DESC");
+ try {
+ List<Result> recentLocations = null;
+ if (c != null) {
+ // Post process query results.
+ recentLocations = processLocationsQueryResults(c);
+ }
+ return recentLocations;
+ } finally {
+ if (c != null) {
+ c.close();
+ }
+ }
+ }
+
+ /**
+ * Post-process the query results to return the first MAX_LOCATION_SUGGESTIONS
+ * unique locations in alphabetical order.
+ *
+ * TODO: Refactor to share code with the recent titles auto-complete.
+ */
+ private static List<Result> processLocationsQueryResults(Cursor cursor) {
+ TreeSet<String> locations = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
+ int numColumns = cursor.getColumnCount();
+ cursor.moveToPosition(-1);
+
+ // Remove dupes.
+ while ((locations.size() < MAX_LOCATION_SUGGESTIONS) && cursor.moveToNext()) {
+ String location = cursor.getString(EVENT_INDEX_LOCATION).trim();
+ String data[] = new String[numColumns];
+ locations.add(location);
+ }
+
+ // Copy the sorted results.
+ List<Result> results = new ArrayList<Result>();
+ for (String location : locations) {
+ results.add(new Result(null, location, R.drawable.ic_history_holo_light, null));
+ }
+ return results;
+ }
+}