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.app.FragmentManager; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.database.Cursor; 24 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 25 import android.text.TextUtils; 26 import android.util.AttributeSet; 27 import android.view.View; 28 import android.view.View.OnClickListener; 29 import android.view.ViewGroup; 30 import android.widget.AdapterView; 31 import android.widget.AdapterView.OnItemClickListener; 32 import android.widget.ArrayAdapter; 33 import android.widget.CheckedTextView; 34 import android.widget.ImageView; 35 import android.widget.LinearLayout; 36 import android.widget.ListPopupWindow; 37 import android.widget.ListView; 38 import android.widget.TextView; 39 40 import com.android.contacts.GroupMetaDataLoader; 41 import com.android.contacts.R; 42 import com.android.contacts.group.GroupNameEditDialogFragment; 43 import com.android.contacts.model.RawContactDelta; 44 import com.android.contacts.model.RawContactModifier; 45 import com.android.contacts.model.ValuesDelta; 46 import com.android.contacts.model.account.AccountWithDataSet; 47 import com.android.contacts.model.dataitem.DataKind; 48 import com.android.contacts.util.UiClosables; 49 50 import com.google.common.base.Objects; 51 52 import 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 */ 58 public 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 GroupSelectionItem(long groupId, String title, boolean checked)70 public GroupSelectionItem(long groupId, String title, boolean checked) { 71 this.mGroupId = groupId; 72 this.mTitle = title; 73 mChecked = checked; 74 } 75 getGroupId()76 public long getGroupId() { 77 return mGroupId; 78 } 79 isChecked()80 public boolean isChecked() { 81 return mChecked; 82 } 83 setChecked(boolean checked)84 public void setChecked(boolean checked) { 85 mChecked = checked; 86 } 87 88 @Override toString()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 GroupMembershipAdapter(Context context, int textViewResourceId)104 public GroupMembershipAdapter(Context context, int textViewResourceId) { 105 super(context, textViewResourceId); 106 } 107 getItemIsCheckable(int position)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 getItemViewType(int position)114 public int getItemViewType(int position) { 115 return getItemIsCheckable(position) ? 0 : 1; 116 } 117 118 @Override getViewTypeCount()119 public int getViewTypeCount() { 120 return 2; 121 } 122 123 @Override getView(int position, View convertView, ViewGroup parent)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 getNewestGroupPosition()141 public int getNewestGroupPosition() { 142 return mNewestGroupPosition; 143 } 144 setNewestGroupPosition(int newestGroupPosition)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 GroupMembershipView(Context context)183 public GroupMembershipView(Context context) { 184 super(context); 185 } 186 GroupMembershipView(Context context, AttributeSet attrs)187 public GroupMembershipView(Context context, AttributeSet attrs) { 188 super(context, attrs); 189 } 190 191 @Override onFinishInflate()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 setGroupNameEditDialogFragment()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 setEnabled(boolean enabled)212 public void setEnabled(boolean enabled) { 213 super.setEnabled(enabled); 214 if (mGroupList != null) { 215 mGroupList.setEnabled(enabled); 216 } 217 } 218 setKind(DataKind kind)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 setGroupMetaData(Cursor groupMetaData)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. */ wasGroupMetaDataBound()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 */ accountHasGroups()254 public boolean accountHasGroups() { 255 return mAccountHasGroups; 256 } 257 setState(RawContactDelta state)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 updateView()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 onClick(View v)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 onDetachedFromWindow()396 protected void onDetachedFromWindow() { 397 super.onDetachedFromWindow(); 398 UiClosables.closeQuietly(mPopup); 399 mPopup = null; 400 } 401 402 @Override onItemClick(AdapterView<?> parent, View view, int position, long id)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 isGroupChecked(long groupId)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 hasMembership(long groupId)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 createNewGroup()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