blob: 3ca8e36b11a96e6877f24126ae471acd15814cc8 [file] [log] [blame]
Walter Jangf2cad222016-07-14 19:49:06 +00001/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Gary Mai69c182a2016-12-05 13:07:03 -080017package com.android.contacts.list;
Walter Jangf2cad222016-07-14 19:49:06 +000018
19import android.app.ActionBar;
20import android.app.Activity;
21import android.app.AlertDialog;
Marcus Hagerott10f856c2016-08-15 16:28:18 -070022import android.app.Dialog;
23import android.app.DialogFragment;
Walter Jangf2cad222016-07-14 19:49:06 +000024import android.app.LoaderManager.LoaderCallbacks;
25import android.app.ProgressDialog;
26import android.content.AsyncTaskLoader;
27import android.content.ContentProviderOperation;
28import android.content.ContentResolver;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.DialogInterface;
32import android.content.Intent;
33import android.content.Loader;
34import android.content.OperationApplicationException;
Walter Jangf2cad222016-07-14 19:49:06 +000035import android.database.Cursor;
Wenyi Wangcfcffdc2016-07-18 16:52:01 -070036import android.graphics.Color;
37import android.graphics.drawable.ColorDrawable;
Walter Jangf2cad222016-07-14 19:49:06 +000038import android.net.Uri;
39import android.os.Bundle;
40import android.os.RemoteException;
Walter Jangf2cad222016-07-14 19:49:06 +000041import android.provider.ContactsContract;
42import android.provider.ContactsContract.Groups;
43import android.provider.ContactsContract.Settings;
44import android.util.Log;
45import android.view.ContextMenu;
46import android.view.LayoutInflater;
Wenyi Wangcfcffdc2016-07-18 16:52:01 -070047import android.view.Menu;
Walter Jangf2cad222016-07-14 19:49:06 +000048import android.view.MenuItem;
49import android.view.MenuItem.OnMenuItemClickListener;
50import android.view.View;
51import android.view.ViewGroup;
52import android.widget.BaseExpandableListAdapter;
53import android.widget.CheckBox;
54import android.widget.ExpandableListAdapter;
55import android.widget.ExpandableListView;
56import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
57import android.widget.TextView;
58
Arthur Wang3f6a2442016-12-05 14:51:59 -080059import com.android.contacts.R;
Gary Mai69c182a2016-12-05 13:07:03 -080060import com.android.contacts.model.AccountTypeManager;
61import com.android.contacts.model.ValuesDelta;
Gary Mai0a49afa2016-12-05 15:53:58 -080062import com.android.contacts.model.account.AccountDisplayInfo;
63import com.android.contacts.model.account.AccountDisplayInfoFactory;
Gary Mai69c182a2016-12-05 13:07:03 -080064import com.android.contacts.model.account.AccountType;
65import com.android.contacts.model.account.AccountWithDataSet;
66import com.android.contacts.model.account.GoogleAccountType;
67import com.android.contacts.util.EmptyService;
68import com.android.contacts.util.LocalizedNameResolver;
69import com.android.contacts.util.WeakAsyncTask;
Gary Mai0a49afa2016-12-05 15:53:58 -080070
Walter Jangf2cad222016-07-14 19:49:06 +000071import com.google.common.collect.Lists;
72
73import java.util.ArrayList;
74import java.util.Collections;
75import java.util.Comparator;
76import java.util.Iterator;
Marcus Hagerottfac695a2016-08-24 17:02:40 -070077import java.util.List;
Walter Jangf2cad222016-07-14 19:49:06 +000078
79/**
80 * Shows a list of all available {@link Groups} available, letting the user
81 * select which ones they want to be visible.
82 */
Wenyi Wangcfcffdc2016-07-18 16:52:01 -070083public class CustomContactListFilterActivity extends Activity implements
84 ExpandableListView.OnChildClickListener,
85 LoaderCallbacks<CustomContactListFilterActivity.AccountSet> {
Walter Jangf2cad222016-07-14 19:49:06 +000086 private static final String TAG = "CustomContactListFilterActivity";
87
Marcus Hagerott10f856c2016-08-15 16:28:18 -070088 public static final String EXTRA_CURRENT_LIST_FILTER_TYPE = "currentListFilterType";
89
Walter Jangf2cad222016-07-14 19:49:06 +000090 private static final int ACCOUNT_SET_LOADER_ID = 1;
91
92 private ExpandableListView mList;
93 private DisplayAdapter mAdapter;
94
Walter Jangf2cad222016-07-14 19:49:06 +000095 @Override
96 protected void onCreate(Bundle icicle) {
97 super.onCreate(icicle);
98 setContentView(R.layout.contact_list_filter_custom);
99
100 mList = (ExpandableListView) findViewById(android.R.id.list);
101 mList.setOnChildClickListener(this);
102 mList.setHeaderDividersEnabled(true);
Wenyi Wangcfcffdc2016-07-18 16:52:01 -0700103 mList.setChildDivider(new ColorDrawable(Color.TRANSPARENT));
104
105 mList.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
106 @Override
107 public void onLayoutChange(final View v, final int left, final int top, final int right,
108 final int bottom, final int oldLeft, final int oldTop, final int oldRight,
109 final int oldBottom) {
110 mList.setIndicatorBounds(
111 mList.getWidth() - getResources().getDimensionPixelSize(
112 R.dimen.contact_filter_indicator_padding_end),
113 mList.getWidth() - getResources().getDimensionPixelSize(
114 R.dimen.contact_filter_indicator_padding_start));
115 }
116 });
117
Walter Jangf2cad222016-07-14 19:49:06 +0000118 mAdapter = new DisplayAdapter(this);
119
Walter Jangf2cad222016-07-14 19:49:06 +0000120 mList.setOnCreateContextMenuListener(this);
121
122 mList.setAdapter(mAdapter);
123
124 ActionBar actionBar = getActionBar();
125 if (actionBar != null) {
126 // android.R.id.home will be triggered in onOptionsItemSelected()
127 actionBar.setDisplayHomeAsUpEnabled(true);
128 }
129 }
130
131 public static class CustomFilterConfigurationLoader extends AsyncTaskLoader<AccountSet> {
132
133 private AccountSet mAccountSet;
134
135 public CustomFilterConfigurationLoader(Context context) {
136 super(context);
137 }
138
139 @Override
140 public AccountSet loadInBackground() {
141 Context context = getContext();
142 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context);
143 final ContentResolver resolver = context.getContentResolver();
144
145 final AccountSet accounts = new AccountSet();
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700146
Marcus Hagerott67a06392016-10-13 15:16:58 -0700147 // Don't include the null account because it doesn't support writing to
148 // ContactsContract.Settings
149 final List<AccountWithDataSet> sourceAccounts = accountTypes.getAccounts(
150 AccountTypeManager.nonNullAccountFilter());
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700151 final AccountDisplayInfoFactory displayableAccountFactory =
152 new AccountDisplayInfoFactory(context, sourceAccounts);
153 for (AccountWithDataSet account : sourceAccounts) {
Walter Jangf2cad222016-07-14 19:49:06 +0000154 final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
155 if (accountType.isExtension() && !account.hasData(context)) {
156 // Extension with no data -- skip.
157 continue;
158 }
159
160 AccountDisplay accountDisplay =
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700161 new AccountDisplay(resolver, account.name, account.type, account.dataSet,
162 displayableAccountFactory.getAccountDisplayInfo(account));
Walter Jangf2cad222016-07-14 19:49:06 +0000163
164 final Uri.Builder groupsUri = Groups.CONTENT_URI.buildUpon()
165 .appendQueryParameter(Groups.ACCOUNT_NAME, account.name)
166 .appendQueryParameter(Groups.ACCOUNT_TYPE, account.type);
167 if (account.dataSet != null) {
168 groupsUri.appendQueryParameter(Groups.DATA_SET, account.dataSet).build();
169 }
170 final Cursor cursor = resolver.query(groupsUri.build(), null, null, null, null);
171 if (cursor == null) {
172 continue;
173 }
174 android.content.EntityIterator iterator =
175 ContactsContract.Groups.newEntityIterator(cursor);
176 try {
177 boolean hasGroups = false;
178
179 // Create entries for each known group
180 while (iterator.hasNext()) {
181 final ContentValues values = iterator.next().getEntityValues();
182 final GroupDelta group = GroupDelta.fromBefore(values);
183 accountDisplay.addGroup(group);
184 hasGroups = true;
185 }
186 // Create single entry handling ungrouped status
187 accountDisplay.mUngrouped =
188 GroupDelta.fromSettings(resolver, account.name, account.type,
189 account.dataSet, hasGroups);
190 accountDisplay.addGroup(accountDisplay.mUngrouped);
191 } finally {
192 iterator.close();
193 }
194
195 accounts.add(accountDisplay);
196 }
197
198 return accounts;
199 }
200
201 @Override
202 public void deliverResult(AccountSet cursor) {
203 if (isReset()) {
204 return;
205 }
206
207 mAccountSet = cursor;
208
209 if (isStarted()) {
210 super.deliverResult(cursor);
211 }
212 }
213
214 @Override
215 protected void onStartLoading() {
216 if (mAccountSet != null) {
217 deliverResult(mAccountSet);
218 }
219 if (takeContentChanged() || mAccountSet == null) {
220 forceLoad();
221 }
222 }
223
224 @Override
225 protected void onStopLoading() {
226 cancelLoad();
227 }
228
229 @Override
230 protected void onReset() {
231 super.onReset();
232 onStopLoading();
233 mAccountSet = null;
234 }
235 }
236
237 @Override
238 protected void onStart() {
239 getLoaderManager().initLoader(ACCOUNT_SET_LOADER_ID, null, this);
240 super.onStart();
241 }
242
243 @Override
244 public Loader<AccountSet> onCreateLoader(int id, Bundle args) {
245 return new CustomFilterConfigurationLoader(this);
246 }
247
248 @Override
249 public void onLoadFinished(Loader<AccountSet> loader, AccountSet data) {
250 mAdapter.setAccounts(data);
251 }
252
253 @Override
254 public void onLoaderReset(Loader<AccountSet> loader) {
255 mAdapter.setAccounts(null);
256 }
257
258 private static final int DEFAULT_SHOULD_SYNC = 1;
259 private static final int DEFAULT_VISIBLE = 0;
260
261 /**
262 * Entry holding any changes to {@link Groups} or {@link Settings} rows,
263 * such as {@link Groups#SHOULD_SYNC} or {@link Groups#GROUP_VISIBLE}.
264 */
265 protected static class GroupDelta extends ValuesDelta {
266 private boolean mUngrouped = false;
267 private boolean mAccountHasGroups;
268
269 private GroupDelta() {
270 super();
271 }
272
273 /**
274 * Build {@link GroupDelta} from the {@link Settings} row for the given
275 * {@link Settings#ACCOUNT_NAME}, {@link Settings#ACCOUNT_TYPE}, and
276 * {@link Settings#DATA_SET}.
277 */
278 public static GroupDelta fromSettings(ContentResolver resolver, String accountName,
279 String accountType, String dataSet, boolean accountHasGroups) {
280 final Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon()
281 .appendQueryParameter(Settings.ACCOUNT_NAME, accountName)
282 .appendQueryParameter(Settings.ACCOUNT_TYPE, accountType);
283 if (dataSet != null) {
284 settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
285 }
286 final Cursor cursor = resolver.query(settingsUri.build(), new String[] {
287 Settings.SHOULD_SYNC, Settings.UNGROUPED_VISIBLE
288 }, null, null, null);
289
290 try {
291 final ContentValues values = new ContentValues();
292 values.put(Settings.ACCOUNT_NAME, accountName);
293 values.put(Settings.ACCOUNT_TYPE, accountType);
294 values.put(Settings.DATA_SET, dataSet);
295
296 if (cursor != null && cursor.moveToFirst()) {
297 // Read existing values when present
298 values.put(Settings.SHOULD_SYNC, cursor.getInt(0));
299 values.put(Settings.UNGROUPED_VISIBLE, cursor.getInt(1));
300 return fromBefore(values).setUngrouped(accountHasGroups);
301 } else {
302 // Nothing found, so treat as create
303 values.put(Settings.SHOULD_SYNC, DEFAULT_SHOULD_SYNC);
304 values.put(Settings.UNGROUPED_VISIBLE, DEFAULT_VISIBLE);
305 return fromAfter(values).setUngrouped(accountHasGroups);
306 }
307 } finally {
308 if (cursor != null) cursor.close();
309 }
310 }
311
312 public static GroupDelta fromBefore(ContentValues before) {
313 final GroupDelta entry = new GroupDelta();
314 entry.mBefore = before;
315 entry.mAfter = new ContentValues();
316 return entry;
317 }
318
319 public static GroupDelta fromAfter(ContentValues after) {
320 final GroupDelta entry = new GroupDelta();
321 entry.mBefore = null;
322 entry.mAfter = after;
323 return entry;
324 }
325
326 protected GroupDelta setUngrouped(boolean accountHasGroups) {
327 mUngrouped = true;
328 mAccountHasGroups = accountHasGroups;
329 return this;
330 }
331
332 @Override
333 public boolean beforeExists() {
334 return mBefore != null;
335 }
336
337 public boolean getShouldSync() {
338 return getAsInteger(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC,
339 DEFAULT_SHOULD_SYNC) != 0;
340 }
341
342 public boolean getVisible() {
343 return getAsInteger(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE,
344 DEFAULT_VISIBLE) != 0;
345 }
346
347 public void putShouldSync(boolean shouldSync) {
348 put(mUngrouped ? Settings.SHOULD_SYNC : Groups.SHOULD_SYNC, shouldSync ? 1 : 0);
349 }
350
351 public void putVisible(boolean visible) {
352 put(mUngrouped ? Settings.UNGROUPED_VISIBLE : Groups.GROUP_VISIBLE, visible ? 1 : 0);
353 }
354
355 private String getAccountType() {
356 return (mBefore == null ? mAfter : mBefore).getAsString(Settings.ACCOUNT_TYPE);
357 }
358
359 public CharSequence getTitle(Context context) {
360 if (mUngrouped) {
361 final String customAllContactsName =
362 LocalizedNameResolver.getAllContactsName(context, getAccountType());
363 if (customAllContactsName != null) {
364 return customAllContactsName;
365 }
366 if (mAccountHasGroups) {
367 return context.getText(R.string.display_ungrouped);
368 } else {
369 return context.getText(R.string.display_all_contacts);
370 }
371 } else {
372 final Integer titleRes = getAsInteger(Groups.TITLE_RES);
Marcus Hagerott3148fef2016-09-12 14:45:21 -0700373 if (titleRes != null && titleRes != 0) {
Walter Jangf2cad222016-07-14 19:49:06 +0000374 final String packageName = getAsString(Groups.RES_PACKAGE);
Marcus Hagerott3148fef2016-09-12 14:45:21 -0700375 if (packageName != null) {
376 return context.getPackageManager().getText(packageName, titleRes, null);
377 }
Walter Jangf2cad222016-07-14 19:49:06 +0000378 }
Marcus Hagerott3148fef2016-09-12 14:45:21 -0700379 return getAsString(Groups.TITLE);
Walter Jangf2cad222016-07-14 19:49:06 +0000380 }
381 }
382
383 /**
384 * Build a possible {@link ContentProviderOperation} to persist any
385 * changes to the {@link Groups} or {@link Settings} row described by
386 * this {@link GroupDelta}.
387 */
388 public ContentProviderOperation buildDiff() {
389 if (isInsert()) {
390 // Only allow inserts for Settings
391 if (mUngrouped) {
392 mAfter.remove(mIdColumn);
393 return ContentProviderOperation.newInsert(Settings.CONTENT_URI)
394 .withValues(mAfter)
395 .build();
396 }
397 else {
398 throw new IllegalStateException("Unexpected diff");
399 }
400 } else if (isUpdate()) {
401 if (mUngrouped) {
402 String accountName = this.getAsString(Settings.ACCOUNT_NAME);
403 String accountType = this.getAsString(Settings.ACCOUNT_TYPE);
404 String dataSet = this.getAsString(Settings.DATA_SET);
405 StringBuilder selection = new StringBuilder(Settings.ACCOUNT_NAME + "=? AND "
406 + Settings.ACCOUNT_TYPE + "=?");
407 String[] selectionArgs;
408 if (dataSet == null) {
409 selection.append(" AND " + Settings.DATA_SET + " IS NULL");
410 selectionArgs = new String[] {accountName, accountType};
411 } else {
412 selection.append(" AND " + Settings.DATA_SET + "=?");
413 selectionArgs = new String[] {accountName, accountType, dataSet};
414 }
415 return ContentProviderOperation.newUpdate(Settings.CONTENT_URI)
416 .withSelection(selection.toString(), selectionArgs)
417 .withValues(mAfter)
418 .build();
419 } else {
420 return ContentProviderOperation.newUpdate(
421 addCallerIsSyncAdapterParameter(Groups.CONTENT_URI))
422 .withSelection(Groups._ID + "=" + this.getId(), null)
423 .withValues(mAfter)
424 .build();
425 }
426 } else {
427 return null;
428 }
429 }
430 }
431
432 private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
433 return uri.buildUpon()
434 .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
435 .build();
436 }
437
438 /**
439 * {@link Comparator} to sort by {@link Groups#_ID}.
440 */
441 private static Comparator<GroupDelta> sIdComparator = new Comparator<GroupDelta>() {
442 public int compare(GroupDelta object1, GroupDelta object2) {
443 final Long id1 = object1.getId();
444 final Long id2 = object2.getId();
445 if (id1 == null && id2 == null) {
446 return 0;
447 } else if (id1 == null) {
448 return -1;
449 } else if (id2 == null) {
450 return 1;
451 } else if (id1 < id2) {
452 return -1;
453 } else if (id1 > id2) {
454 return 1;
455 } else {
456 return 0;
457 }
458 }
459 };
460
461 /**
462 * Set of all {@link AccountDisplay} entries, one for each source.
463 */
464 protected static class AccountSet extends ArrayList<AccountDisplay> {
465 public ArrayList<ContentProviderOperation> buildDiff() {
466 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
467 for (AccountDisplay account : this) {
468 account.buildDiff(diff);
469 }
470 return diff;
471 }
472 }
473
474 /**
475 * {@link GroupDelta} details for a single {@link AccountWithDataSet}, usually shown as
476 * children under a single expandable group.
477 */
478 protected static class AccountDisplay {
479 public final String mName;
480 public final String mType;
481 public final String mDataSet;
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700482 public final AccountDisplayInfo mAccountDisplayInfo;
Walter Jangf2cad222016-07-14 19:49:06 +0000483
484 public GroupDelta mUngrouped;
485 public ArrayList<GroupDelta> mSyncedGroups = Lists.newArrayList();
486 public ArrayList<GroupDelta> mUnsyncedGroups = Lists.newArrayList();
487
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700488 public GroupDelta getGroup(int position) {
489 if (position < mSyncedGroups.size()) {
490 return mSyncedGroups.get(position);
491 }
492 position -= mSyncedGroups.size();
493 return mUnsyncedGroups.get(position);
494 }
495
Walter Jangf2cad222016-07-14 19:49:06 +0000496 /**
497 * Build an {@link AccountDisplay} covering all {@link Groups} under the
498 * given {@link AccountWithDataSet}.
499 */
500 public AccountDisplay(ContentResolver resolver, String accountName, String accountType,
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700501 String dataSet, AccountDisplayInfo displayableInfo) {
Walter Jangf2cad222016-07-14 19:49:06 +0000502 mName = accountName;
503 mType = accountType;
504 mDataSet = dataSet;
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700505 mAccountDisplayInfo = displayableInfo;
Walter Jangf2cad222016-07-14 19:49:06 +0000506 }
507
508 /**
509 * Add the given {@link GroupDelta} internally, filing based on its
510 * {@link GroupDelta#getShouldSync()} status.
511 */
512 private void addGroup(GroupDelta group) {
513 if (group.getShouldSync()) {
514 mSyncedGroups.add(group);
515 } else {
516 mUnsyncedGroups.add(group);
517 }
518 }
519
520 /**
521 * Set the {@link GroupDelta#putShouldSync(boolean)} value for all
522 * children {@link GroupDelta} rows.
523 */
524 public void setShouldSync(boolean shouldSync) {
525 final Iterator<GroupDelta> oppositeChildren = shouldSync ?
526 mUnsyncedGroups.iterator() : mSyncedGroups.iterator();
527 while (oppositeChildren.hasNext()) {
528 final GroupDelta child = oppositeChildren.next();
529 setShouldSync(child, shouldSync, false);
530 oppositeChildren.remove();
531 }
532 }
533
534 public void setShouldSync(GroupDelta child, boolean shouldSync) {
535 setShouldSync(child, shouldSync, true);
536 }
537
538 /**
539 * Set {@link GroupDelta#putShouldSync(boolean)}, and file internally
540 * based on updated state.
541 */
542 public void setShouldSync(GroupDelta child, boolean shouldSync, boolean attemptRemove) {
543 child.putShouldSync(shouldSync);
544 if (shouldSync) {
545 if (attemptRemove) {
546 mUnsyncedGroups.remove(child);
547 }
548 mSyncedGroups.add(child);
549 Collections.sort(mSyncedGroups, sIdComparator);
550 } else {
551 if (attemptRemove) {
552 mSyncedGroups.remove(child);
553 }
554 mUnsyncedGroups.add(child);
555 }
556 }
557
558 /**
559 * Build set of {@link ContentProviderOperation} to persist any user
560 * changes to {@link GroupDelta} rows under this {@link AccountWithDataSet}.
561 */
562 public void buildDiff(ArrayList<ContentProviderOperation> diff) {
563 for (GroupDelta group : mSyncedGroups) {
564 final ContentProviderOperation oper = group.buildDiff();
565 if (oper != null) diff.add(oper);
566 }
567 for (GroupDelta group : mUnsyncedGroups) {
568 final ContentProviderOperation oper = group.buildDiff();
569 if (oper != null) diff.add(oper);
570 }
571 }
572 }
573
574 /**
575 * {@link ExpandableListAdapter} that shows {@link GroupDelta} settings,
576 * grouped by {@link AccountWithDataSet} type. Shows footer row when any groups are
577 * unsynced, as determined through {@link AccountDisplay#mUnsyncedGroups}.
578 */
579 protected static class DisplayAdapter extends BaseExpandableListAdapter {
580 private Context mContext;
581 private LayoutInflater mInflater;
582 private AccountTypeManager mAccountTypes;
583 private AccountSet mAccounts;
584
585 private boolean mChildWithPhones = false;
586
587 public DisplayAdapter(Context context) {
588 mContext = context;
589 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
590 mAccountTypes = AccountTypeManager.getInstance(context);
591 }
592
593 public void setAccounts(AccountSet accounts) {
594 mAccounts = accounts;
595 notifyDataSetChanged();
596 }
597
598 /**
599 * In group descriptions, show the number of contacts with phone
600 * numbers, in addition to the total contacts.
601 */
602 public void setChildDescripWithPhones(boolean withPhones) {
603 mChildWithPhones = withPhones;
604 }
605
606 @Override
607 public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
608 ViewGroup parent) {
609 if (convertView == null) {
610 convertView = mInflater.inflate(
611 R.layout.custom_contact_list_filter_account, parent, false);
612 }
613
614 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
615 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
616
617 final AccountDisplay account = (AccountDisplay)this.getGroup(groupPosition);
618
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700619 text1.setText(account.mAccountDisplayInfo.getNameLabel());
620 text1.setVisibility(!account.mAccountDisplayInfo.isDeviceAccount()
621 || account.mAccountDisplayInfo.hasDistinctName()
622 ? View.VISIBLE : View.GONE);
623 text2.setText(account.mAccountDisplayInfo.getTypeLabel());
Walter Jangf2cad222016-07-14 19:49:06 +0000624
Wenyi Wangcfcffdc2016-07-18 16:52:01 -0700625 final int textColor = mContext.getResources().getColor(isExpanded
626 ? R.color.dialtacts_theme_color
627 : R.color.account_filter_text_color);
628 text1.setTextColor(textColor);
629 text2.setTextColor(textColor);
630
Walter Jangf2cad222016-07-14 19:49:06 +0000631 return convertView;
632 }
633
634 @Override
635 public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
636 View convertView, ViewGroup parent) {
637 if (convertView == null) {
638 convertView = mInflater.inflate(
639 R.layout.custom_contact_list_filter_group, parent, false);
640 }
641
642 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
643 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
644 final CheckBox checkbox = (CheckBox)convertView.findViewById(android.R.id.checkbox);
645
646 final AccountDisplay account = mAccounts.get(groupPosition);
647 final GroupDelta child = (GroupDelta)this.getChild(groupPosition, childPosition);
648 if (child != null) {
649 // Handle normal group, with title and checkbox
650 final boolean groupVisible = child.getVisible();
651 checkbox.setVisibility(View.VISIBLE);
652 checkbox.setChecked(groupVisible);
653
654 final CharSequence groupTitle = child.getTitle(mContext);
655 text1.setText(groupTitle);
656 text2.setVisibility(View.GONE);
657 } else {
658 // When unknown child, this is "more" footer view
659 checkbox.setVisibility(View.GONE);
660 text1.setText(R.string.display_more_groups);
661 text2.setVisibility(View.GONE);
662 }
663
Wenyi Wangcfcffdc2016-07-18 16:52:01 -0700664 // Show divider at bottom only for the last child.
665 final View dividerBottom = convertView.findViewById(R.id.adapter_divider_bottom);
666 dividerBottom.setVisibility(isLastChild ? View.VISIBLE : View.GONE);
667
Walter Jangf2cad222016-07-14 19:49:06 +0000668 return convertView;
669 }
670
671 @Override
672 public Object getChild(int groupPosition, int childPosition) {
673 final AccountDisplay account = mAccounts.get(groupPosition);
674 final boolean validChild = childPosition >= 0
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700675 && childPosition < account.mSyncedGroups.size()
676 + account.mUnsyncedGroups.size();
Walter Jangf2cad222016-07-14 19:49:06 +0000677 if (validChild) {
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700678 return account.getGroup(childPosition);
Walter Jangf2cad222016-07-14 19:49:06 +0000679 } else {
680 return null;
681 }
682 }
683
684 @Override
685 public long getChildId(int groupPosition, int childPosition) {
686 final GroupDelta child = (GroupDelta)getChild(groupPosition, childPosition);
687 if (child != null) {
688 final Long childId = child.getId();
689 return childId != null ? childId : Long.MIN_VALUE;
690 } else {
691 return Long.MIN_VALUE;
692 }
693 }
694
695 @Override
696 public int getChildrenCount(int groupPosition) {
697 // Count is any synced groups, plus possible footer
698 final AccountDisplay account = mAccounts.get(groupPosition);
Marcus Hagerottfac695a2016-08-24 17:02:40 -0700699 return account.mSyncedGroups.size() + account.mUnsyncedGroups.size();
Walter Jangf2cad222016-07-14 19:49:06 +0000700 }
701
702 @Override
703 public Object getGroup(int groupPosition) {
704 return mAccounts.get(groupPosition);
705 }
706
707 @Override
708 public int getGroupCount() {
709 if (mAccounts == null) {
710 return 0;
711 }
712 return mAccounts.size();
713 }
714
715 @Override
716 public long getGroupId(int groupPosition) {
717 return groupPosition;
718 }
719
720 @Override
721 public boolean hasStableIds() {
722 return true;
723 }
724
725 @Override
726 public boolean isChildSelectable(int groupPosition, int childPosition) {
727 return true;
728 }
729 }
730
Walter Jangf2cad222016-07-14 19:49:06 +0000731 /**
732 * Handle any clicks on {@link ExpandableListAdapter} children, which
733 * usually mean toggling its visible state.
734 */
735 @Override
736 public boolean onChildClick(ExpandableListView parent, View view, int groupPosition,
737 int childPosition, long id) {
738 final CheckBox checkbox = (CheckBox)view.findViewById(android.R.id.checkbox);
739
740 final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
741 final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
742 if (child != null) {
743 checkbox.toggle();
744 child.putVisible(checkbox.isChecked());
745 } else {
746 // Open context menu for bringing back unsynced
747 this.openContextMenu(view);
748 }
749 return true;
750 }
751
752 // TODO: move these definitions to framework constants when we begin
753 // defining this mode through <sync-adapter> tags
754 private static final int SYNC_MODE_UNSUPPORTED = 0;
755 private static final int SYNC_MODE_UNGROUPED = 1;
756 private static final int SYNC_MODE_EVERYTHING = 2;
757
758 protected int getSyncMode(AccountDisplay account) {
759 // TODO: read sync mode through <sync-adapter> definition
760 if (GoogleAccountType.ACCOUNT_TYPE.equals(account.mType) && account.mDataSet == null) {
761 return SYNC_MODE_EVERYTHING;
762 } else {
763 return SYNC_MODE_UNSUPPORTED;
764 }
765 }
766
767 @Override
768 public void onCreateContextMenu(ContextMenu menu, View view,
769 ContextMenu.ContextMenuInfo menuInfo) {
770 super.onCreateContextMenu(menu, view, menuInfo);
771
772 // Bail if not working with expandable long-press, or if not child
773 if (!(menuInfo instanceof ExpandableListContextMenuInfo)) return;
774
775 final ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo;
776 final int groupPosition = ExpandableListView.getPackedPositionGroup(info.packedPosition);
777 final int childPosition = ExpandableListView.getPackedPositionChild(info.packedPosition);
778
779 // Skip long-press on expandable parents
780 if (childPosition == -1) return;
781
782 final AccountDisplay account = (AccountDisplay)mAdapter.getGroup(groupPosition);
783 final GroupDelta child = (GroupDelta)mAdapter.getChild(groupPosition, childPosition);
784
785 // Ignore when selective syncing unsupported
786 final int syncMode = getSyncMode(account);
787 if (syncMode == SYNC_MODE_UNSUPPORTED) return;
788
789 if (child != null) {
790 showRemoveSync(menu, account, child, syncMode);
791 } else {
792 showAddSync(menu, account, syncMode);
793 }
794 }
795
796 protected void showRemoveSync(ContextMenu menu, final AccountDisplay account,
797 final GroupDelta child, final int syncMode) {
798 final CharSequence title = child.getTitle(this);
799
800 menu.setHeaderTitle(title);
801 menu.add(R.string.menu_sync_remove).setOnMenuItemClickListener(
802 new OnMenuItemClickListener() {
803 public boolean onMenuItemClick(MenuItem item) {
804 handleRemoveSync(account, child, syncMode, title);
805 return true;
806 }
807 });
808 }
809
810 protected void handleRemoveSync(final AccountDisplay account, final GroupDelta child,
811 final int syncMode, CharSequence title) {
812 final boolean shouldSyncUngrouped = account.mUngrouped.getShouldSync();
813 if (syncMode == SYNC_MODE_EVERYTHING && shouldSyncUngrouped
814 && !child.equals(account.mUngrouped)) {
815 // Warn before removing this group when it would cause ungrouped to stop syncing
816 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
817 final CharSequence removeMessage = this.getString(
818 R.string.display_warn_remove_ungrouped, title);
819 builder.setTitle(R.string.menu_sync_remove);
820 builder.setMessage(removeMessage);
821 builder.setNegativeButton(android.R.string.cancel, null);
822 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
823 public void onClick(DialogInterface dialog, int which) {
824 // Mark both this group and ungrouped to stop syncing
825 account.setShouldSync(account.mUngrouped, false);
826 account.setShouldSync(child, false);
827 mAdapter.notifyDataSetChanged();
828 }
829 });
830 builder.show();
831 } else {
832 // Mark this group to not sync
833 account.setShouldSync(child, false);
834 mAdapter.notifyDataSetChanged();
835 }
836 }
837
838 protected void showAddSync(ContextMenu menu, final AccountDisplay account, final int syncMode) {
839 menu.setHeaderTitle(R.string.dialog_sync_add);
840
841 // Create item for each available, unsynced group
842 for (final GroupDelta child : account.mUnsyncedGroups) {
843 if (!child.getShouldSync()) {
844 final CharSequence title = child.getTitle(this);
845 menu.add(title).setOnMenuItemClickListener(new OnMenuItemClickListener() {
846 public boolean onMenuItemClick(MenuItem item) {
847 // Adding specific group for syncing
848 if (child.mUngrouped && syncMode == SYNC_MODE_EVERYTHING) {
849 account.setShouldSync(true);
850 } else {
851 account.setShouldSync(child, true);
852 }
853 mAdapter.notifyDataSetChanged();
854 return true;
855 }
856 });
857 }
858 }
859 }
860
Marcus Hagerott10f856c2016-08-15 16:28:18 -0700861 private boolean hasUnsavedChanges() {
862 if (mAdapter == null || mAdapter.mAccounts == null) {
863 return false;
864 }
865 if (getCurrentListFilterType() != ContactListFilter.FILTER_TYPE_CUSTOM) {
866 return true;
867 }
868 final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff();
869 if (diff.isEmpty()) {
870 return false;
871 }
872 return true;
873 }
874
Walter Jangf2cad222016-07-14 19:49:06 +0000875 @SuppressWarnings("unchecked")
876 private void doSaveAction() {
877 if (mAdapter == null || mAdapter.mAccounts == null) {
878 finish();
879 return;
880 }
881
882 setResult(RESULT_OK);
883
884 final ArrayList<ContentProviderOperation> diff = mAdapter.mAccounts.buildDiff();
885 if (diff.isEmpty()) {
886 finish();
887 return;
888 }
889
890 new UpdateTask(this).execute(diff);
891 }
892
893 /**
894 * Background task that persists changes to {@link Groups#GROUP_VISIBLE},
895 * showing spinner dialog to user while updating.
896 */
897 public static class UpdateTask extends
898 WeakAsyncTask<ArrayList<ContentProviderOperation>, Void, Void, Activity> {
899 private ProgressDialog mProgress;
900
901 public UpdateTask(Activity target) {
902 super(target);
903 }
904
905 /** {@inheritDoc} */
906 @Override
907 protected void onPreExecute(Activity target) {
908 final Context context = target;
909
910 mProgress = ProgressDialog.show(
911 context, null, context.getText(R.string.savingDisplayGroups));
912
913 // Before starting this task, start an empty service to protect our
914 // process from being reclaimed by the system.
915 context.startService(new Intent(context, EmptyService.class));
916 }
917
918 /** {@inheritDoc} */
919 @Override
920 protected Void doInBackground(
921 Activity target, ArrayList<ContentProviderOperation>... params) {
922 final Context context = target;
923 final ContentValues values = new ContentValues();
924 final ContentResolver resolver = context.getContentResolver();
925
926 try {
927 final ArrayList<ContentProviderOperation> diff = params[0];
928 resolver.applyBatch(ContactsContract.AUTHORITY, diff);
929 } catch (RemoteException e) {
930 Log.e(TAG, "Problem saving display groups", e);
931 } catch (OperationApplicationException e) {
932 Log.e(TAG, "Problem saving display groups", e);
933 }
934
935 return null;
936 }
937
938 /** {@inheritDoc} */
939 @Override
940 protected void onPostExecute(Activity target, Void result) {
941 final Context context = target;
942
943 try {
944 mProgress.dismiss();
945 } catch (Exception e) {
946 Log.e(TAG, "Error dismissing progress dialog", e);
947 }
948
949 target.finish();
950
951 // Stop the service that was protecting us
952 context.stopService(new Intent(context, EmptyService.class));
953 }
954 }
955
956 @Override
Wenyi Wangcfcffdc2016-07-18 16:52:01 -0700957 public boolean onCreateOptionsMenu(Menu menu) {
958 super.onCreateOptionsMenu(menu);
959
960 final MenuItem menuItem = menu.add(Menu.NONE, R.id.menu_save, Menu.NONE,
961 R.string.menu_custom_filter_save);
962 menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
963
964 return true;
965 }
966
967 @Override
Walter Jangf2cad222016-07-14 19:49:06 +0000968 public boolean onOptionsItemSelected(MenuItem item) {
969 switch (item.getItemId()) {
970 case android.R.id.home:
Marcus Hagerott10f856c2016-08-15 16:28:18 -0700971 confirmFinish();
Walter Jangf2cad222016-07-14 19:49:06 +0000972 return true;
Wenyi Wangcfcffdc2016-07-18 16:52:01 -0700973 case R.id.menu_save:
974 this.doSaveAction();
975 return true;
Walter Jangf2cad222016-07-14 19:49:06 +0000976 default:
977 break;
978 }
979 return super.onOptionsItemSelected(item);
980 }
Marcus Hagerott10f856c2016-08-15 16:28:18 -0700981
982 @Override
983 public void onBackPressed() {
984 confirmFinish();
985 }
986
987 private void confirmFinish() {
988 // Prompt the user whether they want to discard there customizations unless
989 // nothing will be changed.
990 if (hasUnsavedChanges()) {
991 new ConfirmNavigationDialogFragment().show(getFragmentManager(),
992 "ConfirmNavigationDialog");
993 } else {
994 setResult(RESULT_CANCELED);
995 finish();
996 }
997 }
998
999 private int getCurrentListFilterType() {
1000 return getIntent().getIntExtra(EXTRA_CURRENT_LIST_FILTER_TYPE,
1001 ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
1002 }
1003
1004 public static class ConfirmNavigationDialogFragment
1005 extends DialogFragment implements DialogInterface.OnClickListener {
1006
1007 @Override
1008 public Dialog onCreateDialog(Bundle savedInstanceState) {
1009 return new AlertDialog.Builder(getActivity(), getTheme())
1010 .setMessage(R.string.leave_customize_confirmation_dialog_message)
1011 .setNegativeButton(android.R.string.no, null)
1012 .setPositiveButton(android.R.string.yes, this)
1013 .create();
1014 }
1015
1016 @Override
1017 public void onClick(DialogInterface dialogInterface, int i) {
1018 if (i == DialogInterface.BUTTON_POSITIVE) {
1019 getActivity().setResult(RESULT_CANCELED);
1020 getActivity().finish();
1021 }
1022 }
1023 }
Walter Jangf2cad222016-07-14 19:49:06 +00001024}