1/*
2 * Copyright (C) 2010 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
17package com.android.contacts.editor;
18
19import android.app.Activity;
20import android.app.FragmentManager;
21import android.content.Context;
22import android.content.res.Resources;
23import android.database.Cursor;
24import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
25import android.text.TextUtils;
26import android.util.AttributeSet;
27import android.view.View;
28import android.view.View.OnClickListener;
29import android.view.ViewGroup;
30import android.widget.AdapterView;
31import android.widget.AdapterView.OnItemClickListener;
32import android.widget.ArrayAdapter;
33import android.widget.CheckedTextView;
34import android.widget.ImageView;
35import android.widget.LinearLayout;
36import android.widget.ListPopupWindow;
37import android.widget.ListView;
38import android.widget.TextView;
39
40import com.android.contacts.GroupMetaDataLoader;
41import com.android.contacts.R;
42import com.android.contacts.group.GroupNameEditDialogFragment;
43import com.android.contacts.model.RawContactDelta;
44import com.android.contacts.model.RawContactModifier;
45import com.android.contacts.model.ValuesDelta;
46import com.android.contacts.model.account.AccountWithDataSet;
47import com.android.contacts.model.dataitem.DataKind;
48import com.android.contacts.util.UiClosables;
49
50import com.google.common.base.Objects;
51
52import java.util.ArrayList;
53
54/**
55 * An editor for group membership.  Displays the current group membership list and
56 * brings up a dialog to change it.
57 */
58public class GroupMembershipView extends LinearLayout
59        implements OnClickListener, OnItemClickListener {
60
61    public static final String TAG_CREATE_GROUP_FRAGMENT = "createGroupDialog";
62
63    private static final int CREATE_NEW_GROUP_GROUP_ID = 133;
64
65    public static final class GroupSelectionItem {
66        private final long mGroupId;
67        private final String mTitle;
68        private boolean mChecked;
69
70        public GroupSelectionItem(long groupId, String title, boolean checked) {
71            this.mGroupId = groupId;
72            this.mTitle = title;
73            mChecked = checked;
74        }
75
76        public long getGroupId() {
77            return mGroupId;
78        }
79
80        public boolean isChecked() {
81            return mChecked;
82        }
83
84        public void setChecked(boolean checked) {
85            mChecked = checked;
86        }
87
88        @Override
89        public String toString() {
90            return mTitle;
91        }
92    }
93
94    /**
95     * Extends the array adapter to show checkmarks on all but the last list item for
96     * the group membership popup.  Note that this is highly specific to the fact that the
97     * group_membership_list_item.xml is a CheckedTextView object.
98     */
99    private class GroupMembershipAdapter<T> extends ArrayAdapter<T> {
100
101        // The position of the group with the largest group ID
102        private int mNewestGroupPosition;
103
104        public GroupMembershipAdapter(Context context, int textViewResourceId) {
105            super(context, textViewResourceId);
106        }
107
108        public boolean getItemIsCheckable(int position) {
109            // Item is checkable if it is NOT the last one in the list
110            return position != getCount()-1;
111        }
112
113        @Override
114        public int getItemViewType(int position) {
115            return getItemIsCheckable(position) ? 0 : 1;
116        }
117
118        @Override
119        public int getViewTypeCount() {
120            return 2;
121        }
122
123        @Override
124        public View getView(int position, View convertView, ViewGroup parent) {
125            final View itemView = super.getView(position, convertView, parent);
126            if (itemView == null) {
127                return null;
128            }
129
130            // Hide the checkable drawable.  This assumes that the item views
131            // are CheckedTextView objects
132            final CheckedTextView checkedTextView = (CheckedTextView)itemView;
133            if (!getItemIsCheckable(position)) {
134                checkedTextView.setCheckMarkDrawable(null);
135            }
136            checkedTextView.setTextColor(mPrimaryTextColor);
137
138            return checkedTextView;
139        }
140
141        public int getNewestGroupPosition() {
142            return mNewestGroupPosition;
143        }
144
145        public void setNewestGroupPosition(int newestGroupPosition) {
146            mNewestGroupPosition = newestGroupPosition;
147        }
148
149    }
150
151    private RawContactDelta mState;
152    private Cursor mGroupMetaData;
153    private boolean mAccountHasGroups;
154    private String mAccountName;
155    private String mAccountType;
156    private String mDataSet;
157    private TextView mGroupList;
158    private GroupMembershipAdapter<GroupSelectionItem> mAdapter;
159    private long mDefaultGroupId;
160    private long mFavoritesGroupId;
161    private ListPopupWindow mPopup;
162    private DataKind mKind;
163    private boolean mDefaultGroupVisibilityKnown;
164    private boolean mDefaultGroupVisible;
165    private boolean mCreatedNewGroup;
166    private GroupNameEditDialogFragment mGroupNameEditDialogFragment;
167    private GroupNameEditDialogFragment.Listener mListener =
168            new GroupNameEditDialogFragment.Listener() {
169                @Override
170                public void onGroupNameEditCancelled() {
171                }
172
173                @Override
174                public void onGroupNameEditCompleted(String name) {
175                    mCreatedNewGroup = true;
176                }
177            };
178
179    private String mNoGroupString;
180    private int mPrimaryTextColor;
181    private int mHintTextColor;
182
183    public GroupMembershipView(Context context) {
184        super(context);
185    }
186
187    public GroupMembershipView(Context context, AttributeSet attrs) {
188        super(context, attrs);
189    }
190
191    @Override
192    protected void onFinishInflate() {
193        super.onFinishInflate();
194        Resources resources = getContext().getResources();
195        mPrimaryTextColor = resources.getColor(R.color.primary_text_color);
196        mHintTextColor = resources.getColor(R.color.editor_disabled_text_color);
197        mNoGroupString = getContext().getString(R.string.group_edit_field_hint_text);
198        setFocusable(true);
199        setFocusableInTouchMode(true);
200    }
201
202    private void setGroupNameEditDialogFragment() {
203        final FragmentManager fragmentManager = ((Activity) getContext()).getFragmentManager();
204        mGroupNameEditDialogFragment = (GroupNameEditDialogFragment)
205                fragmentManager.findFragmentByTag(TAG_CREATE_GROUP_FRAGMENT);
206        if (mGroupNameEditDialogFragment != null) {
207            mGroupNameEditDialogFragment.setListener(mListener);
208        }
209    }
210
211    @Override
212    public void setEnabled(boolean enabled) {
213        super.setEnabled(enabled);
214        if (mGroupList != null) {
215            mGroupList.setEnabled(enabled);
216        }
217    }
218
219    public void setKind(DataKind kind) {
220        mKind = kind;
221        final ImageView imageView = (ImageView) findViewById(R.id.kind_icon);
222        imageView.setContentDescription(getResources().getString(kind.titleRes));
223    }
224
225    public void setGroupMetaData(Cursor groupMetaData) {
226        this.mGroupMetaData = groupMetaData;
227        updateView();
228        // Open up the list of groups if a new group was just created.
229        if (mCreatedNewGroup) {
230            mCreatedNewGroup = false;
231            onClick(this); // This causes the popup to open.
232            if (mPopup != null) {
233                // Ensure that the newly created group is checked.
234                final int position = mAdapter.getNewestGroupPosition();
235                ListView listView = mPopup.getListView();
236                if (listView != null && !listView.isItemChecked(position)) {
237                    // Newly created group is not checked, so check it.
238                    listView.setItemChecked(position, true);
239                    onItemClick(listView, null, position, listView.getItemIdAtPosition(position));
240                }
241            }
242        }
243    }
244
245    /** Whether {@link #setGroupMetaData} has been invoked yet. */
246    public boolean wasGroupMetaDataBound() {
247        return mGroupMetaData != null;
248    }
249
250    /**
251     * Return true if the account has groups to edit group membership for contacts
252     * belong to the account.
253     */
254    public boolean accountHasGroups() {
255        return mAccountHasGroups;
256    }
257
258    public void setState(RawContactDelta state) {
259        mState = state;
260        mAccountType = mState.getAccountType();
261        mAccountName = mState.getAccountName();
262        mDataSet = mState.getDataSet();
263        mDefaultGroupVisibilityKnown = false;
264        mCreatedNewGroup = false;
265        updateView();
266        setGroupNameEditDialogFragment();
267    }
268
269    private void updateView() {
270        if (mGroupMetaData == null || mGroupMetaData.isClosed() || mAccountType == null
271                || mAccountName == null) {
272            setVisibility(GONE);
273            return;
274        }
275
276        mFavoritesGroupId = 0;
277        mDefaultGroupId = 0;
278
279        StringBuilder sb = new StringBuilder();
280        mGroupMetaData.moveToPosition(-1);
281        while (mGroupMetaData.moveToNext()) {
282            String accountName = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
283            String accountType = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
284            String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET);
285            if (accountName.equals(mAccountName) && accountType.equals(mAccountType)
286                    && Objects.equal(dataSet, mDataSet)) {
287                long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
288                if (!mGroupMetaData.isNull(GroupMetaDataLoader.FAVORITES)
289                        && mGroupMetaData.getInt(GroupMetaDataLoader.FAVORITES) != 0) {
290                    mFavoritesGroupId = groupId;
291                } else if (!mGroupMetaData.isNull(GroupMetaDataLoader.AUTO_ADD)
292                            && mGroupMetaData.getInt(GroupMetaDataLoader.AUTO_ADD) != 0) {
293                    mDefaultGroupId = groupId;
294                } else {
295                    mAccountHasGroups = true;
296                }
297
298                // Exclude favorites from the list - they are handled with special UI (star)
299                // Also exclude the default group.
300                if (groupId != mFavoritesGroupId && groupId != mDefaultGroupId
301                        && hasMembership(groupId)) {
302                    String title = mGroupMetaData.getString(GroupMetaDataLoader.TITLE);
303                    if (!TextUtils.isEmpty(title)) {
304                        if (sb.length() != 0) {
305                            sb.append(", ");
306                        }
307                        sb.append(title);
308                    }
309                }
310            }
311        }
312
313        if (!mAccountHasGroups) {
314            setVisibility(GONE);
315            return;
316        }
317
318        if (mGroupList == null) {
319            mGroupList = (TextView) findViewById(R.id.group_list);
320            mGroupList.setOnClickListener(this);
321        }
322
323        mGroupList.setEnabled(isEnabled());
324        if (sb.length() == 0) {
325            mGroupList.setText(mNoGroupString);
326            mGroupList.setTextColor(mHintTextColor);
327        } else {
328            mGroupList.setText(sb);
329            mGroupList.setTextColor(mPrimaryTextColor);
330        }
331        setVisibility(VISIBLE);
332
333        if (!mDefaultGroupVisibilityKnown) {
334            // Only show the default group (My Contacts) if the contact is NOT in it
335            mDefaultGroupVisible = mDefaultGroupId != 0 && !hasMembership(mDefaultGroupId);
336            mDefaultGroupVisibilityKnown = true;
337        }
338    }
339
340    @Override
341    public void onClick(View v) {
342        if (UiClosables.closeQuietly(mPopup)) {
343            mPopup = null;
344            return;
345        }
346
347        requestFocus();
348        mAdapter = new GroupMembershipAdapter<GroupSelectionItem>(
349                getContext(), R.layout.group_membership_list_item);
350
351        long newestGroupId = -1;
352
353        mGroupMetaData.moveToPosition(-1);
354        while (mGroupMetaData.moveToNext()) {
355            String accountName = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
356            String accountType = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
357            String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET);
358            if (accountName.equals(mAccountName) && accountType.equals(mAccountType)
359                    && Objects.equal(dataSet, mDataSet)) {
360                long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
361                if (groupId != mFavoritesGroupId
362                        && (groupId != mDefaultGroupId || mDefaultGroupVisible)) {
363                    if (groupId > newestGroupId) {
364                        newestGroupId = groupId;
365                        mAdapter.setNewestGroupPosition(mAdapter.getCount());
366                    }
367                    String title = mGroupMetaData.getString(GroupMetaDataLoader.TITLE);
368                    boolean checked = hasMembership(groupId);
369                    mAdapter.add(new GroupSelectionItem(groupId, title, checked));
370                }
371            }
372        }
373
374        mAdapter.add(new GroupSelectionItem(CREATE_NEW_GROUP_GROUP_ID,
375                getContext().getString(R.string.create_group_item_label), false));
376
377        mPopup = new ListPopupWindow(getContext(), null);
378        mPopup.setAnchorView(mGroupList);
379        mPopup.setAdapter(mAdapter);
380        mPopup.setModal(true);
381        mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
382        mPopup.show();
383
384        ListView listView = mPopup.getListView();
385        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
386        listView.setOverScrollMode(OVER_SCROLL_ALWAYS);
387        int count = mAdapter.getCount();
388        for (int i = 0; i < count; i++) {
389            listView.setItemChecked(i, mAdapter.getItem(i).isChecked());
390        }
391
392        listView.setOnItemClickListener(this);
393    }
394
395    @Override
396    protected void onDetachedFromWindow() {
397        super.onDetachedFromWindow();
398        UiClosables.closeQuietly(mPopup);
399        mPopup = null;
400    }
401
402    @Override
403    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
404        ListView list = (ListView) parent;
405        int count = mAdapter.getCount();
406
407        if (list.isItemChecked(count - 1)) {
408            list.setItemChecked(count - 1, false);
409            createNewGroup();
410            return;
411        }
412
413        for (int i = 0; i < count; i++) {
414            mAdapter.getItem(i).setChecked(list.isItemChecked(i));
415        }
416
417        // First remove the memberships that have been unchecked
418        ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
419        if (entries != null) {
420            for (ValuesDelta entry : entries) {
421                if (!entry.isDelete()) {
422                    Long groupId = entry.getGroupRowId();
423                    if (groupId != null && groupId != mFavoritesGroupId
424                            && (groupId != mDefaultGroupId || mDefaultGroupVisible)
425                            && !isGroupChecked(groupId)) {
426                        entry.markDeleted();
427                    }
428                }
429            }
430        }
431
432        // Now add the newly selected items
433        for (int i = 0; i < count; i++) {
434            GroupSelectionItem item = mAdapter.getItem(i);
435            long groupId = item.getGroupId();
436            if (item.isChecked() && !hasMembership(groupId)) {
437                ValuesDelta entry = RawContactModifier.insertChild(mState, mKind);
438                if (entry != null) {
439                    entry.setGroupRowId(groupId);
440                }
441            }
442        }
443
444        updateView();
445    }
446
447    private boolean isGroupChecked(long groupId) {
448        int count = mAdapter.getCount();
449        for (int i = 0; i < count; i++) {
450            GroupSelectionItem item = mAdapter.getItem(i);
451            if (groupId == item.getGroupId()) {
452                return item.isChecked();
453            }
454        }
455        return false;
456    }
457
458    private boolean hasMembership(long groupId) {
459        if (groupId == mDefaultGroupId && mState.isContactInsert()) {
460            return true;
461        }
462
463        ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
464        if (entries != null) {
465            for (ValuesDelta values : entries) {
466                if (!values.isDelete()) {
467                    Long id = values.getGroupRowId();
468                    if (id != null && id == groupId) {
469                        return true;
470                    }
471                }
472            }
473        }
474        return false;
475    }
476
477    private void createNewGroup() {
478        UiClosables.closeQuietly(mPopup);
479        mPopup = null;
480        mGroupNameEditDialogFragment =
481                    GroupNameEditDialogFragment.newInstanceForCreation(
482                            new AccountWithDataSet(mAccountName, mAccountType, mDataSet), null);
483        mGroupNameEditDialogFragment.setListener(mListener);
484        mGroupNameEditDialogFragment.show(
485                ((Activity) getContext()).getFragmentManager(),
486                TAG_CREATE_GROUP_FRAGMENT);
487    }
488}
489