| /* |
| * Copyright (C) 2009 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.contacts.list; |
| |
| import android.app.ActionBar; |
| import android.app.Activity; |
| import android.app.AlertDialog; |
| import android.app.Dialog; |
| import android.app.DialogFragment; |
| import android.app.LoaderManager.LoaderCallbacks; |
| import android.app.ProgressDialog; |
| import android.content.AsyncTaskLoader; |
| import android.content.ContentProviderOperation; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.Loader; |
| import android.content.OperationApplicationException; |
| import android.database.Cursor; |
| import android.graphics.Color; |
| import android.graphics.drawable.ColorDrawable; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| 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.Menu; |
| import android.view.MenuItem; |
| import android.view.MenuItem.OnMenuItemClickListener; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.BaseExpandableListAdapter; |
| import android.widget.CheckBox; |
| import android.widget.ExpandableListAdapter; |
| import android.widget.ExpandableListView; |
| import android.widget.ExpandableListView.ExpandableListContextMenuInfo; |
| import android.widget.TextView; |
| |
| import com.android.contacts.model.account.AccountDisplayInfo; |
| import com.android.contacts.model.account.AccountDisplayInfoFactory; |
| import com.android.contacts.common.R; |
| import com.android.contacts.model.AccountTypeManager; |
| import com.android.contacts.model.ValuesDelta; |
| import com.android.contacts.model.account.AccountType; |
| import com.android.contacts.model.account.AccountWithDataSet; |
| import com.android.contacts.model.account.GoogleAccountType; |
| import com.android.contacts.util.EmptyService; |
| import com.android.contacts.util.LocalizedNameResolver; |
| import com.android.contacts.util.WeakAsyncTask; |
| import com.google.common.collect.Lists; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** |
| * Shows a list of all available {@link Groups} available, letting the user |
| * select which ones they want to be visible. |
| */ |
| public class CustomContactListFilterActivity extends Activity implements |
| ExpandableListView.OnChildClickListener, |
| LoaderCallbacks<CustomContactListFilterActivity.AccountSet> { |
| private static final String TAG = "CustomContactListFilterActivity"; |
| |
| public static final String EXTRA_CURRENT_LIST_FILTER_TYPE = "currentListFilterType"; |
| |
| private static final int ACCOUNT_SET_LOADER_ID = 1; |
| |
| private ExpandableListView mList; |
| private DisplayAdapter mAdapter; |
| |
| @Override |
| protected void onCreate(Bundle icicle) { |
| super.onCreate(icicle); |
| setContentView(R.layout.contact_list_filter_custom); |
| |
| mList = (ExpandableListView) findViewById(android.R.id.list); |
| mList.setOnChildClickListener(this); |
| mList.setHeaderDividersEnabled(true); |
| mList.setChildDivider(new ColorDrawable(Color.TRANSPARENT)); |
| |
| mList.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange(final View v, final int left, final int top, final int right, |
| final int bottom, final int oldLeft, final int oldTop, final int oldRight, |
| final int oldBottom) { |
| mList.setIndicatorBounds( |
| mList.getWidth() - getResources().getDimensionPixelSize( |
| R.dimen.contact_filter_indicator_padding_end), |
| mList.getWidth() - getResources().getDimensionPixelSize( |
| R.dimen.contact_filter_indicator_padding_start)); |
| } |
| }); |
| |
| mAdapter = new DisplayAdapter(this); |
| |
| mList.setOnCreateContextMenuListener(this); |
| |
| mList.setAdapter(mAdapter); |
| |
| ActionBar actionBar = getActionBar(); |
| if (actionBar != null) { |
| // android.R.id.home will be triggered in onOptionsItemSelected() |
| actionBar.setDisplayHomeAsUpEnabled(true); |
| } |
| } |
| |
| public static class CustomFilterConfigurationLoader extends AsyncTaskLoader<AccountSet> { |
| |
| private AccountSet mAccountSet; |
| |
| public CustomFilterConfigurationLoader(Context context) { |
| super(context); |
| } |
| |
| @Override |
| public AccountSet loadInBackground() { |
| Context context = getContext(); |
| final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context); |
| final ContentResolver resolver = context.getContentResolver(); |
| |
| final AccountSet accounts = new AccountSet(); |
| |
| // Don't include the null account because it doesn't support writing to |
| // ContactsContract.Settings |
| final List<AccountWithDataSet> sourceAccounts = accountTypes.getAccounts( |
| AccountTypeManager.nonNullAccountFilter()); |
| final AccountDisplayInfoFactory displayableAccountFactory = |
| new AccountDisplayInfoFactory(context, sourceAccounts); |
| for (AccountWithDataSet account : sourceAccounts) { |
| final AccountType accountType = accountTypes.getAccountTypeForAccount(account); |
| if (accountType.isExtension() && !account.hasData(context)) { |
| // Extension with no data -- skip. |
| continue; |
| } |
| |
| AccountDisplay accountDisplay = |
| new AccountDisplay(resolver, account.name, account.type, account.dataSet, |
| displayableAccountFactory.getAccountDisplayInfo(account)); |
| |
| final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon() |
| .appendQueryParameter(Groups.ACCOUNT_NAME, account.name) |
| .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type); |
| if (account.dataSet != null) { |
| groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build(); |
| } |
| final Cursor cursor = resolver.query(groupsUri.build(), null, null, null, null); |
| if (cursor == null) { |
| continue; |
| } |
| android.content.EntityIterator iterator = |
| ContactsContract.Groups.newEntityIterator(cursor); |
| 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); |
| accountDisplay.addGroup(group); |
| hasGroups = true; |
| } |
| // Create single entry handling ungrouped status |
| accountDisplay.mUngrouped = |
| GroupDelta.fromSettings(resolver, account.name, account.type, |
| account.dataSet, hasGroups); |
| accountDisplay.addGroup(accountDisplay.mUngrouped); |
| } finally { |
| iterator.close(); |
| } |
| |
| accounts.add(accountDisplay); |
| } |
| |
| return accounts; |
| } |
| |
| @Override |
| public void deliverResult(AccountSet cursor) { |
| if (isReset()) { |
| return; |
| } |
| |
| mAccountSet = cursor; |
| |
| if (isStarted()) { |
| super.deliverResult(cursor); |
| } |
| } |
| |
| @Override |
| protected void onStartLoading() { |
| if (mAccountSet != null) { |
| deliverResult(mAccountSet); |
| } |
| if (takeContentChanged() || mAccountSet == null) { |
| forceLoad(); |
| } |
| } |
| |
| @Override |
| protected void onStopLoading() { |
| cancelLoad(); |
| } |
| |
| @Override |
| protected void onReset() { |
| super.onReset(); |
| onStopLoading(); |
| mAccountSet = null; |
| } |
| } |
| |
| @Override |
| protected void onStart() { |
| getLoaderManager().initLoader(ACCOUNT_SET_LOADER_ID, null, this); |
| super.onStart(); |
| } |
| |
| @Override |
| public Loader<AccountSet> onCreateLoader(int id, Bundle args) { |
| return new CustomFilterConfigurationLoader(this); |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<AccountSet> loader, AccountSet data) { |
| mAdapter.setAccounts(data); |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<AccountSet> loader) { |
| mAdapter.setAccounts(null); |
| } |
| |
| 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}, {@link Settings#ACCOUNT_TYPE}, and |
| * {@link Settings#DATA_SET}. |
| */ |
| public static GroupDelta fromSettings(ContentResolver resolver, String accountName, |
| String accountType, String dataSet, boolean accountHasGroups) { |
| final Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon() |
| .appendQueryParameter(Settings.ACCOUNT_NAME, accountName) |
| .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType); |
| if (dataSet != null) { |
| settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet); |
| } |
| final Cursor cursor = resolver.query(settingsUri.build(), 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); |
| values.put(Settings.DATA_SET, dataSet); |
| |
| if (cursor != null && cursor.moveToFirst()) { |
| // Read existing values when present |
| values.put(Settings.SHOULD_SYNC, cursor.getInt(0)); |
| values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1)); |
| return fromBefore(values).setUngrouped(accountHasGroups); |
| } else { |
| // Nothing found, so treat as create |
| values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC); |
| values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE); |
| return fromAfter(values).setUngrouped(accountHasGroups); |
| } |
| } finally { |
| if (cursor != null) cursor.close(); |
| } |
| } |
| |
| public static GroupDelta fromBefore(ContentValues before) { |
| final GroupDelta entry = new GroupDelta(); |
| entry.mBefore = before; |
| entry.mAfter = new ContentValues(); |
| return entry; |
| } |
| |
| public static GroupDelta fromAfter(ContentValues after) { |
| final GroupDelta entry = new GroupDelta(); |
| entry.mBefore = null; |
| entry.mAfter = after; |
| return entry; |
| } |
| |
| protected GroupDelta setUngrouped(boolean accountHasGroups) { |
| mUngrouped = true; |
| mAccountHasGroups = accountHasGroups; |
| return this; |
| } |
| |
| @Override |
| public boolean beforeExists() { |
| return mBefore != null; |
| } |
| |
| public boolean getShouldSync() { |
| return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, |
| DEFAULT_SHOULD_SYNC) != 0; |
| } |
| |
| public boolean getVisible() { |
| return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, |
| DEFAULT_VISIBLE) != 0; |
| } |
| |
| public void putShouldSync(boolean shouldSync) { |
| put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0); |
| } |
| |
| public void putVisible(boolean visible) { |
| put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0); |
| } |
| |
| private String getAccountType() { |
| return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE); |
| } |
| |
| public CharSequence getTitle(Context context) { |
| if (mUngrouped) { |
| final String customAllContactsName = |
| LocalizedNameResolver.getAllContactsName(context, getAccountType()); |
| if (customAllContactsName != null) { |
| return customAllContactsName; |
| } |
| if (mAccountHasGroups) { |
| return context.getText(R.string.display_ungrouped); |
| } else { |
| return context.getText(R.string.display_all_contacts); |
| } |
| } else { |
| final Integer titleRes = getAsInteger(Groups.TITLE_RES); |
| if (titleRes != null && titleRes != 0) { |
| final String packageName = getAsString(Groups.RES_PACKAGE); |
| if (packageName != null) { |
| return context.getPackageManager().getText(packageName, titleRes, null); |
| } |
| } |
| 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 (isInsert()) { |
| // Only allow inserts for Settings |
| if (mUngrouped) { |
| mAfter.remove(mIdColumn); |
| return ContentProviderOperation.newInsert(Settings.CONTENT_URI) |
| .withValues(mAfter) |
| .build(); |
| } |
| else { |
| throw new IllegalStateException("Unexpected diff"); |
| } |
| } else if (isUpdate()) { |
| if (mUngrouped) { |
| String accountName = this.getAsString(Settings.ACCOUNT_NAME); |
| String accountType = this.getAsString(Settings.ACCOUNT_TYPE); |
| String dataSet = this.getAsString(Settings.DATA_SET); |
| StringBuilder selection = new StringBuilder(Settings.ACCOUNT_NAME + "=? AND " |
| + Settings.ACCOUNT_TYPE + "=?"); |
| String[] selectionArgs; |
| if (dataSet == null) { |
| selection.append(" AND " + Settings.DATA_SET + " IS NULL"); |
| selectionArgs = new String[] {accountName, accountType}; |
| } else { |
| selection.append(" AND " + Settings.DATA_SET + "=?"); |
| selectionArgs = new String[] {accountName, accountType, dataSet}; |
| } |
| return ContentProviderOperation.newUpdate(Settings.CONTENT_URI) |
| .withSelection(selection.toString(), selectionArgs) |
| .withValues(mAfter) |
| .build(); |
| } else { |
| return ContentProviderOperation.newUpdate( |
| addCallerIsSyncAdapterParameter(Groups.CONTENT_URI)) |
| .withSelection(Groups._ID + "=" + this.getId(), null) |
| .withValues(mAfter) |
| .build(); |
| } |
| } else { |
| return null; |
| } |
| } |
| } |
| |
| 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 AccountWithDataSet}, usually shown as |
| * children under a single expandable group. |
| */ |
| protected static class AccountDisplay { |
| public final String mName; |
| public final String mType; |
| public final String mDataSet; |
| public final AccountDisplayInfo mAccountDisplayInfo; |
| |
| public GroupDelta mUngrouped; |
| public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList(); |
| public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList(); |
| |
| public GroupDelta getGroup(int position) { |
| if (position < mSyncedGroups.size()) { |
| return mSyncedGroups.get(position); |
| } |
| position -= mSyncedGroups.size(); |
| return mUnsyncedGroups.get(position); |
| } |
| |
| /** |
| * Build an {@link AccountDisplay} covering all {@link Groups} under the |
| * given {@link AccountWithDataSet}. |
| */ |
| public AccountDisplay(ContentResolver resolver, String accountName, String accountType, |
| String dataSet, AccountDisplayInfo displayableInfo) { |
| mName = accountName; |
| mType = accountType; |
| mDataSet = dataSet; |
| mAccountDisplayInfo = displayableInfo; |
| } |
| |
| /** |
| * 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 AccountWithDataSet}. |
| */ |
| 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 AccountWithDataSet} type. 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 AccountTypeManager mAccountTypes; |
| private AccountSet mAccounts; |
| |
| private boolean mChildWithPhones = false; |
| |
| public DisplayAdapter(Context context) { |
| mContext = context; |
| mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); |
| mAccountTypes = AccountTypeManager.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; |
| } |
| |
| @Override |
| public View getGroupView(int groupPosition, boolean isExpanded, View convertView, |
| ViewGroup parent) { |
| if (convertView == null) { |
| convertView = mInflater.inflate( |
| R.layout.custom_contact_list_filter_account, 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); |
| |
| text1.setText(account.mAccountDisplayInfo.getNameLabel()); |
| text1.setVisibility(!account.mAccountDisplayInfo.isDeviceAccount() |
| || account.mAccountDisplayInfo.hasDistinctName() |
| ? View.VISIBLE : View.GONE); |
| text2.setText(account.mAccountDisplayInfo.getTypeLabel()); |
| |
| final int textColor = mContext.getResources().getColor(isExpanded |
| ? R.color.dialtacts_theme_color |
| : R.color.account_filter_text_color); |
| text1.setTextColor(textColor); |
| text2.setTextColor(textColor); |
| |
| return convertView; |
| } |
| |
| @Override |
| public View getChildView(int groupPosition, int childPosition, boolean isLastChild, |
| View convertView, ViewGroup parent) { |
| if (convertView == null) { |
| convertView = mInflater.inflate( |
| R.layout.custom_contact_list_filter_group, 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); |
| 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); |
| } |
| |
| // Show divider at bottom only for the last child. |
| final View dividerBottom = convertView.findViewById(R.id.adapter_divider_bottom); |
| dividerBottom.setVisibility(isLastChild ? View.VISIBLE : View.GONE); |
| |
| return convertView; |
| } |
| |
| @Override |
| public Object getChild(int groupPosition, int childPosition) { |
| final AccountDisplay account = mAccounts.get(groupPosition); |
| final boolean validChild = childPosition >= 0 |
| && childPosition < account.mSyncedGroups.size() |
| + account.mUnsyncedGroups.size(); |
| if (validChild) { |
| return account.getGroup(childPosition); |
| } else { |
| return null; |
| } |
| } |
| |
| @Override |
| 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; |
| } |
| } |
| |
| @Override |
| public int getChildrenCount(int groupPosition) { |
| // Count is any synced groups, plus possible footer |
| final AccountDisplay account = mAccounts.get(groupPosition); |
| return account.mSyncedGroups.size() + account.mUnsyncedGroups.size(); |
| } |
| |
| @Override |
| public Object getGroup(int groupPosition) { |
| return mAccounts.get(groupPosition); |
| } |
| |
| @Override |
| public int getGroupCount() { |
| if (mAccounts == null) { |
| return 0; |
| } |
| return mAccounts.size(); |
| } |
| |
| @Override |
| public long getGroupId(int groupPosition) { |
| return groupPosition; |
| } |
| |
| @Override |
| public boolean hasStableIds() { |
| return true; |
| } |
| |
| @Override |
| public boolean isChildSelectable(int groupPosition, int childPosition) { |
| return true; |
| } |
| } |
| |
| /** |
| * 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 (GoogleAccountType.ACCOUNT_TYPE.equals(account.mType) && account.mDataSet == null) { |
| 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; |
| } |
| }); |
| } |
| } |
| } |
| |
| private boolean hasUnsavedChanges() { |
| if (mAdapter == null || mAdapter.mAccounts == null) { |
| return false; |
| } |
| if (getCurrentListFilterType() != ContactListFilter.FILTER_TYPE_CUSTOM) { |
| return true; |
| } |
| final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff(); |
| if (diff.isEmpty()) { |
| return false; |
| } |
| return true; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private void doSaveAction() { |
| if (mAdapter == null || mAdapter.mAccounts == null) { |
| finish(); |
| return; |
| } |
| |
| setResult(RESULT_OK); |
| |
| final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff(); |
| if (diff.isEmpty()) { |
| finish(); |
| return; |
| } |
| |
| new UpdateTask(this).execute(diff); |
| } |
| |
| /** |
| * Background task that persists changes to {@link Groups#GROUP_VISIBLE}, |
| * showing spinner dialog to user while updating. |
| */ |
| public static class UpdateTask extends |
| WeakAsyncTask<ArrayList<ContentProviderOperation>, Void, Void, Activity> { |
| private ProgressDialog mProgress; |
| |
| public UpdateTask(Activity target) { |
| super(target); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected void onPreExecute(Activity target) { |
| final Context context = target; |
| |
| mProgress = 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, ArrayList<ContentProviderOperation>... params) { |
| final Context context = target; |
| final ContentValues values = new ContentValues(); |
| final ContentResolver resolver = context.getContentResolver(); |
| |
| try { |
| final ArrayList<ContentProviderOperation> diff = params[0]; |
| 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; |
| |
| try { |
| mProgress.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 boolean onCreateOptionsMenu(Menu menu) { |
| super.onCreateOptionsMenu(menu); |
| |
| final MenuItem menuItem = menu.add(Menu.NONE, R.id.menu_save, Menu.NONE, |
| R.string.menu_custom_filter_save); |
| menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); |
| |
| return true; |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| switch (item.getItemId()) { |
| case android.R.id.home: |
| confirmFinish(); |
| return true; |
| case R.id.menu_save: |
| this.doSaveAction(); |
| return true; |
| default: |
| break; |
| } |
| return super.onOptionsItemSelected(item); |
| } |
| |
| @Override |
| public void onBackPressed() { |
| confirmFinish(); |
| } |
| |
| private void confirmFinish() { |
| // Prompt the user whether they want to discard there customizations unless |
| // nothing will be changed. |
| if (hasUnsavedChanges()) { |
| new ConfirmNavigationDialogFragment().show(getFragmentManager(), |
| "ConfirmNavigationDialog"); |
| } else { |
| setResult(RESULT_CANCELED); |
| finish(); |
| } |
| } |
| |
| private int getCurrentListFilterType() { |
| return getIntent().getIntExtra(EXTRA_CURRENT_LIST_FILTER_TYPE, |
| ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS); |
| } |
| |
| public static class ConfirmNavigationDialogFragment |
| extends DialogFragment implements DialogInterface.OnClickListener { |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| return new AlertDialog.Builder(getActivity(), getTheme()) |
| .setMessage(R.string.leave_customize_confirmation_dialog_message) |
| .setNegativeButton(android.R.string.no, null) |
| .setPositiveButton(android.R.string.yes, this) |
| .create(); |
| } |
| |
| @Override |
| public void onClick(DialogInterface dialogInterface, int i) { |
| if (i == DialogInterface.BUTTON_POSITIVE) { |
| getActivity().setResult(RESULT_CANCELED); |
| getActivity().finish(); |
| } |
| } |
| } |
| } |