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