| /* |
| * 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.ContactsSearchManager; |
| import com.android.contacts.R; |
| import com.android.contacts.model.ContactsSource; |
| import com.android.contacts.model.GoogleSource; |
| import com.android.contacts.model.Sources; |
| import com.android.contacts.model.EntityDelta.ValuesDelta; |
| import com.android.contacts.util.EmptyService; |
| 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.Dialog; |
| import android.app.ExpandableListActivity; |
| import android.app.ProgressDialog; |
| import android.content.ContentProviderOperation; |
| 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.ContentProviderOperation.Builder; |
| 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.View; |
| import android.view.ViewGroup; |
| import android.view.MenuItem.OnMenuItemClickListener; |
| import android.widget.AdapterView; |
| import android.widget.BaseExpandableListAdapter; |
| import android.widget.CheckBox; |
| import android.widget.ExpandableListAdapter; |
| import android.widget.ExpandableListView; |
| import android.widget.ListView; |
| import android.widget.TextView; |
| import android.widget.ExpandableListView.ExpandableListContextMenuInfo; |
| |
| 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 ContactsPreferencesActivity extends ExpandableListActivity implements |
| AdapterView.OnItemClickListener, View.OnClickListener { |
| private static final String TAG = "DisplayGroupsActivity"; |
| |
| public interface Prefs { |
| public static final String DISPLAY_ONLY_PHONES = "only_phones"; |
| public static final boolean DISPLAY_ONLY_PHONES_DEFAULT = false; |
| |
| } |
| |
| private static final int DIALOG_SORT_ORDER = 1; |
| private static final int DIALOG_DISPLAY_ORDER = 2; |
| |
| private ExpandableListView mList; |
| private DisplayAdapter mAdapter; |
| |
| private SharedPreferences mPrefs; |
| private ContactsPreferences mContactsPrefs; |
| |
| private CheckBox mDisplayPhones; |
| |
| private View mHeaderPhones; |
| private View mHeaderSeparator; |
| |
| private View mSortOrderView; |
| private TextView mSortOrderTextView; |
| private int mSortOrder; |
| |
| private View mDisplayOrderView; |
| private TextView mDisplayOrderTextView; |
| private int mDisplayOrder; |
| |
| @Override |
| protected void onCreate(Bundle icicle) { |
| super.onCreate(icicle); |
| setContentView(R.layout.contacts_preferences); |
| |
| mList = getExpandableListView(); |
| mList.setHeaderDividersEnabled(true); |
| mPrefs = PreferenceManager.getDefaultSharedPreferences(this); |
| mContactsPrefs = new ContactsPreferences(this); |
| mAdapter = new DisplayAdapter(this); |
| |
| final LayoutInflater inflater = getLayoutInflater(); |
| |
| createWithPhonesOnlyPreferenceView(inflater); |
| createSortOrderPreferenceView(inflater); |
| createDisplayOrderPreferenceView(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); |
| |
| mSortOrder = mContactsPrefs.getSortOrder(); |
| mDisplayOrder = mContactsPrefs.getDisplayOrder(); |
| } |
| |
| 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(Prefs.DISPLAY_ONLY_PHONES, |
| Prefs.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 createSortOrderPreferenceView(LayoutInflater inflater) { |
| mSortOrderView = inflater.inflate(R.layout.preference_with_more_button, mList, false); |
| |
| View preferenceLayout = mSortOrderView.findViewById(R.id.preference); |
| |
| TextView label = (TextView)preferenceLayout.findViewById(R.id.label); |
| label.setText(getString(R.string.display_options_sort_list_by)); |
| |
| mSortOrderTextView = (TextView)preferenceLayout.findViewById(R.id.data); |
| } |
| |
| private void createDisplayOrderPreferenceView(LayoutInflater inflater) { |
| mDisplayOrderView = inflater.inflate(R.layout.preference_with_more_button, mList, false); |
| View preferenceLayout = mDisplayOrderView.findViewById(R.id.preference); |
| |
| TextView label = (TextView)preferenceLayout.findViewById(R.id.label); |
| label.setText(getString(R.string.display_options_view_names_as)); |
| |
| mDisplayOrderTextView = (TextView)preferenceLayout.findViewById(R.id.data); |
| } |
| |
| 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(mSortOrderView); |
| mList.removeHeaderView(mDisplayOrderView); |
| 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); |
| if (mContactsPrefs.isSortOrderUserChangeable()) { |
| mList.addHeaderView(mSortOrderView, null, true); |
| } |
| |
| if (mContactsPrefs.isSortOrderUserChangeable()) { |
| mList.addHeaderView(mDisplayOrderView, null, true); |
| } |
| |
| mList.addHeaderView(mHeaderSeparator, null, false); |
| |
| setListAdapter(mAdapter); |
| |
| bindView(); |
| |
| // Start background query to find account details |
| new QueryGroupsTask(this).execute(); |
| } |
| |
| private void bindView() { |
| mSortOrderTextView.setText( |
| mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY |
| ? getString(R.string.display_options_sort_by_given_name) |
| : getString(R.string.display_options_sort_by_family_name)); |
| |
| mDisplayOrderTextView.setText( |
| mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY |
| ? getString(R.string.display_options_view_given_name_first) |
| : getString(R.string.display_options_view_family_name_first)); |
| } |
| |
| @Override |
| protected Dialog onCreateDialog(int id, Bundle args) { |
| switch (id) { |
| case DIALOG_SORT_ORDER: |
| return createSortOrderDialog(); |
| case DIALOG_DISPLAY_ORDER: |
| return createDisplayOrderDialog(); |
| } |
| |
| return null; |
| } |
| |
| private Dialog createSortOrderDialog() { |
| String[] items = new String[] { |
| getString(R.string.display_options_sort_by_given_name), |
| getString(R.string.display_options_sort_by_family_name), |
| }; |
| |
| return new AlertDialog.Builder(this) |
| .setIcon(com.android.internal.R.drawable.ic_dialog_menu_generic) |
| .setTitle(R.string.display_options_sort_list_by) |
| .setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int whichButton) { |
| setSortOrder(dialog); |
| dialog.dismiss(); |
| } |
| }) |
| .setNegativeButton(android.R.string.cancel, null) |
| .create(); |
| } |
| |
| private Dialog createDisplayOrderDialog() { |
| String[] items = new String[] { |
| getString(R.string.display_options_view_given_name_first), |
| getString(R.string.display_options_view_family_name_first), |
| }; |
| |
| return new AlertDialog.Builder(this) |
| .setIcon(com.android.internal.R.drawable.ic_dialog_menu_generic) |
| .setTitle(R.string.display_options_view_names_as) |
| .setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() { |
| public void onClick(DialogInterface dialog, int whichButton) { |
| setDisplayOrder(dialog); |
| dialog.dismiss(); |
| } |
| }) |
| .setNegativeButton(android.R.string.cancel, null) |
| .create(); |
| } |
| |
| @Override |
| protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { |
| switch (id) { |
| case DIALOG_SORT_ORDER: |
| setCheckedItem(dialog, |
| mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY ? 0 : 1); |
| break; |
| case DIALOG_DISPLAY_ORDER: |
| setCheckedItem(dialog, |
| mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY |
| ? 0 : 1); |
| break; |
| } |
| } |
| |
| private void setCheckedItem(Dialog dialog, int position) { |
| ListView listView = ((AlertDialog)dialog).getListView(); |
| listView.setItemChecked(position, true); |
| listView.setSelection(position); |
| } |
| |
| protected void setSortOrder(DialogInterface dialog) { |
| ListView listView = ((AlertDialog)dialog).getListView(); |
| int checked = listView.getCheckedItemPosition(); |
| mSortOrder = checked == 0 |
| ? ContactsContract.Preferences.SORT_ORDER_PRIMARY |
| : ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE; |
| |
| bindView(); |
| } |
| |
| protected void setDisplayOrder(DialogInterface dialog) { |
| ListView listView = ((AlertDialog)dialog).getListView(); |
| int checked = listView.getCheckedItemPosition(); |
| mDisplayOrder = checked == 0 |
| ? ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY |
| : ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE; |
| |
| bindView(); |
| } |
| |
| /** |
| * 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, ContactsPreferencesActivity> { |
| public QueryGroupsTask(ContactsPreferencesActivity target) { |
| super(target); |
| } |
| |
| @Override |
| protected AccountSet doInBackground(ContactsPreferencesActivity 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(ContactsPreferencesActivity 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); |
| } |
| |
| public CharSequence getTitle(Context context) { |
| if (mUngrouped) { |
| 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; |
| } |
| if (view == mDisplayOrderView) { |
| Log.d(TAG, "Showing Display Order dialog"); |
| showDialog(DIALOG_DISPLAY_ORDER); |
| return; |
| } |
| if (view == mSortOrderView) { |
| Log.d(TAG, "Showing Sort Order dialog"); |
| showDialog(DIALOG_SORT_ORDER); |
| 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 Prefs#DISPLAY_ONLY_PHONES}, refreshing |
| * the visible list as needed. |
| */ |
| protected void setDisplayOnlyPhones(boolean displayOnlyPhones) { |
| mDisplayPhones.setChecked(displayOnlyPhones); |
| |
| Editor editor = mPrefs.edit(); |
| editor.putBoolean(Prefs.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() { |
| mContactsPrefs.setSortOrder(mSortOrder); |
| mContactsPrefs.setDisplayOrder(mDisplayOrder); |
| |
| if (mAdapter == null || mAdapter.mAccounts == null) { |
| return; |
| } |
| setDisplayOnlyPhones(mDisplayPhones.isChecked()); |
| 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); |
| } |
| } |
| } |