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