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 66 public GroupSelectionItem(long groupId, String title, boolean checked) { 67 this.mGroupId = groupId; 68 this.mTitle = title; 69 mChecked = checked; 70 } 71 72 public long getGroupId() { 73 return mGroupId; 74 } 75 76 public boolean isChecked() { 77 return mChecked; 78 } 79 80 public void setChecked(boolean checked) { 81 mChecked = checked; 82 } 83 84 @Override 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 97 public GroupMembershipAdapter(Context context, int textViewResourceId) { 98 super(context, textViewResourceId); 99 } 100 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 107 public int getItemViewType(int position) { 108 return getItemIsCheckable(position) ? 0 : 1; 109 } 110 111 @Override 112 public int getViewTypeCount() { 113 return 2; 114 } 115 116 @Override 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 155 public GroupMembershipView(Context context) { 156 super(context); 157 } 158 159 public GroupMembershipView(Context context, AttributeSet attrs) { 160 super(context, attrs); 161 } 162 163 @Override 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 173 public void setEnabled(boolean enabled) { 174 super.setEnabled(enabled); 175 if (mGroupList != null) { 176 mGroupList.setEnabled(enabled); 177 } 178 } 179 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 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. */ 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 */ 215 public boolean accountHasGroups() { 216 return mAccountHasGroups; 217 } 218 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 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 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 349 protected void onDetachedFromWindow() { 350 super.onDetachedFromWindow(); 351 UiClosables.closeQuietly(mPopup); 352 mPopup = null; 353 } 354 355 @Override 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 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 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 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