Iteration on display groups UI, was neglected for awhile.
Switched to using Accounts metadata provided through
Sources cache. Also added long-press and menu item to serve
as our "edit sync groups" in the same UI.
Need to iterating to show all accounts regardless of group
existance, and persist DEFAULT_SHOULD_SYNC values.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6a005a0..f77a7bb 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -233,7 +233,7 @@
/>
</activity>
- <activity android:name=".DisplayGroupsActivity" android:label="@string/displayGroups" />
+ <activity android:name=".ui.DisplayGroupsActivity" android:label="@string/displayGroups" />
<activity
android:name="ShowOrCreateActivity"
diff --git a/res/layout-finger/display_group.xml b/res/layout-finger/display_group.xml
index 48ff7f4..7d36450 100644
--- a/res/layout-finger/display_group.xml
+++ b/res/layout-finger/display_group.xml
@@ -20,34 +20,36 @@
android:minHeight="?android:attr/listPreferredItemHeight"
android:gravity="center_vertical"
android:paddingLeft="?android:attr/expandableListPreferredItemPaddingLeft"
- android:paddingRight="?android:attr/scrollbarSize"
->
+ android:paddingRight="?android:attr/scrollbarSize">
- <TextView
- android:id="@android:id/text1"
+ <RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="6dip"
android:layout_marginTop="6dip"
android:layout_marginBottom="6dip"
android:layout_weight="1"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:textAppearance="?android:attr/textAppearanceLarge"
- android:duplicateParentState="true"
- />
+ android:duplicateParentState="true">
- <CheckBox
- android:id="@android:id/checkbox"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginRight="4dip"
- android:focusable="false"
- android:clickable="false"
- android:gravity="center_vertical"
- android:orientation="vertical"
- android:visibility="gone"
- android:duplicateParentState="true"
- />
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:duplicateParentState="true" />
+
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/text1"
+ android:layout_alignLeft="@android:id/text1"
+ android:maxLines="2"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:duplicateParentState="true" />
+
+ </RelativeLayout>
</LinearLayout>
diff --git a/res/layout-finger/display_header.xml b/res/layout-finger/display_header.xml
index a55c5d0..421d421 100644
--- a/res/layout-finger/display_header.xml
+++ b/res/layout-finger/display_header.xml
@@ -53,7 +53,7 @@
</RelativeLayout>
- <CheckBox
+ <RadioButton
android:id="@android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -61,7 +61,6 @@
android:focusable="false"
android:clickable="false"
android:gravity="center_vertical"
- android:orientation="vertical"
- />
+ android:orientation="vertical" />
</LinearLayout>
diff --git a/res/menu/display_groups.xml b/res/menu/display_groups.xml
new file mode 100644
index 0000000..3d1a6b0
--- /dev/null
+++ b/res/menu/display_groups.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_add"
+ android:icon="@android:drawable/ic_menu_add"
+ android:title="@string/menu_sync_add" />
+
+</menu>
diff --git a/res/values/ids.xml b/res/values/ids.xml
index dbf9b65..48981e0 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -27,4 +27,6 @@
<item type="id" name="dialog_label" />
<item type="id" name="dialog_label_custom" />
+ <item type="id" name="dialog_sync_add" />
+
</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d845e09..2278761 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -864,6 +864,16 @@
<string name="dialog_primary_name">Primary name</string>
<string name="dialog_new_contact_account">Create contact under account</string>
+ <string name="menu_sync_remove">Remove sync group</string>
+ <string name="menu_sync_add">Add sync group</string>
+
+ <!-- List title for a special contacts group that covers all contacts that
+ aren't members of any other group. -->
+ <string name="display_ungrouped">(Ungrouped contacts)</string>
+
+ <!-- Warning message given to users just before they remove a currently syncing
+ group that would also cause all ungrouped contacts to stop syncing. -->
+ <string name="display_warn_remove_ungrouped">Removing '%s' from sync will also remove any ungrouped contacts from sync.</string>
<string name="call_home">Call home</string>
diff --git a/src/com/android/contacts/ContactsGroupSyncSelector.java b/src/com/android/contacts/ContactsGroupSyncSelector.java
index 869870f..0384516 100644
--- a/src/com/android/contacts/ContactsGroupSyncSelector.java
+++ b/src/com/android/contacts/ContactsGroupSyncSelector.java
@@ -42,6 +42,7 @@
import com.google.android.googlelogin.GoogleLoginServiceConstants;
+@Deprecated
public final class ContactsGroupSyncSelector extends ListActivity implements View.OnClickListener {
private static final String[] PROJECTION = new String[] {
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 22cae96..2eb1ddd 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -16,8 +16,9 @@
package com.android.contacts;
-import com.android.contacts.DisplayGroupsActivity.Prefs;
+import com.android.contacts.ui.DisplayGroupsActivity;
import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.ui.DisplayGroupsActivity.Prefs;
import android.app.Activity;
import android.app.AlertDialog;
@@ -258,7 +259,7 @@
private boolean mJustCreated;
private boolean mSyncEnabled;
- private boolean mDisplayAll;
+// private boolean mDisplayAll;
private boolean mDisplayOnlyPhones;
/**
@@ -373,7 +374,6 @@
buildUserGroupUri(groupName);
} else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
mMode = MODE_CUSTOM;
- mDisplayAll = true;
mDisplayOnlyPhones = false;
} else if (UI.LIST_STARRED_ACTION.equals(action)) {
mMode = MODE_STARRED;
@@ -383,12 +383,10 @@
mMode = MODE_STREQUENT;
} else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
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.
- mDisplayAll = true;
final String type = intent.resolveType(this);
if (Contacts.CONTENT_TYPE.equals(type)) {
mMode = MODE_PICK_AGGREGATE;
@@ -585,8 +583,6 @@
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));
@@ -609,7 +605,6 @@
// Load the preferences
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- mDisplayAll = prefs.getBoolean(Prefs.DISPLAY_ALL, Prefs.DISPLAY_ALL_DEFAULT);
mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
@@ -1248,14 +1243,11 @@
* {@link #mDisplayAll} and {@link #mDisplayOnlyPhones} flags.
*/
private String getAggregateSelection() {
- if (!mDisplayAll && mDisplayOnlyPhones) {
+ if (mDisplayOnlyPhones) {
return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES;
- } else if (!mDisplayAll) {
+ } else {
return CLAUSE_ONLY_VISIBLE;
- } else if (mDisplayOnlyPhones) {
- return CLAUSE_ONLY_PHONES;
}
- return null;
}
private Uri getAggregateFilterUri(String filter) {
diff --git a/src/com/android/contacts/DisplayGroupsActivity.java b/src/com/android/contacts/DisplayGroupsActivity.java
deleted file mode 100644
index 7215966..0000000
--- a/src/com/android/contacts/DisplayGroupsActivity.java
+++ /dev/null
@@ -1,640 +0,0 @@
-/*
- * 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.util.NotifyingAsyncQueryHandler;
-
-import android.app.ExpandableListActivity;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.EntityIterator;
-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
- NotifyingAsyncQueryHandler.AsyncQueryListener, 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);
- }
-
- /** {@inheritDoc} */
- 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);
- }
- }
-
- /** {@inheritDoc} */
- public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
- // No actions
- }
-
- /**
- * 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_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_RES_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_RES_TITLE)) {
- final String packageName = mCursor.getString(Projections.COL_RES_PACKAGE);
- final int titleRes = mCursor.getInt(Projections.COL_RES_TITLE);
- 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.TITLE,
- Groups.RES_PACKAGE,
- Groups.TITLE_RES,
- Groups.GROUP_VISIBLE,
- Groups.SUMMARY_COUNT,
- Groups.SUMMARY_WITH_PHONES,
- };
-
- public static final String SORT_ORDER = Groups.ACCOUNT_TYPE + " ASC, "
- + Groups.ACCOUNT_NAME + " ASC";
-
- public static final int COL_ID = 0;
- public static final int COL_TITLE = 1;
- public static final int COL_RES_PACKAGE = 2;
- public static final int COL_RES_TITLE = 3;
- public static final int COL_GROUP_VISIBLE = 4;
- public static final int COL_SUMMARY_COUNT = 5;
- public static final int COL_SUMMARY_WITH_PHONES = 6;
-
- }
-}
diff --git a/src/com/android/contacts/EdgeTriggerView.java b/src/com/android/contacts/EdgeTriggerView.java
deleted file mode 100644
index d40dbad..0000000
--- a/src/com/android/contacts/EdgeTriggerView.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * 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 android.content.Context;
-import android.content.res.TypedArray;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
-import android.widget.FrameLayout;
-
-/**
- * Lightweight view that wraps around an existing view, watching and capturing
- * sliding gestures from the left or right edges within a given tolerance.
- */
-public class EdgeTriggerView extends FrameLayout {
- public static final int FLAG_NONE = 0x00;
- public static final int FLAG_LEFT = 0x01;
- public static final int FLAG_RIGHT = 0x02;
-
- private int mTouchSlop;
-
- private int mEdgeWidth;
- private int mListenEdges;
-
- private boolean mListenLeft = false;
- private boolean mListenRight = false;
-
- private MotionEvent mDownStart;
- private int mEdge = FLAG_NONE;
-
- public static interface EdgeTriggerListener {
- public void onTrigger(float downX, float downY, int edge);
- }
-
- private EdgeTriggerListener mListener;
-
- /**
- * Add a {@link EdgeTriggerListener} to watch for edge events.
- */
- public void setOnEdgeTriggerListener(EdgeTriggerListener listener) {
- mListener = listener;
- }
-
- public EdgeTriggerView(Context context) {
- this(context, null);
- }
-
- public EdgeTriggerView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public EdgeTriggerView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- setClickable(true);
-
- mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
-
- // TODO: enable reading these values again once we move away from symlinks
-// TypedArray a = context.obtainStyledAttributes(attrs,
-// R.styleable.EdgeTriggerView, defStyle, -1);
-// mEdgeWidth = a.getDimensionPixelSize(R.styleable.EdgeTriggerView_edgeWidth, mTouchSlop);
-// mListenEdges = a.getInt(R.styleable.EdgeTriggerView_listenEdges, FLAG_LEFT);
-
- mEdgeWidth = 80;
- mListenEdges = FLAG_LEFT;
-
- mListenLeft = (mListenEdges & FLAG_LEFT) == FLAG_LEFT;
- mListenRight = (mListenEdges & FLAG_RIGHT) == FLAG_RIGHT;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean onInterceptTouchEvent(MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN: {
- // Consider watching this touch event based on listening flags
- final float x = event.getX();
- if (mListenLeft && x < mEdgeWidth) {
- mEdge = FLAG_LEFT;
- } else if (mListenRight && x > getWidth() - mEdgeWidth) {
- mEdge = FLAG_RIGHT;
- } else {
- mEdge = FLAG_NONE;
- }
-
- if (mEdge != FLAG_NONE) {
- mDownStart = MotionEvent.obtain(event);
- } else {
- mDownStart = null;
- }
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- if (mEdge != FLAG_NONE) {
- // If moved far enough, capture touch event for ourselves
- float delta = event.getX() - mDownStart.getX();
- if (mEdge == FLAG_LEFT && delta > mTouchSlop) {
- return true;
- } else if (mEdge == FLAG_RIGHT && delta < -mTouchSlop) {
- return true;
- }
- }
- break;
- }
- }
-
- // Otherwise let the event slip through to children
- return false;
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- // Pass trigger event to listener and return true to consume
- if (mEdge != FLAG_NONE && mListener != null) {
- mListener.onTrigger(mDownStart.getX(), mDownStart.getY(), mEdge);
-
- // Reset values so we don't sent twice
- mEdge = FLAG_NONE;
- mDownStart = null;
- }
- return true;
- }
-}
diff --git a/src/com/android/contacts/SocialStreamActivity.java b/src/com/android/contacts/SocialStreamActivity.java
index a3adf76..11f9367 100644
--- a/src/com/android/contacts/SocialStreamActivity.java
+++ b/src/com/android/contacts/SocialStreamActivity.java
@@ -16,7 +16,6 @@
package com.android.contacts;
-import com.android.contacts.EdgeTriggerView.EdgeTriggerListener;
import com.android.contacts.ui.FastTrackWindow;
import org.xmlpull.v1.XmlPullParser;
@@ -71,7 +70,7 @@
import java.util.HashMap;
import java.util.List;
-public class SocialStreamActivity extends ListActivity implements OnClickListener, EdgeTriggerListener {
+public class SocialStreamActivity extends ListActivity implements OnClickListener {
private static final String TAG = "SocialStreamActivity";
private static final String[] PROJ_ACTIVITIES = new String[] {
@@ -108,12 +107,9 @@
private SocialAdapter mAdapter;
private ListView mListView;
- private EdgeTriggerView mEdgeTrigger;
private FastTrackWindow mFastTrack;
private MappingCache mMappingCache;
- private static final boolean USE_GESTURE = false;
-
private ContactsCache mContactsCache;
@Override
@@ -133,12 +129,6 @@
mListView = getListView();
mFastTrack = new FastTrackWindow(this);
-
- if (USE_GESTURE) {
- // Find and listen for edge triggers
- mEdgeTrigger = (EdgeTriggerView)findViewById(R.id.edge_trigger);
- mEdgeTrigger.setOnEdgeTriggerListener(this);
- }
}
/** {@inheritDoc} */
@@ -147,23 +137,6 @@
showFastTrack(v, (Long)v.getTag());
}
- /** {@inheritDoc} */
- public void onTrigger(float downX, float downY, int edge) {
- // Find list item user triggered over
- final int position = mListView.pointToPosition((int)downX, (int)downY);
- if (position == ListView.INVALID_POSITION) return;
-
- // Reverse to find the exact top of the triggered entry
- final int index = position - mListView.getFirstVisiblePosition();
- final View anchor = mListView.getChildAt(index);
-
- Cursor cursor = (Cursor)mAdapter.getItem(position);
- long aggId = cursor.getLong(COL_AGGREGATE_ID);
-
- showFastTrack(anchor, aggId);
-
- }
-
private int[] mLocation = new int[2];
private Rect mRect = new Rect();
@@ -335,9 +308,7 @@
holder.published = (TextView) view.findViewById(R.id.published);
view.setTag(holder);
- if (!USE_GESTURE) {
- holder.photo.setOnClickListener(mPhotoListener);
- }
+ holder.photo.setOnClickListener(mPhotoListener);
return view;
}
@@ -409,7 +380,10 @@
* Store a mapping from a package name and mime-type pair to a set of
* {@link RemoteViews}, default icon, and column to use from the
* {@link Data} table to use as a summary.
+ *
+ * @deprecated use {@link ContactsSource} instead
*/
+ @Deprecated
public static class Mapping {
String packageName;
String mimeType;
@@ -431,7 +405,10 @@
* Store a parsed <code>Mapping</code> object, which maps package and
* mime-type combinations to {@link RemoteViews} XML resources, default
* icons, and summary columns in the {@link Data} table.
+ *
+ * @deprecated use {@link Sources} instead
*/
+ @Deprecated
public static class MappingCache extends HashMap<String, Mapping> {
private static final String TAG = "MappingCache";
@@ -603,12 +580,14 @@
* The size of the thumbnail is defined by the dimension
* android.R.dimen.launcher_application_icon_size. This method is not
* thread-safe and should be invoked on the UI thread only.
- *
+ *
* @param bitmap The bitmap to get a thumbnail of.
* @param context The application's context.
* @return A thumbnail for the specified bitmap or the bitmap itself if
* the thumbnail could not be created.
+ * @deprecated use {@link Bitmap#createScaledBitmap} instead.
*/
+ @Deprecated
static Bitmap createBitmapThumbnail(Bitmap bitmap, Context context, int size) {
int width = size;
int height = size;
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index 8611bb3..b9bd4ae 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -158,6 +158,15 @@
throw new UnsupportedOperationException("Custom constraint parser not implemented");
}
+ public CharSequence getDisplayLabel(Context context) {
+ if (this.titleRes > 0) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.getText(this.resPackageName, this.titleRes, null);
+ } else {
+ return this.accountType;
+ }
+ }
+
/**
* {@link Comparator} to sort by {@link DataKind#weight}.
*/
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index 196a306..e19d1b6 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -122,25 +122,27 @@
}
throw new IllegalStateException("Couldn't find authenticator for specific account type");
}
-
+
/**
* Return list of all known, writable {@link ContactsSource}. Sources
* returned may require inflation before they can be used.
*/
- public ArrayList<Account> getWritableAccounts() {
+ public ArrayList<Account> getAccounts(boolean writableOnly) {
final AccountManager am = AccountManager.get(mContext);
final Account[] accounts = am.getAccounts();
- final ArrayList<Account> writable = new ArrayList<Account>();
+ final ArrayList<Account> matching = new ArrayList<Account>();
for (Account account : accounts) {
// Ensure we have details loaded for each account
final ContactsSource source = getInflatedSource(account.type,
ContactsSource.LEVEL_SUMMARY);
- if (!source.readOnly) {
- writable.add(account);
+ final boolean hasContacts = source != null;
+ final boolean matchesWritable = (!writableOnly || (writableOnly && !source.readOnly));
+ if (hasContacts && matchesWritable) {
+ matching.add(account);
}
}
- return writable;
+ return matching;
}
protected ContactsSource getSourceForType(String accountType) {
@@ -157,7 +159,7 @@
*/
public ContactsSource getInflatedSource(String accountType, int inflateLevel) {
final ContactsSource source = getSourceForType(accountType);
- if (source.isInflated(inflateLevel)) {
+ if (source == null || source.isInflated(inflateLevel)) {
// Found inflated, so return directly
return source;
} else {
diff --git a/src/com/android/contacts/ui/DisplayGroupsActivity.java b/src/com/android/contacts/ui/DisplayGroupsActivity.java
new file mode 100644
index 0000000..b202127
--- /dev/null
+++ b/src/com/android/contacts/ui/DisplayGroupsActivity.java
@@ -0,0 +1,702 @@
+/*
+ * 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.ui;
+
+import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.util.WeakAsyncTask;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ExpandableListActivity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageManager;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.Settings;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.widget.AdapterView;
+import android.widget.CheckBox;
+import android.widget.CursorTreeAdapter;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.RadioButton;
+import android.widget.TextView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+
+/**
+ * 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
+ AdapterView.OnItemClickListener {
+ private static final String TAG = "DisplayGroupsActivity";
+
+ private static final int UNGROUPED_ID = -2;
+
+ public interface Prefs {
+ 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 RadioButton mDisplayAll;
+ private RadioButton mDisplayPhones;
+
+ 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);
+
+ 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);
+ mDisplayAll = (RadioButton)mHeaderAll.findViewById(android.R.id.checkbox);
+ {
+ final TextView text1 = (TextView)mHeaderAll.findViewById(android.R.id.text1);
+ final TextView text2 = (TextView)mHeaderAll.findViewById(android.R.id.text2);
+ text1.setText(R.string.showAllGroups);
+ text2.setVisibility(View.GONE);
+ }
+ 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);
+ mDisplayPhones = (RadioButton)mHeaderPhones.findViewById(android.R.id.checkbox);
+ {
+ 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);
+ }
+ mList.addHeaderView(mHeaderPhones, null, true);
+
+
+ // 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);
+ }
+ mList.addHeaderView(mHeaderSeparator, null, false);
+
+ mAdapter = new DisplayGroupsAdapter(null, this, this);
+
+ boolean displayOnlyPhones = mPrefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
+ Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
+
+ mDisplayAll.setChecked(!displayOnlyPhones);
+ mDisplayPhones.setChecked(displayOnlyPhones);
+
+ mAdapter.setChildDescripWithPhones(displayOnlyPhones);
+
+ setListAdapter(mAdapter);
+
+ // Catch clicks on the header views
+ mList.setOnItemClickListener(this);
+ mList.setOnCreateContextMenuListener(this);
+
+ // Start background query to find account details
+ new QuerySettingsTask(this).execute();
+ }
+
+ private static class QuerySettingsTask extends
+ WeakAsyncTask<Void, Void, Cursor, DisplayGroupsActivity> {
+ public QuerySettingsTask(DisplayGroupsActivity target) {
+ super(target);
+ }
+
+ @Override
+ protected Cursor doInBackground(DisplayGroupsActivity target, Void... params) {
+ final Context context = target;
+ final Sources sources = Sources.getInstance(context);
+
+ // Query to find Settings for all data sources
+ final ContentResolver resolver = context.getContentResolver();
+ final Cursor cursor = resolver.query(ContactsContract.Settings.CONTENT_URI,
+ SettingsQuery.PROJECTION, null, null, null);
+ target.startManagingCursor(cursor);
+
+ // Make records for each account known by Settings
+ final HashSet<Account> knownAccounts = new HashSet<Account>();
+ while (cursor.moveToNext()) {
+ final String accountName = cursor.getString(SettingsQuery.ACCOUNT_NAME);
+ final String accountType = cursor.getString(SettingsQuery.ACCOUNT_TYPE);
+ final Account account = new Account(accountName, accountType);
+ knownAccounts.add(account);
+ }
+
+ // Assert that Settings exist for each data source
+ boolean changedSettings = false;
+ final ArrayList<Account> expectedAccounts = sources.getAccounts(false);
+ for (Account account : expectedAccounts) {
+ if (!knownAccounts.contains(account)) {
+ // Expected account that doesn't exist yet in Settings
+ final ContentValues values = new ContentValues();
+ values.put(Settings.ACCOUNT_NAME, account.name);
+ values.put(Settings.ACCOUNT_TYPE, account.type);
+ resolver.insert(Settings.CONTENT_URI, values);
+
+ // Make sure we requery to catch this insert
+ changedSettings = true;
+ }
+ }
+
+ if (changedSettings) {
+ // Catch any new sources discovered above
+ cursor.requery();
+ }
+
+ // Wrap cursor to provide _id column
+ final Cursor settingsCursor = new CursorWrapper(cursor) {
+ @Override
+ public long getLong(int columnIndex) {
+ if (columnIndex == -1) {
+ return this.getPosition();
+ } else {
+ return super.getLong(columnIndex);
+ }
+ }
+ };
+
+ return settingsCursor;
+ }
+
+ @Override
+ protected void onPostExecute(DisplayGroupsActivity target, Cursor result) {
+ // Update cursor for data sources
+ target.mAdapter.setGroupCursor(result);
+ }
+ }
+
+ /**
+ * 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) {
+ switch (view.getId()) {
+ case R.id.header_all: {
+ mDisplayAll.toggle();
+ setDisplayOnlyPhones(!mDisplayAll.isChecked());
+ break;
+ }
+ case R.id.header_phones: {
+ mDisplayPhones.toggle();
+ setDisplayOnlyPhones(mDisplayPhones.isChecked());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Assign a specific value to {@link Prefs#DISPLAY_ONLY_PHONES}, refreshing
+ * the visible list as needed.
+ */
+ protected void setDisplayOnlyPhones(boolean displayOnlyPhones) {
+ mDisplayAll.setChecked(!displayOnlyPhones);
+ mDisplayPhones.setChecked(displayOnlyPhones);
+
+ Editor editor = mPrefs.edit();
+ editor.putBoolean(Prefs.DISPLAY_ONLY_PHONES, displayOnlyPhones);
+ editor.commit();
+
+ 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 v, int groupPosition,
+ int childPosition, long id) {
+ 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();
+
+ // TODO: heavy update, perhaps push to background query
+ if (id != UNGROUPED_ID) {
+ // Handle persisting for normal group
+ values.put(Groups.GROUP_VISIBLE, checkbox.isChecked() ? 1 : 0);
+
+ final Uri groupUri = ContentUris.withAppendedId(Groups.CONTENT_URI, id);
+ final int count = resolver.update(groupUri, values, null, null);
+ } else {
+ // Handle persisting for ungrouped through Settings
+ values.put(Settings.UNGROUPED_VISIBLE, checkbox.isChecked() ? 1 : 0);
+
+ final Cursor settings = mAdapter.getGroup(groupPosition);
+ final int count = resolver.update(Settings.CONTENT_URI, values, Groups.ACCOUNT_NAME
+ + "=? AND " + Groups.ACCOUNT_TYPE + "=?", new String[] {
+ settings.getString(SettingsQuery.ACCOUNT_NAME),
+ settings.getString(SettingsQuery.ACCOUNT_TYPE)
+ });
+ }
+
+ 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;
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, 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);
+
+ final Cursor groupCursor = mAdapter.getGroup(groupPosition);
+ // TODO: read sync mode through <sync-adapter> definition
+ final int syncMode = SYNC_MODE_EVERYTHING;
+
+ // Ignore when selective syncing unsupported
+ if (syncMode == SYNC_MODE_UNSUPPORTED) return;
+
+ final String accountName = groupCursor.getString(SettingsQuery.ACCOUNT_NAME);
+ final String accountType = groupCursor.getString(SettingsQuery.ACCOUNT_TYPE);
+ final Account account = new Account(accountName, accountType);
+
+ if (childPosition == -1) {
+ // Show add dialog for this overall source
+ showAddSync(menu, groupCursor, account, syncMode);
+
+ } else {
+ // Show remove dialog for this specific group
+ final Cursor childCursor = mAdapter.getChild(groupPosition, childPosition);
+ showRemoveSync(menu, account, childCursor, syncMode);
+ }
+ }
+
+ protected void showRemoveSync(ContextMenu menu, final Account account, Cursor childCursor,
+ final int syncMode) {
+ final long groupId = childCursor.getLong(GroupsQuery._ID);
+ final CharSequence title = getGroupTitle(this, childCursor);
+
+ menu.setHeaderTitle(title);
+ menu.add(R.string.menu_sync_remove).setOnMenuItemClickListener(
+ new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ handleRemoveSync(groupId, account, syncMode, title);
+ return true;
+ }
+ });
+ }
+
+ protected void handleRemoveSync(final long groupId, final Account account, final int syncMode,
+ CharSequence title) {
+ if (syncMode == SYNC_MODE_EVERYTHING && groupId != UNGROUPED_ID) {
+ // 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.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 this group to not sync
+ setGroupShouldSync(groupId, account, syncMode, false);
+ }
+ });
+ builder.show();
+ } else {
+ // Mark this group to not sync
+ setGroupShouldSync(groupId, account, syncMode, false);
+ }
+ }
+
+ protected void showAddSync(ContextMenu menu, Cursor groupCursor, final Account account, final int syncMode) {
+ menu.setHeaderTitle(R.string.menu_sync_add);
+
+ // Create single "Ungrouped" item when not synced
+ final boolean ungroupedAvailable = groupCursor.getInt(SettingsQuery.SHOULD_SYNC) == 0;
+ if (ungroupedAvailable) {
+ menu.add(R.string.display_ungrouped).setOnMenuItemClickListener(
+ new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // Adding specific group for syncing
+ setGroupShouldSync(UNGROUPED_ID, account, syncMode, true);
+ return true;
+ }
+ });
+ }
+
+ // Create item for each available, unsynced group
+ final Cursor availableGroups = this.managedQuery(Groups.CONTENT_SUMMARY_URI,
+ GroupsQuery.PROJECTION, Groups.SHOULD_SYNC + "=0", null);
+ while (availableGroups.moveToNext()) {
+ // Create item this unsynced group
+ final long groupId = availableGroups.getLong(GroupsQuery._ID);
+ final CharSequence title = getGroupTitle(this, availableGroups);
+ menu.add(title).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // Adding specific group for syncing
+ setGroupShouldSync(groupId, account, syncMode, true);
+ return true;
+ }
+ });
+ }
+ }
+
+ /**
+ * Mark the {@link Groups#SHOULD_SYNC} state of the given group.
+ */
+ protected void setGroupShouldSync(long groupId, Account account, int syncMode, boolean shouldSync) {
+ final ContentResolver resolver = getContentResolver();
+ final ContentValues values = new ContentValues();
+
+ if (syncMode == SYNC_MODE_UNSUPPORTED) {
+ // Ignore changes when source doesn't support syncing
+ return;
+ }
+
+ if (groupId == UNGROUPED_ID) {
+ // Updating the overall syncing flag for this account
+ values.put(Settings.SHOULD_SYNC, shouldSync ? 1 : 0);
+ resolver.update(Settings.CONTENT_URI, values, Settings.ACCOUNT_NAME + "=? AND "
+ + Settings.ACCOUNT_TYPE + "=?", new String[] {
+ account.name, account.type
+ });
+
+ if (syncMode == SYNC_MODE_EVERYTHING && shouldSync) {
+ // If syncing mode is everything, force-enable all children groups
+ values.clear();
+ values.put(Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
+ resolver.update(Groups.CONTENT_URI, values, Groups.ACCOUNT_NAME + "=? AND "
+ + Groups.ACCOUNT_TYPE + "=?", new String[] {
+ account.name, account.type
+ });
+ }
+ } else {
+ // Treat as normal group
+ values.put(Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
+ resolver.update(Groups.CONTENT_URI, values, Groups._ID + "=" + groupId, null);
+
+ if (syncMode == SYNC_MODE_EVERYTHING && !shouldSync) {
+ // Remove "everything" from sync, user has already been warned
+ values.clear();
+ values.put(Settings.SHOULD_SYNC, shouldSync ? 1 : 0);
+ resolver.update(Settings.CONTENT_URI, values, Settings.ACCOUNT_NAME + "=? AND "
+ + Settings.ACCOUNT_TYPE + "=?", new String[] {
+ account.name, account.type
+ });
+ }
+ }
+ }
+
+ /**
+ * Return the best title for the {@link Groups} entry at the current
+ * {@link Cursor} position.
+ */
+ protected static CharSequence getGroupTitle(Context context, Cursor cursor) {
+ final PackageManager pm = context.getPackageManager();
+ if (!cursor.isNull(GroupsQuery.TITLE_RES)) {
+ final String packageName = cursor.getString(GroupsQuery.RES_PACKAGE);
+ final int titleRes = cursor.getInt(GroupsQuery.TITLE_RES);
+ return pm.getText(packageName, titleRes, null);
+ } else {
+ return cursor.getString(GroupsQuery.TITLE);
+ }
+ }
+
+ /**
+ * Special {@link Cursor} that shows zero or one items based on
+ * {@link Settings#SHOULD_SYNC} value. This header only supports
+ * {@link #SYNC_MODE_UNGROUPED} and {@link #SYNC_MODE_UNSUPPORTED}.
+ */
+ private static class HeaderCursor extends AbstractCursor {
+ private Context mContext;
+ private Cursor mCursor;
+ private int mPosition;
+
+ public HeaderCursor(Context context, Cursor cursor, int position) {
+ mContext = context;
+ mCursor = cursor;
+ mPosition = position;
+ }
+
+ @Override
+ public int getCount() {
+ assertParent();
+
+ final boolean shouldSync = mCursor.getInt(SettingsQuery.SHOULD_SYNC) != 0;
+ return shouldSync ? 1 : 0;
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return GroupsQuery.PROJECTION;
+ }
+
+ protected void assertParent() {
+ mCursor.moveToPosition(mPosition);
+ }
+
+ @Override
+ public String getString(int column) {
+ assertParent();
+ switch(column) {
+ case GroupsQuery.ACCOUNT_NAME:
+ return mCursor.getString(SettingsQuery.ACCOUNT_NAME);
+ case GroupsQuery.ACCOUNT_TYPE:
+ return mCursor.getString(SettingsQuery.ACCOUNT_TYPE);
+ case GroupsQuery.TITLE:
+ return null;
+ case GroupsQuery.RES_PACKAGE:
+ return mContext.getPackageName();
+ case GroupsQuery.TITLE_RES:
+ return Integer.toString(UNGROUPED_ID);
+ }
+ throw new IllegalArgumentException("Requested column not available as string");
+ }
+
+ @Override
+ public short getShort(int column) {
+ throw new IllegalArgumentException("Requested column not available as short");
+ }
+
+ @Override
+ public int getInt(int column) {
+ assertParent();
+ switch(column) {
+ case GroupsQuery._ID:
+ return UNGROUPED_ID;
+ case GroupsQuery.TITLE_RES:
+ return R.string.display_ungrouped;
+ case GroupsQuery.GROUP_VISIBLE:
+ return mCursor.getInt(SettingsQuery.UNGROUPED_VISIBLE);
+ case GroupsQuery.SUMMARY_COUNT:
+ return mCursor.getInt(SettingsQuery.UNGROUPED_COUNT);
+ case GroupsQuery.SUMMARY_WITH_PHONES:
+ return mCursor.getInt(SettingsQuery.UNGROUPED_WITH_PHONES);
+ }
+ throw new IllegalArgumentException("Requested column not available as int");
+ }
+
+ @Override
+ public long getLong(int column) {
+ return getInt(column);
+ }
+
+ @Override
+ public float getFloat(int column) {
+ throw new IllegalArgumentException("Requested column not available as float");
+ }
+
+ @Override
+ public double getDouble(int column) {
+ throw new IllegalArgumentException("Requested column not available as double");
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return getString(column) == null;
+ }
+ }
+
+ /**
+ * 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 Account}.
+ */
+ private static class DisplayGroupsAdapter extends CursorTreeAdapter {
+ private Context mContext;
+ private Activity mActivity;
+ private LayoutInflater mInflater;
+ private Sources mSources;
+
+ private boolean mChildWithPhones = false;
+
+ public DisplayGroupsAdapter(Cursor cursor, Context context, Activity activity) {
+ super(cursor, context, true);
+
+ mContext = context;
+ mActivity = activity;
+ mSources = Sources.getInstance(mContext);
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ /**
+ * In group descriptions, show the number of contacts with phone
+ * numbers, in addition to the total contacts.
+ */
+ public void setChildDescripWithPhones(boolean withPhones) {
+ mChildWithPhones = withPhones;
+ }
+
+ @Override
+ protected View newGroupView(Context context, Cursor cursor, boolean isExpanded,
+ ViewGroup parent) {
+ return mInflater.inflate(R.layout.display_group, parent, false);
+ }
+
+ @Override
+ protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
+ final TextView text1 = (TextView)view.findViewById(android.R.id.text1);
+ final TextView text2 = (TextView)view.findViewById(android.R.id.text2);
+
+ final String accountName = cursor.getString(SettingsQuery.ACCOUNT_NAME);
+ final String accountType = cursor.getString(SettingsQuery.ACCOUNT_TYPE);
+
+ final ContactsSource source = mSources.getInflatedSource(accountType,
+ ContactsSource.LEVEL_SUMMARY);
+
+ text1.setText(source.getDisplayLabel(mContext));
+ text2.setText(accountName);
+ text2.setVisibility(accountName == null ? View.GONE : View.VISIBLE);
+ }
+
+ @Override
+ protected Cursor getChildrenCursor(Cursor groupCursor) {
+ final String selection = Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE
+ + "=? AND " + Groups.SHOULD_SYNC + "=1";
+ final String[] selectionArgs = new String[] {
+ groupCursor.getString(SettingsQuery.ACCOUNT_NAME),
+ groupCursor.getString(SettingsQuery.ACCOUNT_TYPE)
+ };
+
+ final int position = groupCursor.getPosition();
+ final Cursor ungroupedCursor = new HeaderCursor(mContext, groupCursor, position);
+
+ final ContentResolver resolver = mContext.getContentResolver();
+ final Cursor groupsCursor = resolver.query(Groups.CONTENT_SUMMARY_URI,
+ GroupsQuery.PROJECTION, selection, selectionArgs, null);
+ mActivity.startManagingCursor(groupsCursor);
+
+ return new MergeCursor(new Cursor[] { ungroupedCursor, groupsCursor });
+ }
+
+ @Override
+ protected View newChildView(Context context, Cursor cursor, boolean isLastChild,
+ ViewGroup parent) {
+ return mInflater.inflate(R.layout.display_child, parent, false);
+ }
+
+ @Override
+ protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
+ final TextView text1 = (TextView)view.findViewById(android.R.id.text1);
+ final TextView text2 = (TextView)view.findViewById(android.R.id.text2);
+ final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
+
+ final int count = cursor.getInt(GroupsQuery.SUMMARY_COUNT);
+ final int withPhones = cursor.getInt(GroupsQuery.SUMMARY_WITH_PHONES);
+ final int membersVisible = cursor.getInt(GroupsQuery.GROUP_VISIBLE);
+
+ // Read title, but override with string resource when present
+ final CharSequence title = getGroupTitle(mContext, cursor);
+ final CharSequence descrip = mContext.getResources().getQuantityString(
+ mChildWithPhones ? R.plurals.groupDescripPhones : R.plurals.groupDescrip,
+ count, count, withPhones);
+
+ text1.setText(title);
+ text2.setText(descrip);
+ checkbox.setChecked((membersVisible == 1));
+ }
+ }
+
+ private interface SettingsQuery {
+ final String[] PROJECTION = new String[] {
+ Settings.ACCOUNT_NAME,
+ Settings.ACCOUNT_TYPE,
+ Settings.SHOULD_SYNC,
+ Settings.UNGROUPED_VISIBLE,
+ Settings.UNGROUPED_COUNT,
+ Settings.UNGROUPED_WITH_PHONES,
+ };
+
+ final int ACCOUNT_NAME = 0;
+ final int ACCOUNT_TYPE = 1;
+ final int SHOULD_SYNC = 2;
+ final int UNGROUPED_VISIBLE = 3;
+ final int UNGROUPED_COUNT = 4;
+ final int UNGROUPED_WITH_PHONES = 5;
+ }
+
+ private interface GroupsQuery {
+ final String[] PROJECTION = new String[] {
+ Groups._ID,
+ Groups.TITLE,
+ Groups.RES_PACKAGE,
+ Groups.TITLE_RES,
+ Groups.GROUP_VISIBLE,
+ Groups.SUMMARY_COUNT,
+ Groups.SUMMARY_WITH_PHONES,
+ Groups.ACCOUNT_NAME,
+ Groups.ACCOUNT_TYPE,
+ };
+
+ final int _ID = 0;
+ final int TITLE = 1;
+ final int RES_PACKAGE = 2;
+ final int TITLE_RES = 3;
+ final int GROUP_VISIBLE = 4;
+ final int SUMMARY_COUNT = 5;
+ final int SUMMARY_WITH_PHONES = 6;
+ final int ACCOUNT_NAME = 7;
+ final int ACCOUNT_TYPE = 8;
+ }
+}
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index a5b1a47..4fa6f01 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -48,6 +48,7 @@
import android.content.EntityIterator;
import android.content.Intent;
import android.content.OperationApplicationException;
+import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
@@ -659,7 +660,7 @@
final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- final ArrayList<Account> writable = sources.getWritableAccounts();
+ final ArrayList<Account> writable = sources.getAccounts(true);
final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(target,
android.R.layout.simple_list_item_2, writable) {
@Override
@@ -676,9 +677,8 @@
final Account account = this.getItem(position);
final ContactsSource source = sources.getInflatedSource(account.type,
ContactsSource.LEVEL_SUMMARY);
- if (source.titleRes > 0) {
- text1.setText(source.titleRes);
- }
+
+ text1.setText(source.getDisplayLabel(target));
text2.setText(account.name);
return convertView;