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