1 /* 2 * Copyright (C) 2009 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.content.Context; 20 import android.database.Cursor; 21 import android.os.Bundle; 22 import android.os.Parcelable; 23 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 24 import android.provider.ContactsContract.CommonDataKinds.Nickname; 25 import android.provider.ContactsContract.CommonDataKinds.Photo; 26 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 27 import android.provider.ContactsContract.Contacts; 28 import android.provider.ContactsContract.Data; 29 import android.util.AttributeSet; 30 import android.util.Pair; 31 import android.view.LayoutInflater; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.ImageView; 35 import android.widget.LinearLayout; 36 import android.widget.TextView; 37 38 import com.android.contacts.GroupMetaDataLoader; 39 import com.android.contacts.R; 40 import com.android.contacts.common.model.account.AccountType; 41 import com.android.contacts.common.model.account.AccountType.EditType; 42 import com.android.contacts.common.model.dataitem.DataKind; 43 import com.android.contacts.common.model.RawContactDelta; 44 import com.android.contacts.common.model.ValuesDelta; 45 import com.android.contacts.common.model.RawContactModifier; 46 47 import com.google.common.base.Objects; 48 49 import java.util.ArrayList; 50 51 /** 52 * Custom view that provides all the editor interaction for a specific 53 * {@link Contacts} represented through an {@link RawContactDelta}. Callers can 54 * reuse this view and quickly rebuild its contents through 55 * {@link #setState(RawContactDelta, AccountType, ViewIdGenerator)}. 56 * <p> 57 * Internal updates are performed against {@link ValuesDelta} so that the 58 * source {@link RawContact} can be swapped out. Any state-based changes, such as 59 * adding {@link Data} rows or changing {@link EditType}, are performed through 60 * {@link RawContactModifier} to ensure that {@link AccountType} are enforced. 61 */ 62 public class RawContactEditorView extends BaseRawContactEditorView { 63 private static final String KEY_SUPER_INSTANCE_STATE = "superInstanceState"; 64 65 private LayoutInflater mInflater; 66 67 private StructuredNameEditorView mName; 68 private PhoneticNameEditorView mPhoneticName; 69 private TextFieldsEditorView mNickName; 70 71 private GroupMembershipView mGroupMembershipView; 72 73 private ViewGroup mFields; 74 75 private View mAccountSelector; 76 private TextView mAccountSelectorTypeTextView; 77 private TextView mAccountSelectorNameTextView; 78 79 private View mAccountHeader; 80 private TextView mAccountHeaderTypeTextView; 81 private TextView mAccountHeaderNameTextView; 82 private ImageView mAccountIconImageView; 83 84 private long mRawContactId = -1; 85 private boolean mAutoAddToDefaultGroup = true; 86 private Cursor mGroupMetaData; 87 private DataKind mGroupMembershipKind; 88 private RawContactDelta mState; 89 RawContactEditorView(Context context)90 public RawContactEditorView(Context context) { 91 super(context); 92 } 93 RawContactEditorView(Context context, AttributeSet attrs)94 public RawContactEditorView(Context context, AttributeSet attrs) { 95 super(context, attrs); 96 } 97 98 @Override setEnabled(boolean enabled)99 public void setEnabled(boolean enabled) { 100 super.setEnabled(enabled); 101 102 View view = getPhotoEditor(); 103 if (view != null) { 104 view.setEnabled(enabled); 105 } 106 107 if (mName != null) { 108 mName.setEnabled(enabled); 109 } 110 111 if (mPhoneticName != null) { 112 mPhoneticName.setEnabled(enabled); 113 } 114 115 if (mFields != null) { 116 int count = mFields.getChildCount(); 117 for (int i = 0; i < count; i++) { 118 mFields.getChildAt(i).setEnabled(enabled); 119 } 120 } 121 122 if (mGroupMembershipView != null) { 123 mGroupMembershipView.setEnabled(enabled); 124 } 125 } 126 127 @Override onFinishInflate()128 protected void onFinishInflate() { 129 super.onFinishInflate(); 130 131 mInflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 132 133 mName = (StructuredNameEditorView)findViewById(R.id.edit_name); 134 mName.setDeletable(false); 135 136 mPhoneticName = (PhoneticNameEditorView)findViewById(R.id.edit_phonetic_name); 137 mPhoneticName.setDeletable(false); 138 139 mNickName = (TextFieldsEditorView)findViewById(R.id.edit_nick_name); 140 141 mFields = (ViewGroup)findViewById(R.id.sect_fields); 142 143 mAccountHeader = findViewById(R.id.account_header_container); 144 mAccountHeaderTypeTextView = (TextView) findViewById(R.id.account_type); 145 mAccountHeaderNameTextView = (TextView) findViewById(R.id.account_name); 146 mAccountIconImageView = (ImageView) findViewById(android.R.id.icon); 147 148 // The same header is used by both full editor and read-only editor view. The header is 149 // left-aligned with read-only editor view but is not aligned well with full editor. So we 150 // need to shift the text in the header a little bit for full editor. 151 LinearLayout accountInfoView = (LinearLayout) findViewById(R.id.account_info); 152 final int topBottomPaddingDp = (int) getResources().getDimension(R.dimen 153 .editor_account_header_expandable_top_bottom_padding); 154 final int leftPaddingDp = (int) getResources().getDimension(R.dimen 155 .editor_account_header_expandable_left_padding); 156 accountInfoView.setPadding(leftPaddingDp, topBottomPaddingDp, 0, topBottomPaddingDp); 157 158 mAccountSelector = findViewById(R.id.account_selector_container); 159 mAccountSelectorTypeTextView = (TextView) findViewById(R.id.account_type_selector); 160 mAccountSelectorNameTextView = (TextView) findViewById(R.id.account_name_selector); 161 } 162 163 @Override onSaveInstanceState()164 protected Parcelable onSaveInstanceState() { 165 Bundle bundle = new Bundle(); 166 // super implementation of onSaveInstanceState returns null 167 bundle.putParcelable(KEY_SUPER_INSTANCE_STATE, super.onSaveInstanceState()); 168 return bundle; 169 } 170 171 @Override onRestoreInstanceState(Parcelable state)172 protected void onRestoreInstanceState(Parcelable state) { 173 if (state instanceof Bundle) { 174 Bundle bundle = (Bundle) state; 175 super.onRestoreInstanceState(bundle.getParcelable(KEY_SUPER_INSTANCE_STATE)); 176 return; 177 } 178 super.onRestoreInstanceState(state); 179 } 180 181 /** 182 * Set the internal state for this view, given a current 183 * {@link RawContactDelta} state and the {@link AccountType} that 184 * apply to that state. 185 */ 186 @Override setState(RawContactDelta state, AccountType type, ViewIdGenerator vig, boolean isProfile)187 public void setState(RawContactDelta state, AccountType type, ViewIdGenerator vig, 188 boolean isProfile) { 189 190 mState = state; 191 192 // Remove any existing sections 193 mFields.removeAllViews(); 194 195 // Bail if invalid state or account type 196 if (state == null || type == null) return; 197 198 setId(vig.getId(state, null, null, ViewIdGenerator.NO_VIEW_INDEX)); 199 200 // Make sure we have a StructuredName 201 RawContactModifier.ensureKindExists(state, type, StructuredName.CONTENT_ITEM_TYPE); 202 203 mRawContactId = state.getRawContactId(); 204 205 // Fill in the account info 206 final Pair<String,String> accountInfo = isProfile 207 ? EditorUiUtils.getLocalAccountInfo(getContext(), state.getAccountName(), type) 208 : EditorUiUtils.getAccountInfo(getContext(), state.getAccountName(), type); 209 if (accountInfo.first == null) { 210 // Hide this view so the other text view will be centered vertically 211 mAccountHeaderNameTextView.setVisibility(View.GONE); 212 } else { 213 mAccountHeaderNameTextView.setVisibility(View.VISIBLE); 214 mAccountHeaderNameTextView.setText(accountInfo.first); 215 } 216 mAccountHeaderTypeTextView.setText(accountInfo.second); 217 updateAccountHeaderContentDescription(); 218 219 // The account selector and header are both used to display the same information. 220 mAccountSelectorTypeTextView.setText(mAccountHeaderTypeTextView.getText()); 221 mAccountSelectorTypeTextView.setVisibility(mAccountHeaderTypeTextView.getVisibility()); 222 mAccountSelectorNameTextView.setText(mAccountHeaderNameTextView.getText()); 223 mAccountSelectorNameTextView.setVisibility(mAccountHeaderNameTextView.getVisibility()); 224 // Showing the account header at the same time as the account selector drop down is 225 // confusing. They should be mutually exclusive. 226 mAccountHeader.setVisibility(mAccountSelector.getVisibility() == View.GONE 227 ? View.VISIBLE : View.GONE); 228 229 mAccountIconImageView.setImageDrawable(state.getRawContactAccountType(getContext()) 230 .getDisplayIcon(getContext())); 231 232 // Show photo editor when supported 233 RawContactModifier.ensureKindExists(state, type, Photo.CONTENT_ITEM_TYPE); 234 setHasPhotoEditor((type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null)); 235 getPhotoEditor().setEnabled(isEnabled()); 236 mName.setEnabled(isEnabled()); 237 238 mPhoneticName.setEnabled(isEnabled()); 239 240 // Show and hide the appropriate views 241 mFields.setVisibility(View.VISIBLE); 242 mName.setVisibility(View.VISIBLE); 243 mPhoneticName.setVisibility(View.VISIBLE); 244 245 mGroupMembershipKind = type.getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE); 246 if (mGroupMembershipKind != null) { 247 mGroupMembershipView = (GroupMembershipView)mInflater.inflate( 248 R.layout.item_group_membership, mFields, false); 249 mGroupMembershipView.setKind(mGroupMembershipKind); 250 mGroupMembershipView.setEnabled(isEnabled()); 251 } 252 253 // Create editor sections for each possible data kind 254 for (DataKind kind : type.getSortedDataKinds()) { 255 // Skip kind of not editable 256 if (!kind.editable) continue; 257 258 final String mimeType = kind.mimeType; 259 if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { 260 // Handle special case editor for structured name 261 final ValuesDelta primary = state.getPrimaryEntry(mimeType); 262 mName.setValues( 263 type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME), 264 primary, state, false, vig); 265 mPhoneticName.setValues( 266 type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME), 267 primary, state, false, vig); 268 // It is useful to use Nickname outside of a KindSectionView so that we can treat it 269 // as a part of StructuredName's fake KindSectionView, even though it uses a 270 // different CP2 mime-type. We do a bit of extra work below to make this possible. 271 final DataKind nickNameKind = type.getKindForMimetype(Nickname.CONTENT_ITEM_TYPE); 272 if (nickNameKind != null) { 273 ValuesDelta primaryNickNameEntry = state.getPrimaryEntry(nickNameKind.mimeType); 274 if (primaryNickNameEntry == null) { 275 primaryNickNameEntry = RawContactModifier.insertChild(state, nickNameKind); 276 } 277 mNickName.setValues(nickNameKind, primaryNickNameEntry, state, false, vig); 278 mNickName.setDeletable(false); 279 } else { 280 mPhoneticName.setPadding(0, 0, 0, (int) getResources().getDimension( 281 R.dimen.editor_padding_between_editor_views)); 282 mNickName.setVisibility(View.GONE); 283 } 284 } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 285 // Handle special case editor for photos 286 final ValuesDelta primary = state.getPrimaryEntry(mimeType); 287 getPhotoEditor().setValues(kind, primary, state, false, vig); 288 } else if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) { 289 if (mGroupMembershipView != null) { 290 mGroupMembershipView.setState(state); 291 mFields.addView(mGroupMembershipView); 292 } 293 } else if (DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME.equals(mimeType) 294 || DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME.equals(mimeType) 295 || Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { 296 // Don't create fields for each of these mime-types. They are handled specially. 297 continue; 298 } else { 299 // Otherwise use generic section-based editors 300 if (kind.fieldList == null) continue; 301 final KindSectionView section = (KindSectionView)mInflater.inflate( 302 R.layout.item_kind_section, mFields, false); 303 section.setEnabled(isEnabled()); 304 section.setState(kind, state, /* readOnly =*/ false, vig); 305 mFields.addView(section); 306 } 307 } 308 309 addToDefaultGroupIfNeeded(); 310 } 311 312 @Override setGroupMetaData(Cursor groupMetaData)313 public void setGroupMetaData(Cursor groupMetaData) { 314 mGroupMetaData = groupMetaData; 315 addToDefaultGroupIfNeeded(); 316 if (mGroupMembershipView != null) { 317 mGroupMembershipView.setGroupMetaData(groupMetaData); 318 } 319 } 320 setAutoAddToDefaultGroup(boolean flag)321 public void setAutoAddToDefaultGroup(boolean flag) { 322 this.mAutoAddToDefaultGroup = flag; 323 } 324 325 /** 326 * If automatic addition to the default group was requested (see 327 * {@link #setAutoAddToDefaultGroup}, checks if the raw contact is in any 328 * group and if it is not adds it to the default group (in case of Google 329 * contacts that's "My Contacts"). 330 */ addToDefaultGroupIfNeeded()331 private void addToDefaultGroupIfNeeded() { 332 if (!mAutoAddToDefaultGroup || mGroupMetaData == null || mGroupMetaData.isClosed() 333 || mState == null) { 334 return; 335 } 336 337 boolean hasGroupMembership = false; 338 ArrayList<ValuesDelta> entries = mState.getMimeEntries(GroupMembership.CONTENT_ITEM_TYPE); 339 if (entries != null) { 340 for (ValuesDelta values : entries) { 341 Long id = values.getGroupRowId(); 342 if (id != null && id.longValue() != 0) { 343 hasGroupMembership = true; 344 break; 345 } 346 } 347 } 348 349 if (!hasGroupMembership) { 350 long defaultGroupId = getDefaultGroupId(); 351 if (defaultGroupId != -1) { 352 ValuesDelta entry = RawContactModifier.insertChild(mState, mGroupMembershipKind); 353 if (entry != null) { 354 entry.setGroupRowId(defaultGroupId); 355 } 356 } 357 } 358 } 359 360 /** 361 * Returns the default group (e.g. "My Contacts") for the current raw contact's 362 * account. Returns -1 if there is no such group. 363 */ getDefaultGroupId()364 private long getDefaultGroupId() { 365 String accountType = mState.getAccountType(); 366 String accountName = mState.getAccountName(); 367 String accountDataSet = mState.getDataSet(); 368 mGroupMetaData.moveToPosition(-1); 369 while (mGroupMetaData.moveToNext()) { 370 String name = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_NAME); 371 String type = mGroupMetaData.getString(GroupMetaDataLoader.ACCOUNT_TYPE); 372 String dataSet = mGroupMetaData.getString(GroupMetaDataLoader.DATA_SET); 373 if (name.equals(accountName) && type.equals(accountType) 374 && Objects.equal(dataSet, accountDataSet)) { 375 long groupId = mGroupMetaData.getLong(GroupMetaDataLoader.GROUP_ID); 376 if (!mGroupMetaData.isNull(GroupMetaDataLoader.AUTO_ADD) 377 && mGroupMetaData.getInt(GroupMetaDataLoader.AUTO_ADD) != 0) { 378 return groupId; 379 } 380 } 381 } 382 return -1; 383 } 384 getNameEditor()385 public StructuredNameEditorView getNameEditor() { 386 return mName; 387 } 388 getPhoneticNameEditor()389 public TextFieldsEditorView getPhoneticNameEditor() { 390 return mPhoneticName; 391 } 392 getNickNameEditor()393 public TextFieldsEditorView getNickNameEditor() { 394 return mNickName; 395 } 396 397 @Override getRawContactId()398 public long getRawContactId() { 399 return mRawContactId; 400 } 401 } 402