Added new display groups UI from spec.
Instead of only displaying a single group, we allow the
user to toggle the visibility of all available groups, and
the backend provider takes care of the visibility logic to
keep our list-of-contacts cursor spiffy.
This UI has some limitations and is just a first revision,
for example you can't display contacts from a provider that
aren't part of a group, other than picking the gloal "all
contacts" option. Also, filtering to "only with phones" can
be confusing to users, need to iterate.
The group list is a custom ExpandableListAdapter that walks
the summary cursor, splitting each source package into its
own expandable group. There is some fancy work done so this
only requires one cursor, so it scales nicely.
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 438133e..482f671 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -16,6 +16,8 @@
package com.android.contacts;
+import com.android.contacts.DisplayGroupsActivity.Prefs;
+
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ListActivity;
@@ -83,8 +85,8 @@
/**
* Displays a list of contacts. Usually is embedded into the ContactsActivity.
*/
-public final class ContactsListActivity extends ListActivity
- implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener {
+public final class ContactsListActivity extends ListActivity implements
+ View.OnCreateContextMenuListener {
private static final String TAG = "ContactsListActivity";
private static final String LIST_STATE_KEY = "liststate";
@@ -106,6 +108,7 @@
private static final int SUBACTIVITY_NEW_CONTACT = 1;
private static final int SUBACTIVITY_VIEW_CONTACT = 2;
+ private static final int SUBACTIVITY_DISPLAY_GROUP = 3;
/**
* The action for the join contact activity.
@@ -140,12 +143,10 @@
/** Unknown mode */
static final int MODE_UNKNOWN = 0;
-// /** Show members of the "Contacts" group */
-// static final int MODE_GROUP = 5;
- /** Show all contacts sorted alphabetically */
- static final int MODE_ALL_CONTACTS = 10;
- /** Show all contacts with phone numbers, sorted alphabetically */
- static final int MODE_WITH_PHONES = 15;
+ /** Default mode */
+ static final int MODE_DEFAULT = 4;
+ /** Custom mode */
+ static final int MODE_CUSTOM = 8;
/** Show all starred contacts */
static final int MODE_STARRED = 20;
/** Show frequently contacted contacts */
@@ -173,35 +174,9 @@
static final int MODE_JOIN_AGGREGATE = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
| MODE_MASK_NO_DATA;
- static final int DEFAULT_MODE = MODE_ALL_CONTACTS;
-
/** Maximum number of suggestions shown for joining aggregates */
static final int MAX_SUGGESTIONS = 4;
- /**
- * The type of data to display in the main contacts list.
- */
- static final String PREF_DISPLAY_TYPE = "display_system_group";
-
- /** Unknown display type. */
- static final int DISPLAY_TYPE_UNKNOWN = -1;
- /** Display all contacts */
- static final int DISPLAY_TYPE_ALL = 0;
- /** Display all contacts that have phone numbers */
- static final int DISPLAY_TYPE_ALL_WITH_PHONES = 1;
- /** Display a system group */
- static final int DISPLAY_TYPE_SYSTEM_GROUP = 2;
- /** Display a user group */
- static final int DISPLAY_TYPE_USER_GROUP = 3;
-
- /**
- * Info about what to display. If {@link #PREF_DISPLAY_TYPE}
- * is {@link #DISPLAY_TYPE_SYSTEM_GROUP} then this will be the system id.
- * If {@link #PREF_DISPLAY_TYPE} is {@link #DISPLAY_TYPE_USER_GROUP} then this will
- * be the group name.
- */
- static final String PREF_DISPLAY_INFO = "display_group";
-
static final String NAME_COLUMN = Aggregates.DISPLAY_NAME;
//static final String SORT_STRING = People.SORT_STRING;
@@ -260,44 +235,22 @@
static final int POSTAL_ADDRESS_COLUMN_INDEX = 3;
static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4;
- static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS = 0;
- static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES = 1;
- static final int DISPLAY_GROUP_INDEX_MY_CONTACTS = 2;
-
private static final int QUERY_TOKEN = 42;
/*
- static final String[] GROUPS_PROJECTION = new String[] {
- Groups.SYSTEM_ID, // 0
- Groups.NAME, // 1
- };
- static final int GROUPS_COLUMN_INDEX_SYSTEM_ID = 0;
- static final int GROUPS_COLUMN_INDEX_NAME = 1;
*/
-
- static final String GROUP_WITH_PHONES = "android_smartgroup_phone";
-
ContactItemListAdapter mAdapter;
- int mMode = DEFAULT_MODE;
- // The current display group
- private String mDisplayInfo;
- private int mDisplayType;
- // The current list of display groups, during selection from menu
- private CharSequence[] mDisplayGroups;
- // If true position 2 in mDisplayGroups is the MyContacts group
- private boolean mDisplayGroupsIncludesMyContacts = false;
-
- private int mDisplayGroupOriginalSelection;
- private int mDisplayGroupCurrentSelection;
+ int mMode = MODE_DEFAULT;
private QueryHandler mQueryHandler;
private String mQuery;
- private Uri mGroupFilterUri;
- private Uri mGroupUri;
private boolean mJustCreated;
private boolean mSyncEnabled;
+ private boolean mDisplayAll;
+ private boolean mDisplayOnlyPhones;
+
/**
* Cursor row index that holds reference back to {@link People#_ID}, such as
* {@link ContactMethods#PERSON_ID}. Used when responding to a
@@ -314,7 +267,6 @@
private boolean mListHasFocus;
private boolean mCreateShortcut;
- private boolean mDefaultMode = false;
/**
* Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
@@ -331,6 +283,9 @@
*/
private String mQueryData;
+ private static final String CLAUSE_ONLY_VISIBLE = Aggregates.IN_VISIBLE_GROUP + "=1";
+ private static final String CLAUSE_ONLY_PHONES = Aggregates.PRIMARY_PHONE_ID + " IS NOT NULL";
+
private class DeleteClickListener implements DialogInterface.OnClickListener {
private Uri mUri;
@@ -363,7 +318,7 @@
Log.i(TAG, "Called with action: " + action);
if (UI.LIST_DEFAULT.equals(action)) {
- mDefaultMode = true;
+ mMode = MODE_DEFAULT;
// When mDefaultMode is true the mode is set in onResume(), since the preferneces
// activity may change it whenever this activity isn't running
} /* else if (UI.LIST_GROUP_ACTION.equals(action)) {
@@ -375,7 +330,9 @@
}
buildUserGroupUris(groupName);
}*/ else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
- mMode = MODE_ALL_CONTACTS;
+ mMode = MODE_CUSTOM;
+ mDisplayAll = true;
+ mDisplayOnlyPhones = false;
} else if (UI.LIST_STARRED_ACTION.equals(action)) {
mMode = MODE_STARRED;
} else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
@@ -383,7 +340,9 @@
} else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
mMode = MODE_STREQUENT;
} else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
- mMode = MODE_WITH_PHONES;
+ mMode = MODE_CUSTOM;
+ mDisplayAll = true;
+ mDisplayOnlyPhones = true;
} else if (Intent.ACTION_PICK.equals(action)) {
// XXX These should be showing the data from the URI given in
// the Intent.
@@ -481,7 +440,7 @@
}
if (mMode == MODE_UNKNOWN) {
- mMode = DEFAULT_MODE;
+ mMode = MODE_DEFAULT;
}
// Setup the UI
@@ -542,124 +501,32 @@
TextView empty = (TextView) findViewById(R.id.emptyText);
// Center the text by default
int gravity = Gravity.CENTER;
- switch (mMode) {/*
- case MODE_GROUP:
- if (Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
- if (mSyncEnabled) {
- empty.setText(getText(R.string.noContactsHelpTextWithSync));
- } else {
- empty.setText(getText(R.string.noContactsHelpText));
- }
- gravity = Gravity.NO_GRAVITY;
- } else {
- empty.setText(getString(R.string.groupEmpty, mDisplayInfo));
- }
- break;
- */
- case MODE_STARRED:
- case MODE_STREQUENT:
- case MODE_FREQUENT:
- empty.setText(getText(R.string.noFavorites));
- break;
- case MODE_WITH_PHONES:
- empty.setText(getText(R.string.noContactsWithPhoneNumbers));
- break;
- default:
- empty.setText(getText(R.string.noContacts));
- break;
+ if (mDisplayOnlyPhones) {
+ empty.setText(getText(R.string.noContactsWithPhoneNumbers));
+ } else if (mDisplayAll) {
+ empty.setText(getText(R.string.noContacts));
+ } else {
+ if (mSyncEnabled) {
+ empty.setText(getText(R.string.noContactsHelpTextWithSync));
+ } else {
+ empty.setText(getText(R.string.noContactsHelpText));
+ }
+ gravity = Gravity.NO_GRAVITY;
}
empty.setGravity(gravity);
}
/**
- * Builds the URIs to query when displaying a user group
- *
- * @param groupName the group being displayed
- */
- private void buildUserGroupUris(String groupName) {
- mGroupFilterUri = Uri.parse("content://contacts/groups/name/" + groupName
- + "/members/filter/");
- mGroupUri = Uri.parse("content://contacts/groups/name/" + groupName + "/members");
- }
-
- /**
- * Builds the URIs to query when displaying a system group
- *
- * @param systemId the system group's ID
- */
- private void buildSystemGroupUris(String systemId) {
- mGroupFilterUri = Uri.parse("content://contacts/groups/system_id/" + systemId
- + "/members/filter/");
- mGroupUri = Uri.parse("content://contacts/groups/system_id/" + systemId + "/members");
- }
-
- /**
* Sets the mode when the request is for "default"
*/
private void setDefaultMode() {
// Load the preferences
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- // Lookup the group to display
- mDisplayType = prefs.getInt(PREF_DISPLAY_TYPE, DISPLAY_TYPE_UNKNOWN);
- switch (mDisplayType) {
- case DISPLAY_TYPE_ALL_WITH_PHONES: {
- mMode = MODE_WITH_PHONES;
- mDisplayInfo = null;
- break;
- }
-
- /*case DISPLAY_TYPE_SYSTEM_GROUP: {
- String systemId = prefs.getString(
- PREF_DISPLAY_INFO, null);
- if (!TextUtils.isEmpty(systemId)) {
- // Display the selected system group
- mMode = MODE_GROUP;
- buildSystemGroupUris(systemId);
- mDisplayInfo = systemId;
- } else {
- // No valid group is present, display everything
- mMode = MODE_WITH_PHONES;
- mDisplayInfo = null;
- mDisplayType = DISPLAY_TYPE_ALL;
- }
- break;
- }
-
- case DISPLAY_TYPE_USER_GROUP: {
- String displayGroup = prefs.getString(
- PREF_DISPLAY_INFO, null);
- if (!TextUtils.isEmpty(displayGroup)) {
- // Display the selected user group
- mMode = MODE_GROUP;
- buildUserGroupUris(displayGroup);
- mDisplayInfo = displayGroup;
- } else {
- // No valid group is present, display everything
- mMode = MODE_WITH_PHONES;
- mDisplayInfo = null;
- mDisplayType = DISPLAY_TYPE_ALL;
- }
- break;
- } */
-
- case DISPLAY_TYPE_ALL:
- default: {
- mMode = MODE_ALL_CONTACTS;
- mDisplayInfo = null;
- break;
- }
-
- /* default: {
- // We don't know what to display, default to My Contacts
- mMode = MODE_GROUP;
- mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
- buildSystemGroupUris(Groups.GROUP_MY_CONTACTS);
- mDisplayInfo = Groups.GROUP_MY_CONTACTS;
- break;
- } */
- }
+ mDisplayAll = prefs.getBoolean(Prefs.DISPLAY_ALL, Prefs.DISPLAY_ALL_DEFAULT);
+ mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
+ Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
// Update the empty text view with the proper string, as the group may have changed
setEmptyText();
@@ -674,7 +541,7 @@
// Do this before setting the filter. The filter thread relies
// on some state that is initialized in setDefaultMode
- if (mDefaultMode) {
+ if (mMode == MODE_DEFAULT) {
// If we're in default mode we need to possibly reset the mode due to a change
// in the preferences activity while we weren't running
setDefaultMode();
@@ -715,15 +582,6 @@
}
}
- private void updateGroup() {
- if (mDefaultMode) {
- setDefaultMode();
- }
-
- // Calling requery here may cause an ANR, so always do the async query
- startQuery();
- }
-
@Override
protected void onSaveInstanceState(Bundle icicle) {
super.onSaveInstanceState(icicle);
@@ -779,7 +637,7 @@
*/
// Display group
- if (mDefaultMode) {
+ if (mMode == MODE_DEFAULT) {
menu.add(0, MENU_DISPLAY_GROUP, 0, R.string.menu_displayGroup)
.setIcon(com.android.internal.R.drawable.ic_menu_allfriends);
}
@@ -804,61 +662,13 @@
return super.onCreateOptionsMenu(menu);
}
- /*
- * Implements the handler for display group selection.
- */
- public void onClick(DialogInterface dialogInterface, int which) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- // The OK button was pressed
- if (mDisplayGroupOriginalSelection != mDisplayGroupCurrentSelection) {
- // Set the group to display
- if (mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_ALL_CONTACTS) {
- // Display all
- mDisplayType = DISPLAY_TYPE_ALL;
- mDisplayInfo = null;
- } else if (mDisplayGroupCurrentSelection
- == DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES) {
- // Display all with phone numbers
- mDisplayType = DISPLAY_TYPE_ALL_WITH_PHONES;
- mDisplayInfo = null;
- } /*else if (mDisplayGroupsIncludesMyContacts &&
- mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_MY_CONTACTS) {
- mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
- mDisplayInfo = Groups.GROUP_MY_CONTACTS;
- } */else {
- mDisplayType = DISPLAY_TYPE_USER_GROUP;
- mDisplayInfo = mDisplayGroups[mDisplayGroupCurrentSelection].toString();
- }
-
- // Save the changes to the preferences
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- prefs.edit()
- .putInt(PREF_DISPLAY_TYPE, mDisplayType)
- .putString(PREF_DISPLAY_INFO, mDisplayInfo)
- .commit();
-
- // Update the display state
- updateGroup();
- }
- } else {
- // A list item was selected, cache the position
- mDisplayGroupCurrentSelection = which;
- }
- }
-
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
- /*case MENU_DISPLAY_GROUP:
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.select_group_title)
- .setPositiveButton(android.R.string.ok, this)
- .setNegativeButton(android.R.string.cancel, null);
-
- setGroupEntries(builder);
-
- builder.show();
- return true;*/
+ case MENU_DISPLAY_GROUP:
+ final Intent intent = new Intent(this, DisplayGroupsActivity.class);
+ startActivityForResult(intent, SUBACTIVITY_DISPLAY_GROUP);
+ return true;
case MENU_SEARCH:
startSearch(null, false, null, false);
@@ -884,6 +694,11 @@
mAdapter.notifyDataSetChanged();
}
break;
+
+ case SUBACTIVITY_DISPLAY_GROUP:
+ // Mark as just created so we re-run the view query
+ mJustCreated = true;
+ break;
}
}
@@ -1128,25 +943,15 @@
String[] getProjection() {
switch (mMode) {
- /* case MODE_GROUP: */
- case MODE_ALL_CONTACTS:
- case MODE_WITH_PHONES:
- case MODE_PICK_AGGREGATE:
- case MODE_PICK_OR_CREATE_AGGREGATE:
- case MODE_QUERY:
- case MODE_STARRED:
- case MODE_FREQUENT:
- case MODE_STREQUENT:
- case MODE_INSERT_OR_EDIT_CONTACT:
- return AGGREGATES_SUMMARY_PROJECTION;
-
case MODE_PICK_PHONE:
return PHONES_PROJECTION;
case MODE_PICK_POSTAL:
return POSTALS_PROJECTION;
}
- return null;
+
+ // Default to normal aggregate projection
+ return AGGREGATES_SUMMARY_PROJECTION;
}
private Bitmap loadContactPhoto(long dataId, BitmapFactory.Options options) {
@@ -1166,6 +971,21 @@
return bm;
}
+ /**
+ * Return the selection arguments for a default query based on
+ * {@link #mDisplayAll} and {@link #mDisplayOnlyPhones} flags.
+ */
+ private String getAggregateSelection() {
+ if (!mDisplayAll && mDisplayOnlyPhones) {
+ return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES;
+ } else if (!mDisplayAll) {
+ return CLAUSE_ONLY_VISIBLE;
+ } else if (mDisplayOnlyPhones) {
+ return CLAUSE_ONLY_PHONES;
+ }
+ return null;
+ }
+
private Uri getAggregateFilterUri(String filter) {
if (!TextUtils.isEmpty(filter)) {
return Uri.withAppendedPath(Aggregates.CONTENT_SUMMARY_FILTER_URI, Uri.encode(filter));
@@ -1199,20 +1019,12 @@
getSortOrder(CONTACTS_PROJECTION));
break; */
- case MODE_ALL_CONTACTS:
+ case MODE_DEFAULT:
case MODE_PICK_AGGREGATE:
case MODE_PICK_OR_CREATE_AGGREGATE:
case MODE_INSERT_OR_EDIT_CONTACT:
mQueryHandler.startQuery(QUERY_TOKEN, null, Aggregates.CONTENT_SUMMARY_URI,
- AGGREGATES_SUMMARY_PROJECTION, null, null,
- getSortOrder(AGGREGATES_SUMMARY_PROJECTION));
-
- break;
-
- case MODE_WITH_PHONES:
- mQueryHandler.startQuery(QUERY_TOKEN, null, Aggregates.CONTENT_SUMMARY_URI,
- AGGREGATES_SUMMARY_PROJECTION,
- Aggregates.PRIMARY_PHONE_ID + " IS NOT NULL", null,
+ AGGREGATES_SUMMARY_PROJECTION, getAggregateSelection(), null,
getSortOrder(AGGREGATES_SUMMARY_PROJECTION));
break;
@@ -1298,29 +1110,12 @@
final ContentResolver resolver = getContentResolver();
switch (mMode) {
- /* case MODE_GROUP: {
- Uri uri;
- if (TextUtils.isEmpty(filter)) {
- uri = mGroupUri;
- } else {
- uri = Uri.withAppendedPath(mGroupFilterUri, Uri.encode(filter));
- }
- return resolver.query(uri, CONTACTS_PROJECTION, null, null,
- getSortOrder(CONTACTS_PROJECTION));
- } */
-
- case MODE_ALL_CONTACTS:
+ case MODE_DEFAULT:
case MODE_PICK_AGGREGATE:
case MODE_PICK_OR_CREATE_AGGREGATE:
case MODE_INSERT_OR_EDIT_CONTACT: {
return resolver.query(getAggregateFilterUri(filter), AGGREGATES_SUMMARY_PROJECTION,
- null, null, getSortOrder(AGGREGATES_SUMMARY_PROJECTION));
- }
-
- case MODE_WITH_PHONES: {
- return resolver.query(getAggregateFilterUri(filter), AGGREGATES_SUMMARY_PROJECTION,
- Aggregates.PRIMARY_PHONE_ID + " IS NOT NULL", null,
- getSortOrder(AGGREGATES_SUMMARY_PROJECTION));
+ getAggregateSelection(), null, getSortOrder(AGGREGATES_SUMMARY_PROJECTION));
}
case MODE_STARRED: {
@@ -1402,84 +1197,6 @@
return (Cursor) listView.getAdapter().getItem(index);
}
- /*
- private void setGroupEntries(AlertDialog.Builder builder) {
- boolean syncEverything;
- // For now we only support a single account and the UI doesn't know what
- // the account name is, so we're using a global setting for SYNC_EVERYTHING.
- // Some day when we add multiple accounts to the UI this should use the per
- // account setting.
- String value = Contacts.Settings.getSetting(getContentResolver(), null,
- Contacts.Settings.SYNC_EVERYTHING);
- if (value == null) {
- // If nothing is set yet we default to syncing everything
- syncEverything = true;
- } else {
- syncEverything = !TextUtils.isEmpty(value) && !"0".equals(value);
- }
-
- Cursor cursor;
- if (!syncEverything) {
- cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
- Groups.SHOULD_SYNC + " != 0", null, Groups.DEFAULT_SORT_ORDER);
- } else {
- cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
- null, null, Groups.DEFAULT_SORT_ORDER);
- }
- try {
- ArrayList<CharSequence> groups = new ArrayList<CharSequence>();
- ArrayList<CharSequence> prefStrings = new ArrayList<CharSequence>();
-
- // Add All Contacts
- groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS, getString(R.string.showAllGroups));
- prefStrings.add("");
-
- // Add Contacts with phones
- groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES,
- getString(R.string.groupNameWithPhones));
- prefStrings.add(GROUP_WITH_PHONES);
-
- int currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
- while (cursor.moveToNext()) {
- String systemId = cursor.getString(GROUPS_COLUMN_INDEX_SYSTEM_ID);
- String name = cursor.getString(GROUPS_COLUMN_INDEX_NAME);
- if (cursor.isNull(GROUPS_COLUMN_INDEX_SYSTEM_ID)
- && !Groups.GROUP_MY_CONTACTS.equals(systemId)) {
- // All groups that aren't My Contacts, since that one is localized on the phone
-
- // Localize the "Starred in Android" string which we get from the server side.
- if (Groups.GROUP_ANDROID_STARRED.equals(name)) {
- name = getString(R.string.starredInAndroid);
- }
- groups.add(name);
- if (name.equals(mDisplayInfo)) {
- currentIndex = groups.size() - 1;
- }
- } else {
- // The My Contacts group
- groups.add(DISPLAY_GROUP_INDEX_MY_CONTACTS,
- getString(R.string.groupNameMyContacts));
- if (mDisplayType == DISPLAY_TYPE_SYSTEM_GROUP
- && Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
- currentIndex = DISPLAY_GROUP_INDEX_MY_CONTACTS;
- }
- mDisplayGroupsIncludesMyContacts = true;
- }
- }
- if (mMode == MODE_ALL_CONTACTS) {
- currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
- } else if (mMode == MODE_WITH_PHONES) {
- currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES;
- }
- mDisplayGroups = groups.toArray(new CharSequence[groups.size()]);
- builder.setSingleChoiceItems(mDisplayGroups, currentIndex, this);
- mDisplayGroupOriginalSelection = currentIndex;
- } finally {
- cursor.close();
- }
- }
- */
-
private static class QueryHandler extends AsyncQueryHandler {
protected final WeakReference<ContactsListActivity> mActivity;
diff --git a/src/com/android/contacts/DisplayGroupsActivity.java b/src/com/android/contacts/DisplayGroupsActivity.java
new file mode 100644
index 0000000..c468655
--- /dev/null
+++ b/src/com/android/contacts/DisplayGroupsActivity.java
@@ -0,0 +1,635 @@
+/*
+ * Copyright (C) 2009 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.contacts;
+
+import com.android.contacts.NotifyingAsyncQueryHandler.QueryCompleteListener;
+
+import android.app.ExpandableListActivity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.database.CharArrayBuffer;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.GroupsColumns;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.CheckBox;
+import android.widget.ExpandableListView;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+import android.widget.AdapterView.OnItemClickListener;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Shows a list of all available {@link Groups} available, letting the user
+ * select which ones they want to be visible.
+ */
+public final class DisplayGroupsActivity extends ExpandableListActivity implements
+ QueryCompleteListener, OnItemClickListener {
+ private static final String TAG = "DisplayGroupsActivity";
+
+ public interface Prefs {
+ public static final String DISPLAY_ALL = "display_all";
+ public static final boolean DISPLAY_ALL_DEFAULT = true;
+
+ public static final String DISPLAY_ONLY_PHONES = "only_phones";
+ public static final boolean DISPLAY_ONLY_PHONES_DEFAULT = true;
+
+ }
+
+ private ExpandableListView mList;
+ private DisplayGroupsAdapter mAdapter;
+
+ private SharedPreferences mPrefs;
+ private NotifyingAsyncQueryHandler mHandler;
+
+ private static final int QUERY_TOKEN = 42;
+
+ private View mHeaderAll;
+ private View mHeaderPhones;
+ private View mHeaderSeparator;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(android.R.layout.expandable_list_content);
+
+ mList = getExpandableListView();
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ boolean displayAll = mPrefs.getBoolean(Prefs.DISPLAY_ALL, Prefs.DISPLAY_ALL_DEFAULT);
+ boolean displayOnlyPhones = mPrefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
+ Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
+
+ final LayoutInflater inflater = getLayoutInflater();
+
+ // Add the "All contacts" header modifier.
+ mHeaderAll = inflater.inflate(R.layout.display_header, mList, false);
+ mHeaderAll.setId(R.id.header_all);
+ {
+ CheckBox checkbox = (CheckBox)mHeaderAll.findViewById(android.R.id.checkbox);
+ TextView text1 = (TextView)mHeaderAll.findViewById(android.R.id.text1);
+ checkbox.setChecked(displayAll);
+ text1.setText(R.string.showAllGroups);
+ }
+ mList.addHeaderView(mHeaderAll, null, true);
+
+
+ // Add the "Only contacts with phones" header modifier.
+ mHeaderPhones = inflater.inflate(R.layout.display_header, mList, false);
+ mHeaderPhones.setId(R.id.header_phones);
+ {
+ CheckBox checkbox = (CheckBox)mHeaderPhones.findViewById(android.R.id.checkbox);
+ TextView text1 = (TextView)mHeaderPhones.findViewById(android.R.id.text1);
+ TextView text2 = (TextView)mHeaderPhones.findViewById(android.R.id.text2);
+ checkbox.setChecked(displayOnlyPhones);
+ text1.setText(R.string.showFilterPhones);
+ text2.setText(R.string.showFilterPhonesDescrip);
+ }
+ mList.addHeaderView(mHeaderPhones, null, true);
+
+
+ // Add the separator before showing the detailed group list.
+ mHeaderSeparator = inflater.inflate(R.layout.list_separator, mList, false);
+ {
+ TextView text1 = (TextView)mHeaderSeparator;
+ text1.setText(R.string.headerContactGroups);
+ }
+ mList.addHeaderView(mHeaderSeparator, null, false);
+
+
+ final TextView allContactsView = (TextView)mHeaderAll.findViewById(android.R.id.text2);
+
+ mAdapter = new DisplayGroupsAdapter(this);
+ mAdapter.setAllContactsView(allContactsView);
+
+ mAdapter.setEnabled(!displayAll);
+ mAdapter.setChildDescripWithPhones(displayOnlyPhones);
+
+ setListAdapter(mAdapter);
+
+ // Catch clicks on the header views
+ mList.setOnItemClickListener(this);
+
+ mHandler = new NotifyingAsyncQueryHandler(this, this);
+ startQuery();
+
+ }
+
+ @Override
+ protected void onRestart() {
+ super.onRestart();
+ startQuery();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mHandler.cancelOperation(QUERY_TOKEN);
+ }
+
+
+ private void startQuery() {
+ mHandler.cancelOperation(QUERY_TOKEN);
+ mHandler.startQuery(QUERY_TOKEN, null, Groups.CONTENT_SUMMARY_URI,
+ Projections.PROJ_SUMMARY, null, null, Projections.SORT_ORDER);
+ }
+
+ public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ mAdapter.changeCursor(cursor);
+
+ // Expand all data sources
+ final int groupCount = mAdapter.getGroupCount();
+ for (int i = 0; i < groupCount; i++) {
+ mList.expandGroup(i);
+ }
+ }
+
+ /**
+ * Handle any clicks on header views added to our {@link #mAdapter}, which
+ * are usually the global modifier checkboxes.
+ */
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
+ switch (view.getId()) {
+ case R.id.header_all: {
+ checkbox.toggle();
+ final boolean displayAll = checkbox.isChecked();
+
+ Editor editor = mPrefs.edit();
+ editor.putBoolean(Prefs.DISPLAY_ALL, displayAll);
+ editor.commit();
+
+ mAdapter.setEnabled(!displayAll);
+ mAdapter.notifyDataSetChanged();
+
+ break;
+ }
+ case R.id.header_phones: {
+ checkbox.toggle();
+ final boolean displayOnlyPhones = checkbox.isChecked();
+
+ Editor editor = mPrefs.edit();
+ editor.putBoolean(Prefs.DISPLAY_ONLY_PHONES, displayOnlyPhones);
+ editor.commit();
+
+ mAdapter.setChildDescripWithPhones(displayOnlyPhones);
+ mAdapter.notifyDataSetChanged();
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Handle any clicks on {@link ExpandableListAdapter} children, which
+ * usually mean toggling its visible state.
+ */
+ @Override
+ public boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+ int childPosition, long id) {
+ if (!mAdapter.isEnabled()) {
+ return false;
+ }
+
+ final CheckBox checkbox = (CheckBox)v.findViewById(android.R.id.checkbox);
+ checkbox.toggle();
+
+ // Build visibility update and send down to database
+ final ContentResolver resolver = getContentResolver();
+ final ContentValues values = new ContentValues();
+
+ values.put(Groups.GROUP_VISIBLE, checkbox.isChecked() ? 1 : 0);
+
+ final long groupId = mAdapter.getChildId(groupPosition, childPosition);
+ final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, groupId);
+
+ resolver.update(groupUri, values, null, null);
+
+ return true;
+ }
+
+ /**
+ * Helper for obtaining {@link Resources} instances that are based in an
+ * external package. Maintains internal cache to remain fast.
+ */
+ private static class ExternalResources {
+ private Context mContext;
+ private HashMap<String, Context> mCache = new HashMap<String, Context>();
+
+ public ExternalResources(Context context) {
+ mContext = context;
+ }
+
+ private Context getPackageContext(String packageName) throws NameNotFoundException {
+ Context theirContext = mCache.get(packageName);
+ if (theirContext == null) {
+ theirContext = mContext.createPackageContext(packageName, 0);
+ mCache.put(packageName, theirContext);
+ }
+ return theirContext;
+ }
+
+ public Resources getResources(String packageName) throws NameNotFoundException {
+ return getPackageContext(packageName).getResources();
+ }
+
+ public CharSequence getText(String packageName, int stringRes)
+ throws NameNotFoundException {
+ return getResources(packageName).getText(stringRes);
+ }
+ }
+
+ /**
+ * Adapter that shows all display groups as returned by a {@link Cursor}
+ * over {@link Groups#CONTENT_SUMMARY_URI}, along with their current visible
+ * status. Splits groups into sections based on {@link Groups#PACKAGE}.
+ */
+ private static class DisplayGroupsAdapter extends BaseExpandableListAdapter {
+ private boolean mDataValid;
+ private Cursor mCursor;
+ private Context mContext;
+ private Resources mResources;
+ private ExternalResources mExternalRes;
+ private LayoutInflater mInflater;
+ private int mRowIDColumn;
+
+ private TextView mAllContactsView;
+
+ private boolean mEnabled = true;
+ private boolean mChildWithPhones = false;
+
+ private ContentObserver mContentObserver = new MyChangeObserver();
+ private DataSetObserver mDataSetObserver = new MyDataSetObserver();
+
+ /**
+ * A single group in our expandable list.
+ */
+ private static class Group {
+ public long packageId = -1;
+ public String packageName = null;
+ public int firstPos;
+ public int lastPos;
+ public CharSequence label;
+ }
+
+ /**
+ * Maintain a list of all groups that need to be displayed by this
+ * adapter, usually built by walking across a single {@link Cursor} and
+ * finding the {@link Groups#PACKAGE} boundaries.
+ */
+ private static final ArrayList<Group> mGroups = new ArrayList<Group>();
+
+ public DisplayGroupsAdapter(Context context) {
+ mContext = context;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mResources = context.getResources();
+ mExternalRes = new ExternalResources(mContext);
+ }
+
+ /**
+ * In group descriptions, show the number of contacts with phone
+ * numbers, in addition to the total contacts.
+ */
+ public void setChildDescripWithPhones(boolean withPhones) {
+ mChildWithPhones = withPhones;
+ }
+
+ /**
+ * Set a {@link TextView} to be filled with the total number of contacts
+ * across all available groups.
+ */
+ public void setAllContactsView(TextView allContactsView) {
+ mAllContactsView = allContactsView;
+ }
+
+ /**
+ * Set the {@link View#setEnabled(boolean)} state of any views
+ * constructed by this adapter.
+ */
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ /**
+ * Returns the {@link View#setEnabled(boolean)} value being set for any
+ * children views of this adapter.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Used internally to build the {@link #mGroups} mapping. Call when you
+ * have a valid cursor and are ready to rebuild the mapping.
+ */
+ private void buildInternalMapping() {
+ final PackageManager pm = mContext.getPackageManager();
+ int totalContacts = 0;
+ Group group = null;
+
+ mGroups.clear();
+ mCursor.moveToPosition(-1);
+ while (mCursor.moveToNext()) {
+ final int position = mCursor.getPosition();
+ final long packageId = mCursor.getLong(Projections.COL_PACKAGE_ID);
+ totalContacts += mCursor.getInt(Projections.COL_SUMMARY_COUNT);
+ if (group == null || packageId != group.packageId) {
+ group = new Group();
+ group.packageId = packageId;
+ group.packageName = mCursor.getString(Projections.COL_PACKAGE);
+ group.firstPos = position;
+ group.label = group.packageName;
+
+ try {
+ group.label = pm.getApplicationInfo(group.packageName, 0).loadLabel(pm);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "couldn't find label for package " + group.packageName);
+ }
+
+ mGroups.add(group);
+ }
+ group.lastPos = position;
+ }
+
+ if (mAllContactsView != null) {
+ mAllContactsView.setText(mResources.getQuantityString(R.plurals.groupDescrip,
+ totalContacts, totalContacts));
+ }
+
+ }
+
+ /**
+ * Map the given group and child position into a flattened position on
+ * our single {@link Cursor}.
+ */
+ public int getCursorPosition(int groupPosition, int childPosition) {
+ // The actual cursor position for a child is simply stepping from
+ // the first position for that group.
+ final Group group = mGroups.get(groupPosition);
+ final int position = group.firstPos + childPosition;
+ return position;
+ }
+
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+
+ public Object getChild(int groupPosition, int childPosition) {
+ if (mDataValid && mCursor != null) {
+ final int position = getCursorPosition(groupPosition, childPosition);
+ mCursor.moveToPosition(position);
+ return mCursor;
+ } else {
+ return null;
+ }
+ }
+
+ public long getChildId(int groupPosition, int childPosition) {
+ if (mDataValid && mCursor != null) {
+ final int position = getCursorPosition(groupPosition, childPosition);
+ if (mCursor.moveToPosition(position)) {
+ return mCursor.getLong(mRowIDColumn);
+ } else {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ public int getChildrenCount(int groupPosition) {
+ if (mDataValid && mCursor != null) {
+ final Group group = mGroups.get(groupPosition);
+ final int size = group.lastPos - group.firstPos + 1;
+ return size;
+ } else {
+ return 0;
+ }
+ }
+
+ public Object getGroup(int groupPosition) {
+ if (mDataValid && mCursor != null) {
+ return mGroups.get(groupPosition);
+ } else {
+ return null;
+ }
+ }
+
+ public int getGroupCount() {
+ if (mDataValid && mCursor != null) {
+ return mGroups.size();
+ } else {
+ return 0;
+ }
+ }
+
+ public long getGroupId(int groupPosition) {
+ if (mDataValid && mCursor != null) {
+ final Group group = mGroups.get(groupPosition);
+ return group.packageId;
+ } else {
+ return 0;
+ }
+ }
+
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ if (!mDataValid) {
+ throw new IllegalStateException("called with invalid cursor");
+ }
+
+ final Group group = mGroups.get(groupPosition);
+
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.display_group, parent, false);
+ }
+
+ final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+
+ text1.setText(group.label);
+
+ convertView.setEnabled(mEnabled);
+
+ return convertView;
+ }
+
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ if (!mDataValid) {
+ throw new IllegalStateException("called with invalid cursor");
+ }
+
+ final int position = getCursorPosition(groupPosition, childPosition);
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("couldn't move cursor to position " + position);
+ }
+
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.display_child, parent, false);
+ }
+
+ final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+ final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+ final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox);
+
+ final int count = mCursor.getInt(Projections.COL_SUMMARY_COUNT);
+ final int withPhones = mCursor.getInt(Projections.COL_SUMMARY_WITH_PHONES);
+ final int membersVisible = mCursor.getInt(Projections.COL_GROUP_VISIBLE);
+
+ // Read title, but override with string resource when present
+ CharSequence title = mCursor.getString(Projections.COL_TITLE);
+ if (!mCursor.isNull(Projections.COL_TITLE_RESOURCE)) {
+ final String packageName = mCursor.getString(Projections.COL_PACKAGE);
+ final int titleRes = mCursor.getInt(Projections.COL_TITLE_RESOURCE);
+ try {
+ title = mExternalRes.getText(packageName, titleRes);
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "couldn't load group title resource for " + packageName);
+ }
+ }
+
+ final int descripString = mChildWithPhones ? R.plurals.groupDescripPhones
+ : R.plurals.groupDescrip;
+
+ text1.setText(title);
+ text2.setText(mResources.getQuantityString(descripString, count, count, withPhones));
+ checkbox.setChecked((membersVisible == 1));
+
+ convertView.setEnabled(mEnabled);
+
+ return convertView;
+ }
+
+ public void changeCursor(Cursor cursor) {
+ if (cursor == mCursor) {
+ return;
+ }
+ if (mCursor != null) {
+ mCursor.unregisterContentObserver(mContentObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mCursor.close();
+ }
+ mCursor = cursor;
+ if (cursor != null) {
+ cursor.registerContentObserver(mContentObserver);
+ cursor.registerDataSetObserver(mDataSetObserver);
+ mRowIDColumn = cursor.getColumnIndexOrThrow("_id");
+ mDataValid = true;
+ buildInternalMapping();
+ // notify the observers about the new cursor
+ notifyDataSetChanged();
+ } else {
+ mRowIDColumn = -1;
+ mDataValid = false;
+ // notify the observers about the lack of a data set
+ notifyDataSetInvalidated();
+ }
+ }
+
+ protected void onContentChanged() {
+ if (mCursor != null && !mCursor.isClosed()) {
+ mDataValid = mCursor.requery();
+ }
+ }
+
+ private class MyChangeObserver extends ContentObserver {
+ public MyChangeObserver() {
+ super(new Handler());
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ onContentChanged();
+ }
+ }
+
+ private class MyDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ mDataValid = true;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDataValid = false;
+ notifyDataSetInvalidated();
+ }
+ }
+
+ }
+
+ /**
+ * Database projections used locally.
+ */
+ private interface Projections {
+
+ public static final String[] PROJ_SUMMARY = new String[] {
+ Groups._ID,
+ Groups.PACKAGE_ID,
+ Groups.PACKAGE,
+ Groups.TITLE,
+ Groups.TITLE_RESOURCE,
+ Groups.GROUP_VISIBLE,
+ Groups.SUMMARY_COUNT,
+ Groups.SUMMARY_WITH_PHONES,
+ };
+
+ public static final String SORT_ORDER = Groups.PACKAGE + " ASC";
+
+ public static final int COL_ID = 0;
+ public static final int COL_PACKAGE_ID = 1;
+ public static final int COL_PACKAGE = 2;
+ public static final int COL_TITLE = 3;
+ public static final int COL_TITLE_RESOURCE = 4;
+ public static final int COL_GROUP_VISIBLE = 5;
+ public static final int COL_SUMMARY_COUNT = 6;
+ public static final int COL_SUMMARY_WITH_PHONES = 7;
+
+ }
+
+}