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 String mAccountName;
138     private String mAccountType;
139     private String mDataSet;
140     private TextView mGroupList;
141     private GroupMembershipAdapter<GroupSelectionItem> mAdapter;
142     private long mDefaultGroupId;
143     private long mFavoritesGroupId;
144     private ListPopupWindow mPopup;
145     private DataKind mKind;
146     private boolean mDefaultGroupVisibilityKnown;
147     private boolean mDefaultGroupVisible;
148     private boolean mCreatedNewGroup;
149 
150     private String mNoGroupString;
151     private int mPrimaryTextColor;
152     private int mHintTextColor;
153 
GroupMembershipView(Context context)154     public GroupMembershipView(Context context) {
155         super(context);
156     }
157 
GroupMembershipView(Context context, AttributeSet attrs)158     public GroupMembershipView(Context context, AttributeSet attrs) {
159         super(context, attrs);
160     }
161 
162     @Override
onFinishInflate()163     protected void onFinishInflate() {
164         super.onFinishInflate();
165         Resources resources = mContext.getResources();
166         mPrimaryTextColor = resources.getColor(R.color.primary_text_color);
167         mHintTextColor = resources.getColor(R.color.editor_disabled_text_color);
168         mNoGroupString = mContext.getString(R.string.group_edit_field_hint_text);
169     }
170 
171     @Override
setEnabled(boolean enabled)172     public void setEnabled(boolean enabled) {
173         super.setEnabled(enabled);
174         if (mGroupList != null) {
175             mGroupList.setEnabled(enabled);
176         }
177     }
178 
setKind(DataKind kind)179     public void setKind(DataKind kind) {
180         mKind = kind;
181         final ImageView imageView = (ImageView) findViewById(R.id.kind_icon);
182         imageView.setContentDescription(getResources().getString(kind.titleRes));
183     }
184 
setGroupMetaData(Cursor groupMetaData)185     public void setGroupMetaData(Cursor groupMetaData) {
186         this.mGroupMetaData = groupMetaData;
187         updateView();
188         // Open up the list of groups if a new group was just created.
189         if (mCreatedNewGroup) {
190             mCreatedNewGroup = false;
191             onClick(this); // This causes the popup to open.
192             if (mPopup != null) {
193                 // Ensure that the newly created group is checked.
194                 int position = mAdapter.getCount() - 2;
195                 ListView listView = mPopup.getListView();
196                 if (listView != null && !listView.isItemChecked(position)) {
197                     // Newly created group is not checked, so check it.
198                     listView.setItemChecked(position, true);
199                     onItemClick(listView, null, position, listView.getItemIdAtPosition(position));
200                 }
201             }
202         }
203     }
204 
setState(RawContactDelta state)205     public void setState(RawContactDelta state) {
206         mState = state;
207         mAccountType = mState.getAccountType();
208         mAccountName = mState.getAccountName();
209         mDataSet = mState.getDataSet();
210         mDefaultGroupVisibilityKnown = false;
211         mCreatedNewGroup = false;
212         updateView();
213     }
214 
updateView()215     private void updateView() {
216         if (mGroupMetaData == null || mGroupMetaData.isClosed() || mAccountType == null
217                 || mAccountName == null) {
218             setVisibility(GONE);
219             return;
220         }
221 
222         boolean accountHasGroups = false;
223         mFavoritesGroupId = 0;
224         mDefaultGroupId = 0;
225 
226         StringBuilder sb = new StringBuilder();
227         mGroupMetaData.moveToPosition(-1);
228         while (mGroupMetaData.moveToNext()) {
229             String accountName = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
230             String accountType = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
231             String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET);
232             if (accountName.equals(mAccountName) && accountType.equals(mAccountType)
233                     && Objects.equal(dataSet, mDataSet)) {
234                 long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
235                 if (!mGroupMetaData.isNull(GroupMetaDataLoader.FAVORITES)
236                         && mGroupMetaData.getInt(GroupMetaDataLoader.FAVORITES) != 0) {
237                     mFavoritesGroupId = groupId;
238                 } else if (!mGroupMetaData.isNull(GroupMetaDataLoader.AUTO_ADD)
239                             && mGroupMetaData.getInt(GroupMetaDataLoader.AUTO_ADD) != 0) {
240                     mDefaultGroupId = groupId;
241                 } else {
242                     accountHasGroups = true;
243                 }
244 
245                 // Exclude favorites from the list - they are handled with special UI (star)
246                 // Also exclude the default group.
247                 if (groupId != mFavoritesGroupId && groupId != mDefaultGroupId
248                         && hasMembership(groupId)) {
249                     String title = mGroupMetaData.getString(GroupMetaDataLoader.TITLE);
250                     if (!TextUtils.isEmpty(title)) {
251                         if (sb.length() != 0) {
252                             sb.append(", ");
253                         }
254                         sb.append(title);
255                     }
256                 }
257             }
258         }
259 
260         if (!accountHasGroups) {
261             setVisibility(GONE);
262             return;
263         }
264 
265         if (mGroupList == null) {
266             mGroupList = (TextView) findViewById(R.id.group_list);
267             mGroupList.setOnClickListener(this);
268         }
269 
270         mGroupList.setEnabled(isEnabled());
271         if (sb.length() == 0) {
272             mGroupList.setText(mNoGroupString);
273             mGroupList.setTextColor(mHintTextColor);
274         } else {
275             mGroupList.setText(sb);
276             mGroupList.setTextColor(mPrimaryTextColor);
277         }
278         setVisibility(VISIBLE);
279 
280         if (!mDefaultGroupVisibilityKnown) {
281             // Only show the default group (My Contacts) if the contact is NOT in it
282             mDefaultGroupVisible = mDefaultGroupId != 0 && !hasMembership(mDefaultGroupId);
283             mDefaultGroupVisibilityKnown = true;
284         }
285     }
286 
287     @Override
onClick(View v)288     public void onClick(View v) {
289         if (UiClosables.closeQuietly(mPopup)) {
290             mPopup = null;
291             return;
292         }
293 
294         mAdapter = new GroupMembershipAdapter<GroupSelectionItem>(
295                 getContext(), R.layout.group_membership_list_item);
296 
297         mGroupMetaData.moveToPosition(-1);
298         while (mGroupMetaData.moveToNext()) {
299             String accountName = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME);
300             String accountType = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
301             String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET);
302             if (accountName.equals(mAccountName) && accountType.equals(mAccountType)
303                     && Objects.equal(dataSet, mDataSet)) {
304                 long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID);
305                 if (groupId != mFavoritesGroupId
306                         && (groupId != mDefaultGroupId || mDefaultGroupVisible)) {
307                     String title = mGroupMetaData.getString(GroupMetaDataLoader.TITLE);
308                     boolean checked = hasMembership(groupId);
309                     mAdapter.add(new GroupSelectionItem(groupId, title, checked));
310                 }
311             }
312         }
313 
314         mAdapter.add(new GroupSelectionItem(CREATE_NEW_GROUP_GROUP_ID,
315                 getContext().getString(R.string.create_group_item_label), false));
316 
317         mPopup = new ListPopupWindow(getContext(), null);
318         mPopup.setAnchorView(mGroupList);
319         mPopup.setAdapter(mAdapter);
320         mPopup.setModal(true);
321         mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
322         mPopup.show();
323 
324         ListView listView = mPopup.getListView();
325         listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
326         listView.setOverScrollMode(OVER_SCROLL_ALWAYS);
327         int count = mAdapter.getCount();
328         for (int i = 0; i < count; i++) {
329             listView.setItemChecked(i, mAdapter.getItem(i).isChecked());
330         }
331 
332         listView.setOnItemClickListener(this);
333     }
334 
335     @Override
onDetachedFromWindow()336     protected void onDetachedFromWindow() {
337         super.onDetachedFromWindow();
338         UiClosables.closeQuietly(mPopup);
339         mPopup = null;
340     }
341 
342     @Override
onItemClick(AdapterView<?> parent, View view, int position, long id)343     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
344         ListView list = (ListView) parent;
345         int count = mAdapter.getCount();
346 
347         if (list.isItemChecked(count - 1)) {
348             list.setItemChecked(count - 1, false);
349             createNewGroup();
350             return;
351         }
352 
353         for (int i = 0; i < count; i++) {
354             mAdapter.getItem(i).setChecked(list.isItemChecked(i));
355         }
356 
357         // First remove the memberships that have been unchecked
358         ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
359         if (entries != null) {
360             for (ValuesDelta entry : entries) {
361                 if (!entry.isDelete()) {
362                     Long groupId = entry.getGroupRowId();
363                     if (groupId != null && groupId != mFavoritesGroupId
364                             && (groupId != mDefaultGroupId || mDefaultGroupVisible)
365                             && !isGroupChecked(groupId)) {
366                         entry.markDeleted();
367                     }
368                 }
369             }
370         }
371 
372         // Now add the newly selected items
373         for (int i = 0; i < count; i++) {
374             GroupSelectionItem item = mAdapter.getItem(i);
375             long groupId = item.getGroupId();
376             if (item.isChecked() && !hasMembership(groupId)) {
377                 ValuesDelta entry = RawContactModifier.insertChild(mState, mKind);
378                 if (entry != null) {
379                     entry.setGroupRowId(groupId);
380                 }
381             }
382         }
383 
384         updateView();
385     }
386 
isGroupChecked(long groupId)387     private boolean isGroupChecked(long groupId) {
388         int count = mAdapter.getCount();
389         for (int i = 0; i < count; i++) {
390             GroupSelectionItem item = mAdapter.getItem(i);
391             if (groupId == item.getGroupId()) {
392                 return item.isChecked();
393             }
394         }
395         return false;
396     }
397 
hasMembership(long groupId)398     private boolean hasMembership(long groupId) {
399         if (groupId == mDefaultGroupId && mState.isContactInsert()) {
400             return true;
401         }
402 
403         ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE);
404         if (entries != null) {
405             for (ValuesDelta values : entries) {
406                 if (!values.isDelete()) {
407                     Long id = values.getGroupRowId();
408                     if (id != null && id == groupId) {
409                         return true;
410                     }
411                 }
412             }
413         }
414         return false;
415     }
416 
createNewGroup()417     private void createNewGroup() {
418         UiClosables.closeQuietly(mPopup);
419         mPopup = null;
420 
421         GroupCreationDialogFragment.show(
422                 ((Activity) getContext()).getFragmentManager(),
423                 mAccountType,
424                 mAccountName,
425                 mDataSet,
426                 new OnGroupCreatedListener() {
427                     @Override
428                     public void onGroupCreated() {
429                         mCreatedNewGroup = true;
430                     }
431                 });
432     }
433 
434 }
435