New display options activity
New activity and a new hacky way to invoke it from
the filter spinner.
Also a custom item view for the spinner.
Change-Id: I00e3854023d8602ea94dc7d11f2961b7efeaca38
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b67b010..d6b6bf2 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -307,6 +307,12 @@
android:label="@string/displayGroups"
android:theme="@style/ContactsPreferencesTheme" />
+ <!-- Used to select display and sync groups -->
+ <activity
+ android:name=".list.CustomContactListFilterActivity"
+ android:label="@string/custom_list_filter"
+ android:theme="@style/ContactsPreferencesTheme" />
+
<activity
android:name=".ui.ShowOrCreateActivity"
android:theme="@style/FullyTranslucent">
diff --git a/res/layout-xlarge/contacts_list_content.xml b/res/layout-xlarge/contacts_list_content.xml
index 0ef0544..e5af112 100644
--- a/res/layout-xlarge/contacts_list_content.xml
+++ b/res/layout-xlarge/contacts_list_content.xml
@@ -22,7 +22,8 @@
android:orientation="horizontal"
>
- <com.android.contacts.list.ContactListAizyView
+ <view
+ class="com.android.contacts.list.ContactListAizyView"
android:id="@+id/contacts_list_aizy"
android:layout_width="40dip"
android:layout_height="match_parent"
@@ -35,7 +36,8 @@
android:layout_weight="1"
>
- <Spinner
+ <view
+ class="com.android.contacts.widget.NotifyingSpinner"
android:id="@+id/filter_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/res/layout/contacts_list_content.xml b/res/layout/contacts_list_content.xml
index 0737bbc..34d629f 100644
--- a/res/layout/contacts_list_content.xml
+++ b/res/layout/contacts_list_content.xml
@@ -29,7 +29,8 @@
android:layout_weight="1"
>
- <Spinner
+ <view
+ class="com.android.contacts.widget.NotifyingSpinner"
android:id="@+id/filter_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -54,7 +55,8 @@
android:layout_height="wrap_content"
/>
</LinearLayout>
- <com.android.contacts.list.ContactListAizyView
+ <view
+ class="com.android.contacts.list.ContactListAizyView"
android:id="@+id/contacts_list_aizy"
android:layout_width="30dip"
android:layout_height="match_parent"
diff --git a/res/layout/custom_list_filter.xml b/res/layout/custom_list_filter.xml
new file mode 100644
index 0000000..5176ed2
--- /dev/null
+++ b/res/layout/custom_list_filter.xml
@@ -0,0 +1,62 @@
+<?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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pinned_header_list_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ >
+
+ <LinearLayout
+ android:layout_width="0px"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_weight="1"
+ >
+
+ <Spinner
+ android:id="@+id/filter_spinner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:prompt="@string/list_filter_prompt"
+ android:visibility="gone"
+ />
+
+ <view
+ class="com.android.contacts.ContactEntryListView"
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:fastScrollEnabled="true"
+ android:layout_weight="1"
+ />
+
+ <include layout="@layout/contacts_list_empty"/>
+
+ <ViewStub android:id="@+id/footer_stub"
+ android:layout="@layout/footer_panel"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ />
+ </LinearLayout>
+ <com.android.contacts.list.ContactListAizyView
+ android:id="@+id/contacts_list_aizy"
+ android:layout_width="30dip"
+ android:layout_height="match_parent"
+ />
+</LinearLayout>
diff --git a/res/layout/filter_spinner_item.xml b/res/layout/filter_spinner_item.xml
index 79c871c..b05a019 100644
--- a/res/layout/filter_spinner_item.xml
+++ b/res/layout/filter_spinner_item.xml
@@ -14,8 +14,9 @@
limitations under the License.
-->
-<LinearLayout
+<view
xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.contacts.list.FilterSpinnerItemView"
android:layout_height="52dip"
android:layout_width="fill_parent"
android:paddingLeft="7dip"
@@ -47,5 +48,5 @@
android:gravity="center_vertical"
android:ellipsize="end"
android:paddingLeft="80dip" />
-</LinearLayout>
+</view>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5478665..30134a9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1324,4 +1324,7 @@
<!-- Contact list filter selection indicating that the list shows groups chosen by the user [CHAR LIMIT=64] -->
<string name="list_filter_customize">Customize...</string>
+
+ <!-- Title of the activity that allows the user to customize filtering of contact list [CHAR LIMIT=128] -->
+ <string name="custom_list_filter">Custom contact list</string>
</resources>
diff --git a/src/com/android/contacts/list/CustomContactListFilterActivity.java b/src/com/android/contacts/list/CustomContactListFilterActivity.java
new file mode 100644
index 0000000..b6c6f4c
--- /dev/null
+++ b/src/com/android/contacts/list/CustomContactListFilterActivity.java
@@ -0,0 +1,919 @@
+/*
+ * 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.list;
+
+import com.android.contacts.ContactsSearchManager;
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.GoogleSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.util.EmptyService;
+import com.android.contacts.util.LocalizedNameResolver;
+import com.android.contacts.util.WeakAsyncTask;
+import com.google.android.collect.Lists;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ExpandableListActivity;
+import android.app.ProgressDialog;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderOperation.Builder;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.EntityIterator;
+import android.content.Intent;
+import android.content.OperationApplicationException;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Settings;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.CheckBox;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.TextView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+
+/**
+ * Shows a list of all available {@link Groups} available, letting the user
+ * select which ones they want to be visible.
+ */
+public final class CustomContactListFilterActivity extends ExpandableListActivity implements
+ AdapterView.OnItemClickListener, View.OnClickListener {
+
+ private static final String TAG = "CustomContactListFilterActivity";
+
+ public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false;
+ public static final String PREF_DISPLAY_ONLY_PHONES = "only_phones";
+
+ private ExpandableListView mList;
+ private DisplayAdapter mAdapter;
+
+ private SharedPreferences mPrefs;
+
+ private CheckBox mDisplayPhones;
+
+ private View mHeaderPhones;
+ private View mHeaderSeparator;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.contacts_preferences);
+
+ mList = getExpandableListView();
+ mList.setHeaderDividersEnabled(true);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mAdapter = new DisplayAdapter(this);
+
+ final LayoutInflater inflater = getLayoutInflater();
+
+ createWithPhonesOnlyPreferenceView(inflater);
+ createDisplayGroupHeader(inflater);
+
+ findViewById(R.id.btn_done).setOnClickListener(this);
+ findViewById(R.id.btn_discard).setOnClickListener(this);
+
+ // Catch clicks on the header views
+ mList.setOnItemClickListener(this);
+ mList.setOnCreateContextMenuListener(this);
+ }
+
+ private void createWithPhonesOnlyPreferenceView(LayoutInflater inflater) {
+ // Add the "Only contacts with phones" header modifier.
+ mHeaderPhones = inflater.inflate(R.layout.display_options_phones_only, mList, false);
+ mHeaderPhones.setId(R.id.header_phones);
+ mDisplayPhones = (CheckBox) mHeaderPhones.findViewById(android.R.id.checkbox);
+ mDisplayPhones.setChecked(
+ mPrefs.getBoolean(PREF_DISPLAY_ONLY_PHONES, PREF_DISPLAY_ONLY_PHONES_DEFAULT));
+ {
+ final TextView text1 = (TextView)mHeaderPhones.findViewById(android.R.id.text1);
+ final TextView text2 = (TextView)mHeaderPhones.findViewById(android.R.id.text2);
+ text1.setText(R.string.showFilterPhones);
+ text2.setText(R.string.showFilterPhonesDescrip);
+ }
+ }
+
+ private void createDisplayGroupHeader(LayoutInflater inflater) {
+ // Add the separator before showing the detailed group list.
+ mHeaderSeparator = inflater.inflate(R.layout.list_separator, mList, false);
+ {
+ final TextView text1 = (TextView)mHeaderSeparator;
+ text1.setText(R.string.headerContactGroups);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mList.removeHeaderView(mHeaderPhones);
+ mList.removeHeaderView(mHeaderSeparator);
+
+ // List adapter needs to be reset, because header views cannot be added
+ // to a list with an existing adapter.
+ setListAdapter(null);
+
+ mList.addHeaderView(mHeaderPhones, null, true);
+ mList.addHeaderView(mHeaderSeparator, null, false);
+
+ setListAdapter(mAdapter);
+
+ // Start background query to find account details
+ new QueryGroupsTask(this).execute();
+ }
+
+ /**
+ * Background operation to build set of {@link AccountDisplay} for each
+ * {@link Sources#getAccounts(boolean)} that provides groups.
+ */
+ private static class QueryGroupsTask extends
+ WeakAsyncTask<Void, Void, AccountSet, CustomContactListFilterActivity> {
+ public QueryGroupsTask(CustomContactListFilterActivity target) {
+ super(target);
+ }
+
+ @Override
+ protected AccountSet doInBackground(CustomContactListFilterActivity target,
+ Void... params) {
+ final Context context = target;
+ final Sources sources = Sources.getInstance(context);
+ final ContentResolver resolver = context.getContentResolver();
+
+ // Inflate groups entry for each account
+ final AccountSet accounts = new AccountSet();
+ for (Account account : sources.getAccounts(false)) {
+ accounts.add(new AccountDisplay(resolver, account.name, account.type));
+ }
+
+ return accounts;
+ }
+
+ @Override
+ protected void onPostExecute(CustomContactListFilterActivity target, AccountSet result) {
+ target.mAdapter.setAccounts(result);
+ }
+ }
+
+ private static final int DEFAULT_SHOULD_SYNC = 1;
+ private static final int DEFAULT_VISIBLE = 0;
+
+ /**
+ * Entry holding any changes to {@link Groups} or {@link Settings} rows,
+ * such as {@link Groups#SHOULD_SYNC} or {@link Groups#GROUP_VISIBLE}.
+ */
+ protected static class GroupDelta extends ValuesDelta {
+ private boolean mUngrouped = false;
+ private boolean mAccountHasGroups;
+
+ private GroupDelta() {
+ super();
+ }
+
+ /**
+ * Build {@link GroupDelta} from the {@link Settings} row for the given
+ * {@link Settings#ACCOUNT_NAME} and {@link Settings#ACCOUNT_TYPE}.
+ */
+ public static GroupDelta fromSettings(ContentResolver resolver, String accountName,
+ String accountType, boolean accountHasGroups) {
+ final Uri settingsUri = Settings.CONTENT_URI.buildUpon()
+ .appendQueryParameter(Settings.ACCOUNT_NAME, accountName)
+ .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType).build();
+ final Cursor cursor = resolver.query(settingsUri, new String[] {
+ Settings.SHOULD_SYNC, Settings.UNGROUPED_VISIBLE
+ }, null, null, null);
+
+ try {
+ final ContentValues values = new ContentValues();
+ values.put(Settings.ACCOUNT_NAME, accountName);
+ values.put(Settings.ACCOUNT_TYPE, accountType);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ // Read existing values when present
+ values.put(Settings.SHOULD_SYNC, cursor.getInt(0));
+ values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1));
+ return fromBefore(values).setUngrouped(accountHasGroups);
+ } else {
+ // Nothing found, so treat as create
+ values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC);
+ values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE);
+ return fromAfter(values).setUngrouped(accountHasGroups);
+ }
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ }
+
+ public static GroupDelta fromBefore(ContentValues before) {
+ final GroupDelta entry = new GroupDelta();
+ entry.mBefore = before;
+ entry.mAfter = new ContentValues();
+ return entry;
+ }
+
+ public static GroupDelta fromAfter(ContentValues after) {
+ final GroupDelta entry = new GroupDelta();
+ entry.mBefore = null;
+ entry.mAfter = after;
+ return entry;
+ }
+
+ protected GroupDelta setUngrouped(boolean accountHasGroups) {
+ mUngrouped = true;
+ mAccountHasGroups = accountHasGroups;
+ return this;
+ }
+
+ @Override
+ public boolean beforeExists() {
+ return mBefore != null;
+ }
+
+ public boolean getShouldSync() {
+ return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC,
+ DEFAULT_SHOULD_SYNC) != 0;
+ }
+
+ public boolean getVisible() {
+ return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE,
+ DEFAULT_VISIBLE) != 0;
+ }
+
+ public void putShouldSync(boolean shouldSync) {
+ put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
+ }
+
+ public void putVisible(boolean visible) {
+ put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0);
+ }
+
+ private String getAccountType() {
+ return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE);
+ }
+
+ public CharSequence getTitle(Context context) {
+ if (mUngrouped) {
+ final String customAllContactsName =
+ LocalizedNameResolver.getAllContactsName(context, getAccountType());
+ if (customAllContactsName != null) {
+ return customAllContactsName;
+ }
+ if (mAccountHasGroups) {
+ return context.getText(R.string.display_ungrouped);
+ } else {
+ return context.getText(R.string.display_all_contacts);
+ }
+ } else {
+ final Integer titleRes = getAsInteger(Groups.TITLE_RES);
+ if (titleRes != null) {
+ final String packageName = getAsString(Groups.RES_PACKAGE);
+ return context.getPackageManager().getText(packageName, titleRes, null);
+ } else {
+ return getAsString(Groups.TITLE);
+ }
+ }
+ }
+
+ /**
+ * Build a possible {@link ContentProviderOperation} to persist any
+ * changes to the {@link Groups} or {@link Settings} row described by
+ * this {@link GroupDelta}.
+ */
+ public ContentProviderOperation buildDiff() {
+ if (isNoop()) {
+ return null;
+ } else if (isUpdate()) {
+ // When has changes and "before" exists, then "update"
+ final Builder builder = ContentProviderOperation.newUpdate(
+ mUngrouped
+ ? Settings.CONTENT_URI
+ : addCallerIsSyncAdapterParameter(Groups.CONTENT_URI));
+ if (mUngrouped) {
+ builder.withSelection(Settings.ACCOUNT_NAME + "=? AND " + Settings.ACCOUNT_TYPE
+ + "=?", new String[] {
+ this.getAsString(Settings.ACCOUNT_NAME),
+ this.getAsString(Settings.ACCOUNT_TYPE)
+ });
+ } else {
+ builder.withSelection(Groups._ID + "=" + this.getId(), null);
+ }
+ builder.withValues(mAfter);
+ return builder.build();
+ } else if (isInsert() && mUngrouped) {
+ // Only allow inserts for Settings
+ mAfter.remove(mIdColumn);
+ final Builder builder = ContentProviderOperation.newInsert(Settings.CONTENT_URI);
+ builder.withValues(mAfter);
+ return builder.build();
+ } else {
+ throw new IllegalStateException("Unexpected delete or insert");
+ }
+ }
+ }
+
+ private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+ }
+
+ /**
+ * {@link Comparator} to sort by {@link Groups#_ID}.
+ */
+ private static Comparator<GroupDelta> sIdComparator = new Comparator<GroupDelta>() {
+ public int compare(GroupDelta object1, GroupDelta object2) {
+ final Long id1 = object1.getId();
+ final Long id2 = object2.getId();
+ if (id1 == null && id2 == null) {
+ return 0;
+ } else if (id1 == null) {
+ return -1;
+ } else if (id2 == null) {
+ return 1;
+ } else if (id1 < id2) {
+ return -1;
+ } else if (id1 > id2) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ };
+
+ /**
+ * Set of all {@link AccountDisplay} entries, one for each source.
+ */
+ protected static class AccountSet extends ArrayList<AccountDisplay> {
+ public ArrayList<ContentProviderOperation> buildDiff() {
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ for (AccountDisplay account : this) {
+ account.buildDiff(diff);
+ }
+ return diff;
+ }
+ }
+
+ /**
+ * {@link GroupDelta} details for a single {@link Account}, usually shown as
+ * children under a single expandable group.
+ */
+ protected static class AccountDisplay {
+ public String mName;
+ public String mType;
+
+ public GroupDelta mUngrouped;
+ public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList();
+ public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList();
+
+ /**
+ * Build an {@link AccountDisplay} covering all {@link Groups} under the
+ * given {@link Account}.
+ */
+ public AccountDisplay(ContentResolver resolver, String accountName, String accountType) {
+ mName = accountName;
+ mType = accountType;
+
+ final Uri groupsUri = Groups.CONTENT_URI.buildUpon()
+ .appendQueryParameter(Groups.ACCOUNT_NAME, accountName)
+ .appendQueryParameter(Groups.ACCOUNT_TYPE, accountType).build();
+ EntityIterator iterator = ContactsContract.Groups.newEntityIterator(resolver.query(
+ groupsUri, null, null, null, null));
+ try {
+ boolean hasGroups = false;
+
+ // Create entries for each known group
+ while (iterator.hasNext()) {
+ final ContentValues values = iterator.next().getEntityValues();
+ final GroupDelta group = GroupDelta.fromBefore(values);
+ addGroup(group);
+ hasGroups = true;
+ }
+ // Create single entry handling ungrouped status
+ mUngrouped = GroupDelta.fromSettings(resolver, accountName, accountType, hasGroups);
+ addGroup(mUngrouped);
+ } finally {
+ iterator.close();
+ }
+ }
+
+ /**
+ * Add the given {@link GroupDelta} internally, filing based on its
+ * {@link GroupDelta#getShouldSync()} status.
+ */
+ private void addGroup(GroupDelta group) {
+ if (group.getShouldSync()) {
+ mSyncedGroups.add(group);
+ } else {
+ mUnsyncedGroups.add(group);
+ }
+ }
+
+ /**
+ * Set the {@link GroupDelta#putShouldSync(boolean)} value for all
+ * children {@link GroupDelta} rows.
+ */
+ public void setShouldSync(boolean shouldSync) {
+ final Iterator<GroupDelta> oppositeChildren = shouldSync ?
+ mUnsyncedGroups.iterator() : mSyncedGroups.iterator();
+ while (oppositeChildren.hasNext()) {
+ final GroupDelta child = oppositeChildren.next();
+ setShouldSync(child, shouldSync, false);
+ oppositeChildren.remove();
+ }
+ }
+
+ public void setShouldSync(GroupDelta child, boolean shouldSync) {
+ setShouldSync(child, shouldSync, true);
+ }
+
+ /**
+ * Set {@link GroupDelta#putShouldSync(boolean)}, and file internally
+ * based on updated state.
+ */
+ public void setShouldSync(GroupDelta child, boolean shouldSync, boolean attemptRemove) {
+ child.putShouldSync(shouldSync);
+ if (shouldSync) {
+ if (attemptRemove) {
+ mUnsyncedGroups.remove(child);
+ }
+ mSyncedGroups.add(child);
+ Collections.sort(mSyncedGroups, sIdComparator);
+ } else {
+ if (attemptRemove) {
+ mSyncedGroups.remove(child);
+ }
+ mUnsyncedGroups.add(child);
+ }
+ }
+
+ /**
+ * Build set of {@link ContentProviderOperation} to persist any user
+ * changes to {@link GroupDelta} rows under this {@link Account}.
+ */
+ public void buildDiff(ArrayList<ContentProviderOperation> diff) {
+ for (GroupDelta group : mSyncedGroups) {
+ final ContentProviderOperation oper = group.buildDiff();
+ if (oper != null) diff.add(oper);
+ }
+ for (GroupDelta group : mUnsyncedGroups) {
+ final ContentProviderOperation oper = group.buildDiff();
+ if (oper != null) diff.add(oper);
+ }
+ }
+ }
+
+ /**
+ * {@link ExpandableListAdapter} that shows {@link GroupDelta} settings,
+ * grouped by {@link Account} source. Shows footer row when any groups are
+ * unsynced, as determined through {@link AccountDisplay#mUnsyncedGroups}.
+ */
+ protected static class DisplayAdapter extends BaseExpandableListAdapter {
+ private Context mContext;
+ private LayoutInflater mInflater;
+ private Sources mSources;
+ private AccountSet mAccounts;
+
+ private boolean mChildWithPhones = false;
+
+ public DisplayAdapter(Context context) {
+ mContext = context;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mSources = Sources.getInstance(context);
+ }
+
+ public void setAccounts(AccountSet accounts) {
+ mAccounts = accounts;
+ notifyDataSetChanged();
+ }
+
+ /**
+ * In group descriptions, show the number of contacts with phone
+ * numbers, in addition to the total contacts.
+ */
+ public void setChildDescripWithPhones(boolean withPhones) {
+ mChildWithPhones = withPhones;
+ }
+
+ /** {@inheritDoc} */
+ public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+ View convertView, ViewGroup parent) {
+ 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 AccountDisplay account = mAccounts.get(groupPosition);
+ final GroupDelta child = (GroupDelta)this.getChild(groupPosition, childPosition);
+ if (child != null) {
+ // Handle normal group, with title and checkbox
+ final boolean groupVisible = child.getVisible();
+ checkbox.setVisibility(View.VISIBLE);
+ checkbox.setChecked(groupVisible);
+
+ final CharSequence groupTitle = child.getTitle(mContext);
+ text1.setText(groupTitle);
+
+// final int count = cursor.getInt(GroupsQuery.SUMMARY_COUNT);
+// final int withPhones = cursor.getInt(GroupsQuery.SUMMARY_WITH_PHONES);
+
+// final CharSequence descrip = mContext.getResources().getQuantityString(
+// mChildWithPhones ? R.plurals.groupDescripPhones : R.plurals.groupDescrip,
+// count, count, withPhones);
+
+// text2.setText(descrip);
+ text2.setVisibility(View.GONE);
+ } else {
+ // When unknown child, this is "more" footer view
+ checkbox.setVisibility(View.GONE);
+ text1.setText(R.string.display_more_groups);
+ text2.setVisibility(View.GONE);
+ }
+
+ return convertView;
+ }
+
+ /** {@inheritDoc} */
+ public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+ ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.display_group, parent, false);
+ }
+
+ final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+ final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+
+ final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition);
+
+ final ContactsSource source = mSources.getInflatedSource(account.mType,
+ ContactsSource.LEVEL_SUMMARY);
+
+ text1.setText(account.mName);
+ text2.setText(source.getDisplayLabel(mContext));
+ text2.setVisibility(account.mName == null ? View.GONE : View.VISIBLE);
+
+ return convertView;
+ }
+
+ /** {@inheritDoc} */
+ public Object getChild(int groupPosition, int childPosition) {
+ final AccountDisplay account = mAccounts.get(groupPosition);
+ final boolean validChild = childPosition >= 0
+ && childPosition < account.mSyncedGroups.size();
+ if (validChild) {
+ return account.mSyncedGroups.get(childPosition);
+ } else {
+ return null;
+ }
+ }
+
+ /** {@inheritDoc} */
+ public long getChildId(int groupPosition, int childPosition) {
+ final GroupDelta child = (GroupDelta)getChild(groupPosition, childPosition);
+ if (child != null) {
+ final Long childId = child.getId();
+ return childId != null ? childId : Long.MIN_VALUE;
+ } else {
+ return Long.MIN_VALUE;
+ }
+ }
+
+ /** {@inheritDoc} */
+ public int getChildrenCount(int groupPosition) {
+ // Count is any synced groups, plus possible footer
+ final AccountDisplay account = mAccounts.get(groupPosition);
+ final boolean anyHidden = account.mUnsyncedGroups.size() > 0;
+ return account.mSyncedGroups.size() + (anyHidden ? 1 : 0);
+ }
+
+ /** {@inheritDoc} */
+ public Object getGroup(int groupPosition) {
+ return mAccounts.get(groupPosition);
+ }
+
+ /** {@inheritDoc} */
+ public int getGroupCount() {
+ if (mAccounts == null) {
+ return 0;
+ }
+ return mAccounts.size();
+ }
+
+ /** {@inheritDoc} */
+ public long getGroupId(int groupPosition) {
+ return groupPosition;
+ }
+
+ /** {@inheritDoc} */
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isChildSelectable(int groupPosition, int childPosition) {
+ return true;
+ }
+ }
+
+ /**
+ * 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) {
+ Log.d(TAG, "OnItemClick, position=" + position + ", id=" + id);
+ if (view == mHeaderPhones) {
+ mDisplayPhones.toggle();
+ return;
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.btn_done: {
+ this.doSaveAction();
+ break;
+ }
+ case R.id.btn_discard: {
+ this.finish();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Assign a specific value to {@link #PREF_DISPLAY_ONLY_PHONES}, refreshing
+ * the visible list as needed.
+ */
+ protected void setDisplayOnlyPhones(boolean displayOnlyPhones) {
+ mDisplayPhones.setChecked(displayOnlyPhones);
+
+ Editor editor = mPrefs.edit();
+ editor.putBoolean(PREF_DISPLAY_ONLY_PHONES, displayOnlyPhones);
+ editor.apply();
+
+ mAdapter.setChildDescripWithPhones(displayOnlyPhones);
+ mAdapter.notifyDataSetChanged();
+ }
+
+ /**
+ * Handle any clicks on {@link ExpandableListAdapter} children, which
+ * usually mean toggling its visible state.
+ */
+ @Override
+ public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
+ int childPosition, long id) {
+ final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
+
+ final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
+ final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
+ if (child != null) {
+ checkbox.toggle();
+ child.putVisible(checkbox.isChecked());
+ } else {
+ // Open context menu for bringing back unsynced
+ this.openContextMenu(view);
+ }
+ return true;
+ }
+
+ // TODO: move these definitions to framework constants when we begin
+ // defining this mode through <sync-adapter> tags
+ private static final int SYNC_MODE_UNSUPPORTED = 0;
+ private static final int SYNC_MODE_UNGROUPED = 1;
+ private static final int SYNC_MODE_EVERYTHING = 2;
+
+ protected int getSyncMode(AccountDisplay account) {
+ // TODO: read sync mode through <sync-adapter> definition
+ if (GoogleSource.ACCOUNT_TYPE.equals(account.mType)) {
+ return SYNC_MODE_EVERYTHING;
+ } else {
+ return SYNC_MODE_UNSUPPORTED;
+ }
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View view,
+ ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, view, menuInfo);
+
+ // Bail if not working with expandable long-press, or if not child
+ if (!(menuInfo instanceof ExpandableListContextMenuInfo)) return;
+
+ final ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;
+ final int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
+ final int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
+
+ // Skip long-press on expandable parents
+ if (childPosition == -1) return;
+
+ final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
+ final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
+
+ // Ignore when selective syncing unsupported
+ final int syncMode = getSyncMode(account);
+ if (syncMode == SYNC_MODE_UNSUPPORTED) return;
+
+ if (child != null) {
+ showRemoveSync(menu, account, child, syncMode);
+ } else {
+ showAddSync(menu, account, syncMode);
+ }
+ }
+
+ protected void showRemoveSync(ContextMenu menu, final AccountDisplay account,
+ final GroupDelta child, final int syncMode) {
+ final CharSequence title = child.getTitle(this);
+
+ menu.setHeaderTitle(title);
+ menu.add(R.string.menu_sync_remove).setOnMenuItemClickListener(
+ new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ handleRemoveSync(account, child, syncMode, title);
+ return true;
+ }
+ });
+ }
+
+ protected void handleRemoveSync(final AccountDisplay account, final GroupDelta child,
+ final int syncMode, CharSequence title) {
+ final boolean shouldSyncUngrouped = account.mUngrouped.getShouldSync();
+ if (syncMode == SYNC_MODE_EVERYTHING && shouldSyncUngrouped
+ && !child.equals(account.mUngrouped)) {
+ // Warn before removing this group when it would cause ungrouped to stop syncing
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ final CharSequence removeMessage = this.getString(
+ R.string.display_warn_remove_ungrouped, title);
+ builder.setTitle(R.string.menu_sync_remove);
+ builder.setMessage(removeMessage);
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ // Mark both this group and ungrouped to stop syncing
+ account.setShouldSync(account.mUngrouped, false);
+ account.setShouldSync(child, false);
+ mAdapter.notifyDataSetChanged();
+ }
+ });
+ builder.show();
+ } else {
+ // Mark this group to not sync
+ account.setShouldSync(child, false);
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+
+ protected void showAddSync(ContextMenu menu, final AccountDisplay account, final int syncMode) {
+ menu.setHeaderTitle(R.string.dialog_sync_add);
+
+ // Create item for each available, unsynced group
+ for (final GroupDelta child : account.mUnsyncedGroups) {
+ if (!child.getShouldSync()) {
+ final CharSequence title = child.getTitle(this);
+ menu.add(title).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // Adding specific group for syncing
+ if (child.mUngrouped && syncMode == SYNC_MODE_EVERYTHING) {
+ account.setShouldSync(true);
+ } else {
+ account.setShouldSync(child, true);
+ }
+ mAdapter.notifyDataSetChanged();
+ return true;
+ }
+ });
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onBackPressed() {
+ doSaveAction();
+ }
+
+ private void doSaveAction() {
+ if (mAdapter == null || mAdapter.mAccounts == null) {
+ return;
+ }
+ setDisplayOnlyPhones(mDisplayPhones.isChecked());
+ setResult(RESULT_OK);
+ new UpdateTask(this).execute(mAdapter.mAccounts);
+ }
+
+ /**
+ * Background task that persists changes to {@link Groups#GROUP_VISIBLE},
+ * showing spinner dialog to user while updating.
+ */
+ public static class UpdateTask extends
+ WeakAsyncTask<AccountSet, Void, Void, Activity> {
+ private WeakReference<ProgressDialog> mProgress;
+
+ public UpdateTask(Activity target) {
+ super(target);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onPreExecute(Activity target) {
+ final Context context = target;
+
+ mProgress = new WeakReference<ProgressDialog>(ProgressDialog.show(context, null,
+ context.getText(R.string.savingDisplayGroups)));
+
+ // Before starting this task, start an empty service to protect our
+ // process from being reclaimed by the system.
+ context.startService(new Intent(context, EmptyService.class));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Void doInBackground(Activity target, AccountSet... params) {
+ final Context context = target;
+ final ContentValues values = new ContentValues();
+ final ContentResolver resolver = context.getContentResolver();
+
+ try {
+ // Build changes and persist in transaction
+ final AccountSet set = params[0];
+ final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+ resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Problem saving display groups", e);
+ } catch (OperationApplicationException e) {
+ Log.e(TAG, "Problem saving display groups", e);
+ }
+
+ return null;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onPostExecute(Activity target, Void result) {
+ final Context context = target;
+
+ final ProgressDialog dialog = mProgress.get();
+ if (dialog != null) {
+ try {
+ dialog.dismiss();
+ } catch (Exception e) {
+ Log.e(TAG, "Error dismissing progress dialog", e);
+ }
+ }
+
+ target.finish();
+
+ // Stop the service that was protecting us
+ context.stopService(new Intent(context, EmptyService.class));
+ }
+ }
+
+ @Override
+ public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
+ boolean globalSearch) {
+ if (globalSearch) {
+ super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+ } else {
+ ContactsSearchManager.startSearch(this, initialQuery);
+ }
+ }
+}
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 3e6dbe5..7e3fb0e 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -17,8 +17,11 @@
import com.android.contacts.R;
import com.android.contacts.ui.ContactsPreferencesActivity.Prefs;
+import com.android.contacts.widget.NotifyingSpinner;
+import android.app.Activity;
import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Intent;
import android.content.Loader;
import android.content.SharedPreferences;
import android.database.Cursor;
@@ -32,8 +35,6 @@
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.Spinner;
import android.widget.TextView;
import java.util.ArrayList;
@@ -44,13 +45,17 @@
* picking a contact with one of the PICK intents).
*/
public class DefaultContactBrowseListFragment extends ContactBrowseListFragment
- implements OnItemSelectedListener {
+ implements OnItemSelectedListener, NotifyingSpinner.SelectionListener {
private static final String KEY_EDIT_MODE = "editMode";
private static final String KEY_CREATE_CONTACT_ENABLED = "createContactEnabled";
private static final String KEY_DISPLAY_WITH_PHONES_ONLY = "displayWithPhonesOnly";
private static final String KEY_VISIBLE_CONTACTS_RESTRICTION = "visibleContactsRestriction";
+ private static final int GROUP_FILTER_LOADER = -4;
+
+ private static final int REQUEST_CODE_CUSTOMIZE_FILTER = 3;
+
private boolean mEditMode;
private boolean mCreateContactEnabled;
private int mDisplayWithPhonesOnlyOption = ContactsRequest.DISPLAY_ONLY_WITH_PHONES_DISABLED;
@@ -61,7 +66,7 @@
private SparseArray<ContactListFilter> mFilters;
private ArrayList<ContactListFilter> mFilterList;
private int mNextFilterId = 1;
- private Spinner mFilterSpinner;
+ private NotifyingSpinner mFilterSpinner;
private ContactListFilter mFilter;
private boolean mFiltersLoaded;
@@ -198,7 +203,7 @@
}
protected void configureFilterSpinner() {
- mFilterSpinner = (Spinner)getView().findViewById(R.id.filter_spinner);
+ mFilterSpinner = (NotifyingSpinner)getView().findViewById(R.id.filter_spinner);
if (mFilterSpinner == null) {
return;
}
@@ -208,6 +213,7 @@
return;
}
mFilterSpinner.setOnItemSelectedListener(this);
+ mFilterSpinner.setSetSelectionListener(this);
}
@Override
@@ -355,6 +361,24 @@
}
}
+ @Override
+ public void onSetSelection(NotifyingSpinner spinner, int position) {
+ ContactListFilter filter = mFilters.valueAt(position);
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ startActivityForResult(new Intent(getContext(), CustomContactListFilterActivity.class),
+ REQUEST_CODE_CUSTOMIZE_FILTER);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_CUSTOMIZE_FILTER && resultCode == Activity.RESULT_OK) {
+ mFilter = new ContactListFilter(ContactListFilter.FILTER_TYPE_CUSTOM);
+ updateFilterView();
+ reloadData();
+ }
+ }
+
private ContactListFilter getDefaultFilter() {
return mFilters.valueAt(0);
}
@@ -398,55 +422,15 @@
}
public View getView(int position, View convertView, ViewGroup parent, boolean dropdown) {
- View view = convertView != null ? convertView
- : mLayoutInflater.inflate(R.layout.filter_spinner_item, parent, false);
- ImageView icon = (ImageView) view.findViewById(R.id.icon);
- TextView label = (TextView) view.findViewById(R.id.label);
- TextView indentedLabel = (TextView) view.findViewById(R.id.indented_label);
- ContactListFilter filter = mFilters.valueAt(position);
- switch (filter.filterType) {
- case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: {
- icon.setVisibility(View.GONE);
- label.setText(R.string.list_filter_all_accounts);
- label.setVisibility(View.VISIBLE);
- indentedLabel.setVisibility(View.GONE);
- break;
- }
- case ContactListFilter.FILTER_TYPE_CUSTOM: {
- icon.setVisibility(View.GONE);
- label.setText(dropdown
- ? R.string.list_filter_customize
- : R.string.list_filter_custom);
- label.setVisibility(View.VISIBLE);
- indentedLabel.setVisibility(View.GONE);
- break;
- }
- case ContactListFilter.FILTER_TYPE_ACCOUNT: {
- icon.setVisibility(View.VISIBLE);
- if (filter.icon != null) {
- icon.setImageDrawable(filter.icon);
- } else {
- icon.setImageResource(R.drawable.unknown_source);
- }
- label.setText(filter.accountName);
- label.setVisibility(View.VISIBLE);
- indentedLabel.setVisibility(View.GONE);
- break;
- }
- case ContactListFilter.FILTER_TYPE_GROUP: {
- icon.setVisibility(View.GONE);
- if (dropdown) {
- label.setVisibility(View.GONE);
- indentedLabel.setText(filter.title);
- indentedLabel.setVisibility(View.VISIBLE);
- } else {
- label.setText(filter.title);
- label.setVisibility(View.VISIBLE);
- indentedLabel.setVisibility(View.GONE);
- }
- break;
- }
+ FilterSpinnerItemView view;
+ if (dropdown && convertView != null) {
+ view = (FilterSpinnerItemView) convertView;
+ } else {
+ view = (FilterSpinnerItemView) mLayoutInflater.inflate(
+ R.layout.filter_spinner_item, parent, false);
}
+ view.setContactListFilter(mFilters.valueAt(position));
+ view.bindView(dropdown);
return view;
}
}
diff --git a/src/com/android/contacts/list/FilterSpinnerItemView.java b/src/com/android/contacts/list/FilterSpinnerItemView.java
new file mode 100644
index 0000000..ab98a41
--- /dev/null
+++ b/src/com/android/contacts/list/FilterSpinnerItemView.java
@@ -0,0 +1,105 @@
+/*
+ * 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.contacts.list;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * Contact list filter parameters.
+ */
+public final class FilterSpinnerItemView extends LinearLayout {
+
+ private ImageView mIcon;
+ private TextView mLabel;
+ private TextView mIndentedLabel;
+ private ContactListFilter mFilter;
+
+ public FilterSpinnerItemView(Context context) {
+ super(context);
+ }
+
+ public FilterSpinnerItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setContactListFilter(ContactListFilter filter) {
+ mFilter = filter;
+ }
+
+ public ContactListFilter getContactListFilter() {
+ return mFilter;
+ }
+
+ public void bindView(boolean dropdown) {
+ if (mIcon == null) {
+ mIcon = (ImageView) findViewById(R.id.icon);
+ mLabel = (TextView) findViewById(R.id.label);
+ mIndentedLabel = (TextView) findViewById(R.id.indented_label);
+ }
+
+ switch (mFilter.filterType) {
+ case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: {
+ mIcon.setVisibility(View.GONE);
+ mLabel.setText(R.string.list_filter_all_accounts);
+ mLabel.setVisibility(View.VISIBLE);
+ mIndentedLabel.setVisibility(View.GONE);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_CUSTOM: {
+ mIcon.setVisibility(View.GONE);
+ mLabel.setText(dropdown
+ ? R.string.list_filter_customize
+ : R.string.list_filter_custom);
+ mLabel.setVisibility(View.VISIBLE);
+ mIndentedLabel.setVisibility(View.GONE);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_ACCOUNT: {
+ mIcon.setVisibility(View.VISIBLE);
+ if (mFilter.icon != null) {
+ mIcon.setImageDrawable(mFilter.icon);
+ } else {
+ mIcon.setImageResource(R.drawable.unknown_source);
+ }
+ mLabel.setText(mFilter.accountName);
+ mLabel.setVisibility(View.VISIBLE);
+ mIndentedLabel.setVisibility(View.GONE);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_GROUP: {
+ mIcon.setVisibility(View.GONE);
+ if (dropdown) {
+ mLabel.setVisibility(View.GONE);
+ mIndentedLabel.setText(mFilter.title);
+ mIndentedLabel.setVisibility(View.VISIBLE);
+ } else {
+ mLabel.setText(mFilter.title);
+ mLabel.setVisibility(View.VISIBLE);
+ mIndentedLabel.setVisibility(View.GONE);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/widget/NotifyingSpinner.java b/src/com/android/contacts/widget/NotifyingSpinner.java
new file mode 100644
index 0000000..972cb35
--- /dev/null
+++ b/src/com/android/contacts/widget/NotifyingSpinner.java
@@ -0,0 +1,55 @@
+/*
+ * 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.contacts.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Spinner;
+
+/**
+ * Spinner that notifies a listener when the user taps on an item, whether or not this results
+ * in a change of selection.
+ */
+public class NotifyingSpinner extends Spinner {
+
+ public interface SelectionListener {
+ void onSetSelection(NotifyingSpinner view, int position);
+ }
+
+ private SelectionListener mListener;
+
+ public NotifyingSpinner(Context context) {
+ super(context);
+ }
+
+ public NotifyingSpinner(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setSetSelectionListener(SelectionListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void setSelection(int position) {
+ super.setSelection(position);
+
+ if (mListener != null) {
+ mListener.onSetSelection(this, position);
+ }
+ }
+}