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