1 /*
2  * Copyright (C) 2016 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 package com.android.contacts.group;
17 
18 import android.app.Activity;
19 import android.app.LoaderManager.LoaderCallbacks;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.CursorLoader;
23 import android.content.Intent;
24 import android.content.Loader;
25 import android.database.Cursor;
26 import android.database.CursorWrapper;
27 import android.graphics.PorterDuff;
28 import android.graphics.drawable.Drawable;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.Message;
33 import android.provider.ContactsContract;
34 import android.provider.ContactsContract.Contacts;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.view.Gravity;
38 import android.view.LayoutInflater;
39 import android.view.Menu;
40 import android.view.MenuInflater;
41 import android.view.MenuItem;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.Button;
45 import android.widget.FrameLayout;
46 import android.widget.ImageView;
47 import android.widget.LinearLayout;
48 import android.widget.Toast;
49 import androidx.core.content.ContextCompat;
50 import com.android.contacts.ContactSaveService;
51 import com.android.contacts.ContactsUtils;
52 import com.android.contacts.GroupMetaDataLoader;
53 import com.android.contacts.R;
54 import com.android.contacts.activities.ActionBarAdapter;
55 import com.android.contacts.activities.PeopleActivity;
56 import com.android.contacts.group.GroupMembersAdapter.GroupMembersQuery;
57 import com.android.contacts.interactions.GroupDeletionDialogFragment;
58 import com.android.contacts.list.ContactsRequest;
59 import com.android.contacts.list.ContactsSectionIndexer;
60 import com.android.contacts.list.MultiSelectContactsListFragment;
61 import com.android.contacts.list.MultiSelectEntryContactListAdapter.DeleteContactListener;
62 import com.android.contacts.list.UiIntentActions;
63 import com.android.contacts.logging.ListEvent;
64 import com.android.contacts.logging.ListEvent.ListType;
65 import com.android.contacts.logging.Logger;
66 import com.android.contacts.logging.ScreenEvent;
67 import com.android.contacts.model.account.AccountWithDataSet;
68 import com.android.contacts.util.ImplicitIntentsUtil;
69 import com.android.contactsbind.FeedbackHelper;
70 import com.google.common.primitives.Longs;
71 import java.util.ArrayList;
72 import java.util.HashMap;
73 import java.util.HashSet;
74 import java.util.List;
75 import java.util.Map;
76 import java.util.Set;
77 
78 /** Displays the members of a group. */
79 public class GroupMembersFragment extends MultiSelectContactsListFragment<GroupMembersAdapter> {
80 
81     private static final String TAG = "GroupMembers";
82 
83     private static final String KEY_IS_EDIT_MODE = "editMode";
84     private static final String KEY_GROUP_URI = "groupUri";
85     private static final String KEY_GROUP_METADATA = "groupMetadata";
86 
87     public static final String TAG_GROUP_NAME_EDIT_DIALOG = "groupNameEditDialog";
88 
89     private static final String ARG_GROUP_URI = "groupUri";
90 
91     private static final int LOADER_GROUP_METADATA = 100;
92     private static final int MSG_FAIL_TO_LOAD = 1;
93 
94     /** Filters out duplicate contacts. */
95     private class FilterCursorWrapper extends CursorWrapper {
96 
97         private int[] mIndex;
98         private int mCount = 0;
99         private int mPos = 0;
100 
FilterCursorWrapper(Cursor cursor)101         public FilterCursorWrapper(Cursor cursor) {
102             super(cursor);
103 
104             mCount = super.getCount();
105             mIndex = new int[mCount];
106 
107             final List<Integer> indicesToFilter = new ArrayList<>();
108 
109             if (Log.isLoggable(TAG, Log.VERBOSE)) {
110                 Log.v(TAG, "Group members CursorWrapper start: " + mCount);
111             }
112 
113             final Bundle bundle = cursor.getExtras();
114             final String sections[] = bundle.getStringArray(Contacts
115                     .EXTRA_ADDRESS_BOOK_INDEX_TITLES);
116             final int counts[] = bundle.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
117             final ContactsSectionIndexer indexer = (sections == null || counts == null)
118                     ? null : new ContactsSectionIndexer(sections, counts);
119 
120             mGroupMemberContactIds.clear();
121             for (int i = 0; i < mCount; i++) {
122                 super.moveToPosition(i);
123                 final String contactId = getString(GroupMembersQuery.CONTACT_ID);
124                 if (!mGroupMemberContactIds.contains(contactId)) {
125                     mIndex[mPos++] = i;
126                     mGroupMemberContactIds.add(contactId);
127                 } else {
128                     indicesToFilter.add(i);
129                 }
130             }
131 
132             if (indexer != null && GroupUtil.needTrimming(mCount, counts, indexer.getPositions())) {
133                 GroupUtil.updateBundle(bundle, indexer, indicesToFilter, sections, counts);
134             }
135 
136             mCount = mPos;
137             mPos = 0;
138             super.moveToFirst();
139 
140             if (Log.isLoggable(TAG, Log.VERBOSE)) {
141                 Log.v(TAG, "Group members CursorWrapper end: " + mCount);
142             }
143         }
144 
145         @Override
move(int offset)146         public boolean move(int offset) {
147             return moveToPosition(mPos + offset);
148         }
149 
150         @Override
moveToNext()151         public boolean moveToNext() {
152             return moveToPosition(mPos + 1);
153         }
154 
155         @Override
moveToPrevious()156         public boolean moveToPrevious() {
157             return moveToPosition(mPos - 1);
158         }
159 
160         @Override
moveToFirst()161         public boolean moveToFirst() {
162             return moveToPosition(0);
163         }
164 
165         @Override
moveToLast()166         public boolean moveToLast() {
167             return moveToPosition(mCount - 1);
168         }
169 
170         @Override
moveToPosition(int position)171         public boolean moveToPosition(int position) {
172             if (position >= mCount) {
173                 mPos = mCount;
174                 return false;
175             } else if (position < 0) {
176                 mPos = -1;
177                 return false;
178             }
179             mPos = mIndex[position];
180             return super.moveToPosition(mPos);
181         }
182 
183         @Override
getCount()184         public int getCount() {
185             return mCount;
186         }
187 
188         @Override
getPosition()189         public int getPosition() {
190             return mPos;
191         }
192     }
193 
194     private final LoaderCallbacks<Cursor> mGroupMetaDataCallbacks = new LoaderCallbacks<Cursor>() {
195 
196         @Override
197         public CursorLoader onCreateLoader(int id, Bundle args) {
198             return new GroupMetaDataLoader(mActivity, mGroupUri);
199         }
200 
201         @Override
202         public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
203             if (cursor == null || cursor.isClosed() || !cursor.moveToNext()) {
204                 Log.e(TAG, "Failed to load group metadata for " + mGroupUri);
205                 Toast.makeText(getContext(), R.string.groupLoadErrorToast, Toast.LENGTH_SHORT)
206                         .show();
207                 mHandler.sendEmptyMessage(MSG_FAIL_TO_LOAD);
208                 return;
209             }
210             mGroupMetaData = new GroupMetaData(getActivity(), cursor);
211             onGroupMetadataLoaded();
212         }
213 
214         @Override
215         public void onLoaderReset(Loader<Cursor> loader) {}
216     };
217 
218     private ActionBarAdapter mActionBarAdapter;
219 
220     private PeopleActivity mActivity;
221 
222     private Uri mGroupUri;
223 
224     private boolean mIsEditMode;
225 
226     private GroupMetaData mGroupMetaData;
227 
228     private Set<String> mGroupMemberContactIds = new HashSet();
229 
230     private Handler mHandler = new Handler() {
231         @Override
232         public void handleMessage(Message msg) {
233             if(msg.what == MSG_FAIL_TO_LOAD) {
234                 mActivity.onBackPressed();
235             }
236         }
237     };
238 
newInstance(Uri groupUri)239     public static GroupMembersFragment newInstance(Uri groupUri) {
240         final Bundle args = new Bundle();
241         args.putParcelable(ARG_GROUP_URI, groupUri);
242 
243         final GroupMembersFragment fragment = new GroupMembersFragment();
244         fragment.setArguments(args);
245         return fragment;
246     }
247 
GroupMembersFragment()248     public GroupMembersFragment() {
249         setPhotoLoaderEnabled(true);
250         setSectionHeaderDisplayEnabled(true);
251         setHasOptionsMenu(true);
252         setListType(ListType.GROUP);
253     }
254 
255     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)256     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
257         if (mGroupMetaData == null) {
258             // Hide menu options until metadata is fully loaded
259             return;
260         }
261         inflater.inflate(R.menu.view_group, menu);
262     }
263 
264     @Override
onPrepareOptionsMenu(Menu menu)265     public void onPrepareOptionsMenu(Menu menu) {
266         final boolean isSelectionMode = mActionBarAdapter.isSelectionMode();
267         final boolean isGroupEditable = mGroupMetaData != null && mGroupMetaData.editable;
268         final boolean isGroupReadOnly = mGroupMetaData != null && mGroupMetaData.readOnly;
269 
270         setVisible(getContext(), menu, R.id.menu_multi_send_email, !mIsEditMode && !isGroupEmpty());
271         setVisible(getContext(), menu, R.id.menu_multi_send_message,
272                 !mIsEditMode && !isGroupEmpty());
273         setVisible(getContext(), menu, R.id.menu_add, isGroupEditable && !isSelectionMode);
274         setVisible(getContext(), menu, R.id.menu_rename_group,
275                 !isGroupReadOnly && !isSelectionMode);
276         setVisible(getContext(), menu, R.id.menu_delete_group,
277                 !isGroupReadOnly && !isSelectionMode);
278         setVisible(getContext(), menu, R.id.menu_edit_group,
279                 isGroupEditable && !mIsEditMode && !isSelectionMode && !isGroupEmpty());
280         setVisible(getContext(), menu, R.id.menu_remove_from_group,
281                 isGroupEditable && isSelectionMode && !mIsEditMode);
282     }
283 
isGroupEmpty()284     private boolean isGroupEmpty() {
285         return getAdapter() != null && getAdapter().isEmpty();
286     }
287 
setVisible(Context context, Menu menu, int id, boolean visible)288     private static void setVisible(Context context, Menu menu, int id, boolean visible) {
289         final MenuItem menuItem = menu.findItem(id);
290         if (menuItem != null) {
291             menuItem.setVisible(visible);
292             final Drawable icon = menuItem.getIcon();
293             if (icon != null) {
294                 icon.mutate().setColorFilter(ContextCompat.getColor(context,
295                         R.color.actionbar_icon_color), PorterDuff.Mode.SRC_ATOP);
296             }
297         }
298     }
299 
300     /**
301      * Helper class for cp2 query used to look up all contact's emails and phone numbers.
302      */
303     public static abstract class Query {
304         public static final String EMAIL_SELECTION =
305                 ContactsContract.Data.MIMETYPE + "='"
306                         + ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'";
307 
308         public static final String PHONE_SELECTION =
309                 ContactsContract.Data.MIMETYPE + "='"
310                         + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'";
311 
312         public static final String[] EMAIL_PROJECTION = {
313                 ContactsContract.Data.CONTACT_ID,
314                 ContactsContract.CommonDataKinds.Email._ID,
315                 ContactsContract.Data.IS_SUPER_PRIMARY,
316                 ContactsContract.Data.DATA1
317         };
318 
319         public static final String[] PHONE_PROJECTION = {
320                 ContactsContract.Data.CONTACT_ID,
321                 ContactsContract.CommonDataKinds.Phone._ID,
322                 ContactsContract.Data.IS_SUPER_PRIMARY,
323                 ContactsContract.Data.DATA1
324         };
325 
326         public static final int CONTACT_ID = 0;
327         public static final int ITEM_ID = 1;
328         public static final int PRIMARY = 2;
329         public static final int DATA1 = 3;
330     }
331 
332     /**
333      * Helper class for managing data related to contacts and emails/phone numbers.
334      */
335     private class ContactDataHelperClass {
336 
337         private List<String> items = new ArrayList<>();
338         private String firstItemId = null;
339         private String primaryItemId = null;
340 
addItem(String item, boolean primaryFlag)341         public void addItem(String item, boolean primaryFlag) {
342             if (firstItemId == null) {
343                 firstItemId = item;
344             }
345             if (primaryFlag) {
346                 primaryItemId = item;
347             }
348             items.add(item);
349         }
350 
hasDefaultItem()351         public boolean hasDefaultItem() {
352             return primaryItemId != null || items.size() == 1;
353         }
354 
getDefaultSelectionItemId()355         public String getDefaultSelectionItemId() {
356             return primaryItemId != null
357                     ? primaryItemId
358                     : firstItemId;
359         }
360     }
361 
sendToGroup(long[] ids, String sendScheme, String title)362     private void sendToGroup(long[] ids, String sendScheme, String title) {
363         if (ids == null || ids.length == 0) return;
364 
365         // Get emails or phone numbers
366         // contactMap <contact_id, contact_data>
367         final Map<String, ContactDataHelperClass> contactMap = new HashMap<>();
368         // itemList <item_data>
369         final List<String> itemList = new ArrayList<>();
370         final String sIds = GroupUtil.convertArrayToString(ids);
371         final String select = (ContactsUtils.SCHEME_MAILTO.equals(sendScheme)
372                 ? Query.EMAIL_SELECTION
373                 : Query.PHONE_SELECTION)
374                 + " AND " + ContactsContract.Data.CONTACT_ID + " IN (" + sIds + ")";
375         final ContentResolver contentResolver = getContext().getContentResolver();
376         final Cursor cursor = contentResolver.query(ContactsContract.Data.CONTENT_URI,
377                 ContactsUtils.SCHEME_MAILTO.equals(sendScheme)
378                         ? Query.EMAIL_PROJECTION
379                         : Query.PHONE_PROJECTION,
380                 select, null, null);
381 
382         if (cursor == null) {
383             return;
384         }
385 
386         try {
387             cursor.moveToPosition(-1);
388             while (cursor.moveToNext()) {
389                 final String contactId = cursor.getString(Query.CONTACT_ID);
390                 final String itemId = cursor.getString(Query.ITEM_ID);
391                 final boolean isPrimary = cursor.getInt(Query.PRIMARY) != 0;
392                 final String data = cursor.getString(Query.DATA1);
393 
394                 if (!TextUtils.isEmpty(data)) {
395                     final ContactDataHelperClass contact;
396                     if (!contactMap.containsKey(contactId)) {
397                         contact = new ContactDataHelperClass();
398                         contactMap.put(contactId, contact);
399                     } else {
400                         contact = contactMap.get(contactId);
401                     }
402                     contact.addItem(itemId, isPrimary);
403                     itemList.add(data);
404                 }
405             }
406         } finally {
407             cursor.close();
408         }
409 
410         // Start picker if a contact does not have a default
411         for (ContactDataHelperClass i : contactMap.values()) {
412             if (!i.hasDefaultItem()) {
413                 // Build list of default selected item ids
414                 final List<Long> defaultSelection = new ArrayList<>();
415                 for (ContactDataHelperClass j : contactMap.values()) {
416                     final String selectionItemId = j.getDefaultSelectionItemId();
417                     if (selectionItemId != null) {
418                         defaultSelection.add(Long.parseLong(selectionItemId));
419                     }
420                 }
421                 final long[] defaultSelectionArray = Longs.toArray(defaultSelection);
422                 startSendToSelectionPickerActivity(ids, defaultSelectionArray, sendScheme, title);
423                 return;
424             }
425         }
426 
427         if (itemList.size() == 0 || contactMap.size() < ids.length) {
428             Toast.makeText(getContext(), ContactsUtils.SCHEME_MAILTO.equals(sendScheme)
429                             ? getString(R.string.groupSomeContactsNoEmailsToast)
430                             : getString(R.string.groupSomeContactsNoPhonesToast),
431                     Toast.LENGTH_LONG).show();
432         }
433 
434         if (itemList.size() == 0) {
435             return;
436         }
437 
438         final String itemsString = TextUtils.join(",", itemList);
439         GroupUtil.startSendToSelectionActivity(this, itemsString, sendScheme, title);
440     }
441 
startSendToSelectionPickerActivity(long[] ids, long[] defaultSelection, String sendScheme, String title)442     private void startSendToSelectionPickerActivity(long[] ids, long[] defaultSelection,
443             String sendScheme, String title) {
444         startActivity(GroupUtil.createSendToSelectionPickerIntent(getContext(), ids,
445                 defaultSelection, sendScheme, title));
446     }
447 
startGroupAddMemberActivity()448     private void startGroupAddMemberActivity() {
449         startActivityForResult(GroupUtil.createPickMemberIntent(getContext(), mGroupMetaData,
450                 getMemberContactIds()), GroupUtil.RESULT_GROUP_ADD_MEMBER);
451     }
452 
453     @Override
onOptionsItemSelected(MenuItem item)454     public boolean onOptionsItemSelected(MenuItem item) {
455         final int id = item.getItemId();
456         if (id == android.R.id.home) {
457             mActivity.onBackPressed();
458         } else if (id == R.id.menu_add) {
459             startGroupAddMemberActivity();
460         } else if (id == R.id.menu_multi_send_email) {
461             final long[] ids = mActionBarAdapter.isSelectionMode()
462                     ? getAdapter().getSelectedContactIdsArray()
463                     : GroupUtil.convertStringSetToLongArray(mGroupMemberContactIds);
464             sendToGroup(ids, ContactsUtils.SCHEME_MAILTO,
465                     getString(R.string.menu_sendEmailOption));
466         } else if (id == R.id.menu_multi_send_message) {
467             final long[] ids = mActionBarAdapter.isSelectionMode()
468                     ? getAdapter().getSelectedContactIdsArray()
469                     : GroupUtil.convertStringSetToLongArray(mGroupMemberContactIds);
470             sendToGroup(ids, ContactsUtils.SCHEME_SMSTO,
471                     getString(R.string.menu_sendMessageOption));
472         } else if (id == R.id.menu_rename_group) {
473             GroupNameEditDialogFragment.newInstanceForUpdate(
474                     new AccountWithDataSet(mGroupMetaData.accountName,
475                             mGroupMetaData.accountType, mGroupMetaData.dataSet),
476                     GroupUtil.ACTION_UPDATE_GROUP, mGroupMetaData.groupId,
477                     mGroupMetaData.groupName).show(getFragmentManager(),
478                     TAG_GROUP_NAME_EDIT_DIALOG);
479         } else if (id == R.id.menu_delete_group) {
480             deleteGroup();
481         } else if (id == R.id.menu_edit_group) {
482             mIsEditMode = true;
483             mActionBarAdapter.setSelectionMode(true);
484             displayDeleteButtons(true);
485         } else if (id == R.id.menu_remove_from_group) {
486             logListEvent();
487             removeSelectedContacts();
488         } else {
489             return super.onOptionsItemSelected(item);
490         }
491         return true;
492     }
493 
removeSelectedContacts()494     private void removeSelectedContacts() {
495         final long[] contactIds = getAdapter().getSelectedContactIdsArray();
496         new UpdateGroupMembersAsyncTask(UpdateGroupMembersAsyncTask.TYPE_REMOVE,
497                 getContext(), contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName,
498                 mGroupMetaData.accountType, mGroupMetaData.dataSet).execute();
499 
500         mActionBarAdapter.setSelectionMode(false);
501     }
502 
503     @Override
onActivityResult(int requestCode, int resultCode, Intent data)504     public void onActivityResult(int requestCode, int resultCode, Intent data) {
505         if (resultCode != Activity.RESULT_OK || data == null
506                 || requestCode != GroupUtil.RESULT_GROUP_ADD_MEMBER) {
507             return;
508         }
509 
510         long[] contactIds = data.getLongArrayExtra(
511                 UiIntentActions.TARGET_CONTACT_IDS_EXTRA_KEY);
512         if (contactIds == null) {
513             final long contactId = data.getLongExtra(
514                     UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, -1);
515             if (contactId > -1) {
516                 contactIds = new long[1];
517                 contactIds[0] = contactId;
518             }
519         }
520         new UpdateGroupMembersAsyncTask(
521                 UpdateGroupMembersAsyncTask.TYPE_ADD,
522                 getContext(), contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName,
523                 mGroupMetaData.accountType, mGroupMetaData.dataSet).execute();
524     }
525 
526     private final ActionBarAdapter.Listener mActionBarListener = new ActionBarAdapter.Listener() {
527         @Override
528         public void onAction(int action) {
529             switch (action) {
530                 case ActionBarAdapter.Listener.Action.START_SELECTION_MODE:
531                     if (mIsEditMode) {
532                         displayDeleteButtons(true);
533                         mActionBarAdapter.setActionBarTitle(getString(R.string.title_edit_group));
534                     } else {
535                         displayCheckBoxes(true);
536                     }
537                     mActivity.invalidateOptionsMenu();
538                     break;
539                 case ActionBarAdapter.Listener.Action.STOP_SEARCH_AND_SELECTION_MODE:
540                     mActionBarAdapter.setSearchMode(false);
541                     if (mIsEditMode) {
542                         displayDeleteButtons(false);
543                     } else {
544                         displayCheckBoxes(false);
545                     }
546                     mActivity.invalidateOptionsMenu();
547                     break;
548                 case ActionBarAdapter.Listener.Action.BEGIN_STOPPING_SEARCH_AND_SELECTION_MODE:
549                     break;
550             }
551         }
552 
553         @Override
554         public void onUpButtonPressed() {
555             mActivity.onBackPressed();
556         }
557     };
558 
559     private final OnCheckBoxListActionListener mCheckBoxListener =
560             new OnCheckBoxListActionListener() {
561                 @Override
562                 public void onStartDisplayingCheckBoxes() {
563                     mActionBarAdapter.setSelectionMode(true);
564                 }
565 
566                 @Override
567                 public void onSelectedContactIdsChanged() {
568                     if (mActionBarAdapter == null) {
569                         return;
570                     }
571                     if (mIsEditMode) {
572                         mActionBarAdapter.setActionBarTitle(getString(R.string.title_edit_group));
573                     } else {
574                         mActionBarAdapter.setSelectionCount(getSelectedContactIds().size());
575                     }
576                 }
577 
578                 @Override
579                 public void onStopDisplayingCheckBoxes() {
580                     mActionBarAdapter.setSelectionMode(false);
581                 }
582             };
583 
logListEvent()584     private void logListEvent() {
585         Logger.logListEvent(
586                 ListEvent.ActionType.REMOVE_LABEL,
587                 getListType(),
588                 getAdapter().getCount(),
589                 /* clickedIndex */ -1,
590                 getAdapter().getSelectedContactIdsArray().length);
591     }
592 
deleteGroup()593     private void deleteGroup() {
594         if (getMemberCount() == 0) {
595             final Intent intent = ContactSaveService.createGroupDeletionIntent(
596                     getContext(), mGroupMetaData.groupId);
597             getContext().startService(intent);
598             mActivity.switchToAllContacts();
599         } else {
600             GroupDeletionDialogFragment.show(getFragmentManager(), mGroupMetaData.groupId,
601                     mGroupMetaData.groupName);
602         }
603     }
604 
605     @Override
onActivityCreated(Bundle savedInstanceState)606     public void onActivityCreated(Bundle savedInstanceState) {
607         super.onActivityCreated(savedInstanceState);
608         mActivity = (PeopleActivity) getActivity();
609         mActionBarAdapter = new ActionBarAdapter(mActivity, mActionBarListener,
610                 mActivity.getSupportActionBar(), mActivity.getToolbar(),
611                         R.string.enter_contact_name);
612         mActionBarAdapter.setShowHomeIcon(true);
613         final ContactsRequest contactsRequest = new ContactsRequest();
614         contactsRequest.setActionCode(ContactsRequest.ACTION_GROUP);
615         mActionBarAdapter.initialize(savedInstanceState, contactsRequest);
616         if (mGroupMetaData != null) {
617             mActivity.setTitle(mGroupMetaData.groupName);
618             if (mGroupMetaData.editable) {
619                 setCheckBoxListListener(mCheckBoxListener);
620             }
621         }
622     }
623 
624     @Override
getActionBarAdapter()625     public ActionBarAdapter getActionBarAdapter() {
626         return mActionBarAdapter;
627     }
628 
displayDeleteButtons(boolean displayDeleteButtons)629     public void displayDeleteButtons(boolean displayDeleteButtons) {
630         getAdapter().setDisplayDeleteButtons(displayDeleteButtons);
631     }
632 
getMemberContactIds()633     public ArrayList<String> getMemberContactIds() {
634         return new ArrayList<>(mGroupMemberContactIds);
635     }
636 
getMemberCount()637     public int getMemberCount() {
638         return mGroupMemberContactIds.size();
639     }
640 
isEditMode()641     public boolean isEditMode() {
642         return mIsEditMode;
643     }
644 
645     @Override
onCreate(Bundle savedState)646     public void onCreate(Bundle savedState) {
647         super.onCreate(savedState);
648         if (savedState == null) {
649             mGroupUri = getArguments().getParcelable(ARG_GROUP_URI);
650         } else {
651             mIsEditMode = savedState.getBoolean(KEY_IS_EDIT_MODE);
652             mGroupUri = savedState.getParcelable(KEY_GROUP_URI);
653             mGroupMetaData = savedState.getParcelable(KEY_GROUP_METADATA);
654         }
655         maybeAttachCheckBoxListener();
656     }
657 
658     @Override
onResume()659     public void onResume() {
660         super.onResume();
661         // Re-register the listener, which may have been cleared when onSaveInstanceState was
662         // called. See also: onSaveInstanceState
663         mActionBarAdapter.setListener(mActionBarListener);
664     }
665 
666     @Override
startLoading()667     protected void startLoading() {
668         if (mGroupMetaData == null || !mGroupMetaData.isValid()) {
669             getLoaderManager().restartLoader(LOADER_GROUP_METADATA, null, mGroupMetaDataCallbacks);
670         } else {
671             onGroupMetadataLoaded();
672         }
673     }
674 
675     @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)676     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
677         if (data != null) {
678             // Wait until contacts are loaded before showing the scrollbar
679             setVisibleScrollbarEnabled(true);
680 
681             final FilterCursorWrapper cursorWrapper = new FilterCursorWrapper(data);
682             bindMembersCount(cursorWrapper.getCount());
683             super.onLoadFinished(loader, cursorWrapper);
684             // Update state of menu items (e.g. "Remove contacts") based on number of group members.
685             mActivity.invalidateOptionsMenu();
686             mActionBarAdapter.updateOverflowButtonColor();
687         }
688     }
689 
bindMembersCount(int memberCount)690     private void bindMembersCount(int memberCount) {
691         final View accountFilterContainer = getView().findViewById(
692                 R.id.account_filter_header_container);
693         final View emptyGroupView = getView().findViewById(R.id.empty_group);
694         if (memberCount > 0) {
695             final AccountWithDataSet accountWithDataSet = new AccountWithDataSet(
696                     mGroupMetaData.accountName, mGroupMetaData.accountType, mGroupMetaData.dataSet);
697             bindListHeader(getContext(), getListView(), accountFilterContainer,
698                     accountWithDataSet, memberCount);
699             emptyGroupView.setVisibility(View.GONE);
700         } else {
701             hideHeaderAndAddPadding(getContext(), getListView(), accountFilterContainer);
702             emptyGroupView.setVisibility(View.VISIBLE);
703         }
704     }
705 
706     @Override
onSaveInstanceState(Bundle outState)707     public void onSaveInstanceState(Bundle outState) {
708         super.onSaveInstanceState(outState);
709         if (mActionBarAdapter != null) {
710             mActionBarAdapter.setListener(null);
711             mActionBarAdapter.onSaveInstanceState(outState);
712         }
713         outState.putBoolean(KEY_IS_EDIT_MODE, mIsEditMode);
714         outState.putParcelable(KEY_GROUP_URI, mGroupUri);
715         outState.putParcelable(KEY_GROUP_METADATA, mGroupMetaData);
716     }
717 
onGroupMetadataLoaded()718     private void onGroupMetadataLoaded() {
719         if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "Loaded " + mGroupMetaData);
720 
721         maybeAttachCheckBoxListener();
722 
723         mActivity.setTitle(mGroupMetaData.groupName);
724         mActivity.invalidateOptionsMenu();
725         mActivity.updateDrawerGroupMenu(mGroupMetaData.groupId);
726 
727         // Start loading the group members
728         super.startLoading();
729     }
730 
maybeAttachCheckBoxListener()731     private void maybeAttachCheckBoxListener() {
732         // Don't attach the multi select check box listener if we can't edit the group
733         if (mGroupMetaData != null && mGroupMetaData.editable) {
734             setCheckBoxListListener(mCheckBoxListener);
735         }
736     }
737 
738     @Override
createListAdapter()739     protected GroupMembersAdapter createListAdapter() {
740         final GroupMembersAdapter adapter = new GroupMembersAdapter(getContext());
741         adapter.setSectionHeaderDisplayEnabled(true);
742         adapter.setDisplayPhotos(true);
743         adapter.setDeleteContactListener(new DeletionListener());
744         return adapter;
745     }
746 
747     @Override
configureAdapter()748     protected void configureAdapter() {
749         super.configureAdapter();
750         if (mGroupMetaData != null) {
751             getAdapter().setGroupId(mGroupMetaData.groupId);
752         }
753     }
754 
755     @Override
inflateView(LayoutInflater inflater, ViewGroup container)756     protected View inflateView(LayoutInflater inflater, ViewGroup container) {
757         final View view = inflater.inflate(R.layout.contact_list_content, /* root */ null);
758         final View emptyGroupView = inflater.inflate(R.layout.empty_group_view, null);
759 
760         final ImageView image = (ImageView) emptyGroupView.findViewById(R.id.empty_group_image);
761         final LinearLayout.LayoutParams params =
762                 (LinearLayout.LayoutParams) image.getLayoutParams();
763         final int screenHeight = getResources().getDisplayMetrics().heightPixels;
764         params.setMargins(0, screenHeight /
765                 getResources().getInteger(R.integer.empty_group_view_image_margin_divisor), 0, 0);
766         params.gravity = Gravity.CENTER_HORIZONTAL;
767         image.setLayoutParams(params);
768 
769         final FrameLayout contactListLayout = (FrameLayout) view.findViewById(R.id.contact_list);
770         contactListLayout.addView(emptyGroupView);
771 
772         final Button addContactsButton =
773                 (Button) emptyGroupView.findViewById(R.id.add_member_button);
774         addContactsButton.setOnClickListener(new View.OnClickListener() {
775             @Override
776             public void onClick(View v) {
777                 startActivityForResult(GroupUtil.createPickMemberIntent(getContext(),
778                         mGroupMetaData, getMemberContactIds()), GroupUtil.RESULT_GROUP_ADD_MEMBER);
779             }
780         });
781         return view;
782     }
783 
784     @Override
onItemClick(int position, long id)785     protected void onItemClick(int position, long id) {
786         final Uri uri = getAdapter().getContactUri(position);
787         if (uri == null) {
788             return;
789         }
790         if (getAdapter().isDisplayingCheckBoxes()) {
791             super.onItemClick(position, id);
792             return;
793         }
794         final int count = getAdapter().getCount();
795         Logger.logListEvent(ListEvent.ActionType.CLICK, ListEvent.ListType.GROUP, count,
796                 /* clickedIndex */ position, /* numSelected */ 0);
797         ImplicitIntentsUtil.startQuickContact(
798                 getActivity(), uri, ScreenEvent.ScreenType.LIST_GROUP);
799     }
800 
801     @Override
onItemLongClick(int position, long id)802     protected boolean onItemLongClick(int position, long id) {
803         if (mActivity != null && mIsEditMode) {
804             return true;
805         }
806         return super.onItemLongClick(position, id);
807     }
808 
809     private final class DeletionListener implements DeleteContactListener {
810         @Override
onContactDeleteClicked(int position)811         public void onContactDeleteClicked(int position) {
812             final long contactId = getAdapter().getContactId(position);
813             final long[] contactIds = new long[1];
814             contactIds[0] = contactId;
815             new UpdateGroupMembersAsyncTask(UpdateGroupMembersAsyncTask.TYPE_REMOVE,
816                     getContext(), contactIds, mGroupMetaData.groupId, mGroupMetaData.accountName,
817                     mGroupMetaData.accountType, mGroupMetaData.dataSet).execute();
818         }
819     }
820 
getGroupMetaData()821     public GroupMetaData getGroupMetaData() {
822         return mGroupMetaData;
823     }
824 
isCurrentGroup(long groupId)825     public boolean isCurrentGroup(long groupId) {
826         return mGroupMetaData != null && mGroupMetaData.groupId == groupId;
827     }
828 
829     /**
830      * Return true if the fragment is not yet added, being removed, or detached.
831      */
isInactive()832     public boolean isInactive() {
833         return !isAdded() || isRemoving() || isDetached();
834     }
835 
836     @Override
onDestroy()837     public void onDestroy() {
838         if (mActionBarAdapter != null) {
839             mActionBarAdapter.setListener(null);
840         }
841         super.onDestroy();
842     }
843 
updateExistingGroupFragment(Uri newGroupUri, String action)844     public void updateExistingGroupFragment(Uri newGroupUri, String action) {
845         toastForSaveAction(action);
846 
847         if (isEditMode() && getGroupCount() == 1) {
848             // If we're deleting the last group member, exit edit mode
849             exitEditMode();
850         } else if (!GroupUtil.ACTION_REMOVE_FROM_GROUP.equals(action)) {
851             mGroupUri = newGroupUri;
852             mGroupMetaData = null; // Clear mGroupMetaData to trigger a new load.
853             reloadData();
854             mActivity.invalidateOptionsMenu();
855         }
856     }
857 
toastForSaveAction(String action)858     public void toastForSaveAction(String action) {
859         int id = -1;
860         switch(action) {
861             case GroupUtil.ACTION_UPDATE_GROUP:
862                 id = R.string.groupUpdatedToast;
863                 break;
864             case GroupUtil.ACTION_REMOVE_FROM_GROUP:
865                 id = R.string.groupMembersRemovedToast;
866                 break;
867             case GroupUtil.ACTION_CREATE_GROUP:
868                 id = R.string.groupCreatedToast;
869                 break;
870             case GroupUtil.ACTION_ADD_TO_GROUP:
871                 id = R.string.groupMembersAddedToast;
872                 break;
873             case GroupUtil.ACTION_SWITCH_GROUP:
874                 // No toast associated with this action.
875                 break;
876             default:
877                 FeedbackHelper.sendFeedback(getContext(), TAG,
878                         "toastForSaveAction passed unknown action: " + action,
879                         new IllegalArgumentException("Unhandled contact save action " + action));
880         }
881         toast(id);
882     }
883 
toast(int resId)884     private void toast(int resId) {
885         if (resId >= 0) {
886             Toast.makeText(getContext(), resId, Toast.LENGTH_SHORT).show();
887         }
888     }
889 
getGroupCount()890     private int getGroupCount() {
891         return getAdapter() != null ? getAdapter().getCount() : -1;
892     }
893 
exitEditMode()894     public void exitEditMode() {
895         mIsEditMode = false;
896         mActionBarAdapter.setSelectionMode(false);
897         displayDeleteButtons(false);
898     }
899 }
900