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 */
16package com.android.contacts.group;
17
18import android.app.Activity;
19import android.app.LoaderManager.LoaderCallbacks;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.CursorLoader;
23import android.content.Intent;
24import android.content.Loader;
25import android.database.Cursor;
26import android.database.CursorWrapper;
27import android.graphics.PorterDuff;
28import android.graphics.drawable.Drawable;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.Message;
33import android.provider.ContactsContract;
34import android.provider.ContactsContract.Contacts;
35import android.support.v4.content.ContextCompat;
36import android.text.TextUtils;
37import android.util.Log;
38import android.view.Gravity;
39import android.view.LayoutInflater;
40import android.view.Menu;
41import android.view.MenuInflater;
42import android.view.MenuItem;
43import android.view.View;
44import android.view.ViewGroup;
45import android.widget.Button;
46import android.widget.FrameLayout;
47import android.widget.ImageView;
48import android.widget.LinearLayout;
49import android.widget.Toast;
50
51import com.android.contacts.ContactSaveService;
52import com.android.contacts.ContactsUtils;
53import com.android.contacts.GroupMetaDataLoader;
54import com.android.contacts.R;
55import com.android.contacts.activities.ActionBarAdapter;
56import com.android.contacts.activities.PeopleActivity;
57import com.android.contacts.group.GroupMembersAdapter.GroupMembersQuery;
58import com.android.contacts.interactions.GroupDeletionDialogFragment;
59import com.android.contacts.list.ContactsRequest;
60import com.android.contacts.list.ContactsSectionIndexer;
61import com.android.contacts.list.MultiSelectContactsListFragment;
62import com.android.contacts.list.MultiSelectEntryContactListAdapter.DeleteContactListener;
63import com.android.contacts.list.UiIntentActions;
64import com.android.contacts.logging.ListEvent;
65import com.android.contacts.logging.ListEvent.ListType;
66import com.android.contacts.logging.Logger;
67import com.android.contacts.logging.ScreenEvent;
68import com.android.contacts.model.account.AccountWithDataSet;
69import com.android.contacts.util.ImplicitIntentsUtil;
70import com.android.contactsbind.FeedbackHelper;
71import com.google.common.primitives.Longs;
72
73import java.util.ArrayList;
74import java.util.HashMap;
75import java.util.HashSet;
76import java.util.List;
77import java.util.Map;
78import java.util.Set;
79
80/** Displays the members of a group. */
81public 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
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
149        public boolean move(int offset) {
150            return moveToPosition(mPos + offset);
151        }
152
153        @Override
154        public boolean moveToNext() {
155            return moveToPosition(mPos + 1);
156        }
157
158        @Override
159        public boolean moveToPrevious() {
160            return moveToPosition(mPos - 1);
161        }
162
163        @Override
164        public boolean moveToFirst() {
165            return moveToPosition(0);
166        }
167
168        @Override
169        public boolean moveToLast() {
170            return moveToPosition(mCount - 1);
171        }
172
173        @Override
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
187        public int getCount() {
188            return mCount;
189        }
190
191        @Override
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
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
251    public GroupMembersFragment() {
252        setPhotoLoaderEnabled(true);
253        setSectionHeaderDisplayEnabled(true);
254        setHasOptionsMenu(true);
255        setListType(ListType.GROUP);
256    }
257
258    @Override
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
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
287    private boolean isGroupEmpty() {
288        return getAdapter() != null && getAdapter().isEmpty();
289    }
290
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
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
359        public boolean hasDefaultItem() {
360            return primaryItemId != null || items.size() == 1;
361        }
362
363        public String getDefaultSelectionItemId() {
364            return primaryItemId != null
365                    ? primaryItemId
366                    : mostUsedItemId;
367        }
368    }
369
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
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
457    private void startGroupAddMemberActivity() {
458        startActivityForResult(GroupUtil.createPickMemberIntent(getContext(), mGroupMetaData,
459                getMemberContactIds()), RESULT_GROUP_ADD_MEMBER);
460    }
461
462    @Override
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
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
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
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
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
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
634    public ActionBarAdapter getActionBarAdapter() {
635        return mActionBarAdapter;
636    }
637
638    public void displayDeleteButtons(boolean displayDeleteButtons) {
639        getAdapter().setDisplayDeleteButtons(displayDeleteButtons);
640    }
641
642    public ArrayList<String> getMemberContactIds() {
643        return new ArrayList<>(mGroupMemberContactIds);
644    }
645
646    public int getMemberCount() {
647        return mGroupMemberContactIds.size();
648    }
649
650    public boolean isEditMode() {
651        return mIsEditMode;
652    }
653
654    @Override
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
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
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
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
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
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
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
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
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
757    protected void configureAdapter() {
758        super.configureAdapter();
759        if (mGroupMetaData != null) {
760            getAdapter().setGroupId(mGroupMetaData.groupId);
761        }
762    }
763
764    @Override
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
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
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
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
830    public GroupMetaData getGroupMetaData() {
831        return mGroupMetaData;
832    }
833
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     */
841    public boolean isInactive() {
842        return !isAdded() || isRemoving() || isDetached();
843    }
844
845    @Override
846    public void onDestroy() {
847        if (mActionBarAdapter != null) {
848            mActionBarAdapter.setListener(null);
849        }
850        super.onDestroy();
851    }
852
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
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
893    private void toast(int resId) {
894        if (resId >= 0) {
895            Toast.makeText(getContext(), resId, Toast.LENGTH_SHORT).show();
896        }
897    }
898
899    private int getGroupCount() {
900        return getAdapter() != null ? getAdapter().getCount() : -1;
901    }
902
903    public void exitEditMode() {
904        mIsEditMode = false;
905        mActionBarAdapter.setSelectionMode(false);
906        displayDeleteButtons(false);
907    }
908}
909