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