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.graphics.drawable.Drawable; 21 import android.provider.Contacts.GroupMembership; 22 import android.provider.ContactsContract.CommonDataKinds.Email; 23 import android.provider.ContactsContract.CommonDataKinds.Event; 24 import android.provider.ContactsContract.CommonDataKinds.Im; 25 import android.provider.ContactsContract.CommonDataKinds.Note; 26 import android.provider.ContactsContract.CommonDataKinds.Organization; 27 import android.provider.ContactsContract.CommonDataKinds.Phone; 28 import android.provider.ContactsContract.CommonDataKinds.Photo; 29 import android.provider.ContactsContract.CommonDataKinds.Relation; 30 import android.provider.ContactsContract.CommonDataKinds.SipAddress; 31 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 32 import android.provider.ContactsContract.CommonDataKinds.Website; 33 import android.provider.ContactsContract.Data; 34 import android.text.TextUtils; 35 import android.util.AttributeSet; 36 import android.view.LayoutInflater; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.widget.ImageView; 40 import android.widget.LinearLayout; 41 42 import com.android.contacts.R; 43 import com.android.contacts.common.model.RawContactDelta; 44 import com.android.contacts.common.model.RawContactModifier; 45 import com.android.contacts.common.model.ValuesDelta; 46 import com.android.contacts.common.model.dataitem.DataKind; 47 import com.android.contacts.editor.Editor.EditorListener; 48 49 import java.util.ArrayList; 50 import java.util.List; 51 52 /** 53 * Custom view for an entire section of data as segmented by 54 * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a 55 * section header and a trigger for adding new {@link Data} rows. 56 */ 57 public class KindSectionView extends LinearLayout implements EditorListener { 58 59 public interface Listener { 60 61 /** 62 * Invoked when any editor that is displayed in this section view is deleted by the user. 63 */ onDeleteRequested(Editor editor)64 public void onDeleteRequested(Editor editor); 65 } 66 67 private ViewGroup mEditors; 68 private ImageView mIcon; 69 70 private DataKind mKind; 71 private RawContactDelta mState; 72 private boolean mReadOnly; 73 private boolean mShowOneEmptyEditor; 74 75 /** 76 * Whether this KindSectionView will be removed from the layout. 77 * We need this because we want to animate KindSectionViews away (which takes time), 78 * but calculate which KindSectionViews will be visible immediately after starting removal 79 * animations. 80 */ 81 private boolean mMarkedForRemoval; 82 83 private ViewIdGenerator mViewIdGenerator; 84 85 private LayoutInflater mInflater; 86 87 private Listener mListener; 88 KindSectionView(Context context)89 public KindSectionView(Context context) { 90 this(context, null); 91 } 92 KindSectionView(Context context, AttributeSet attrs)93 public KindSectionView(Context context, AttributeSet attrs) { 94 super(context, attrs); 95 } 96 97 @Override setEnabled(boolean enabled)98 public void setEnabled(boolean enabled) { 99 super.setEnabled(enabled); 100 if (mEditors != null) { 101 int childCount = mEditors.getChildCount(); 102 for (int i = 0; i < childCount; i++) { 103 mEditors.getChildAt(i).setEnabled(enabled); 104 } 105 } 106 107 updateEmptyEditors(/* shouldAnimate = */ true); 108 } 109 isReadOnly()110 public boolean isReadOnly() { 111 return mReadOnly; 112 } 113 114 /** {@inheritDoc} */ 115 @Override onFinishInflate()116 protected void onFinishInflate() { 117 setDrawingCacheEnabled(true); 118 setAlwaysDrawnWithCacheEnabled(true); 119 120 mInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 121 122 mEditors = (ViewGroup) findViewById(R.id.kind_editors); 123 mIcon = (ImageView) findViewById(R.id.kind_icon); 124 } 125 126 @Override onDeleteRequested(Editor editor)127 public void onDeleteRequested(Editor editor) { 128 if (mShowOneEmptyEditor && getEditorCount() == 1) { 129 // If there is only 1 editor in the section, then don't allow the user to delete it. 130 // Just clear the fields in the editor. 131 editor.clearAllFields(); 132 } else { 133 // If there is a listener, let it decide whether to delete the Editor or the entire 134 // KindSectionView so that there is no jank from both animations happening in succession. 135 if (mListener != null) { 136 editor.markDeleted(); 137 mListener.onDeleteRequested(editor); 138 } else { 139 editor.deleteEditor(); 140 } 141 } 142 } 143 144 /** 145 * Calling this signifies that this entire section view is intended to be removed from the 146 * layout. Note, calling this does not change the deleted state of any underlying 147 * {@link Editor}, i.e. {@link com.android.contacts.common.model.ValuesDelta#markDeleted()} 148 * is not invoked on any editor in this section. It is purely marked for higher level UI 149 * layers to manipulate the layout w/o introducing jank. 150 * See b/22228718 for context. 151 */ markForRemoval()152 public void markForRemoval() { 153 mMarkedForRemoval = true; 154 } 155 156 /** 157 * Whether the entire section view is intended to be removed from the layout. 158 */ isMarkedForRemoval()159 public boolean isMarkedForRemoval() { 160 return mMarkedForRemoval; 161 } 162 163 @Override onRequest(int request)164 public void onRequest(int request) { 165 // If a field has become empty or non-empty, then check if another row 166 // can be added dynamically. 167 if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) { 168 updateEmptyEditors(/* shouldAnimate = */ true); 169 } 170 } 171 172 /** 173 * @param showOneEmptyEditor If true, one empty input will always be displayed, 174 * otherwise an empty input will only be displayed if there is no non-empty value. 175 */ setShowOneEmptyEditor(boolean showOneEmptyEditor)176 public void setShowOneEmptyEditor(boolean showOneEmptyEditor) { 177 mShowOneEmptyEditor = showOneEmptyEditor; 178 } 179 setListener(Listener listener)180 public void setListener(Listener listener) { 181 mListener = listener; 182 } 183 setIconVisibility(boolean visible)184 public void setIconVisibility(boolean visible) { 185 mIcon.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); 186 } 187 setState(DataKind kind, RawContactDelta state, boolean readOnly, ViewIdGenerator vig)188 public void setState(DataKind kind, RawContactDelta state, boolean readOnly, 189 ViewIdGenerator vig) { 190 mKind = kind; 191 mState = state; 192 mReadOnly = readOnly; 193 mViewIdGenerator = vig; 194 195 setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX)); 196 197 // TODO: handle resources from remote packages 198 final String titleString = (kind.titleRes == -1 || kind.titleRes == 0) 199 ? "" 200 : getResources().getString(kind.titleRes); 201 mIcon.setContentDescription(titleString); 202 203 mIcon.setImageDrawable(getMimeTypeDrawable(kind.mimeType)); 204 if (mIcon.getDrawable() == null) { 205 mIcon.setContentDescription(null); 206 } 207 208 rebuildFromState(); 209 updateEmptyEditors(/* shouldAnimate = */ false); 210 } 211 212 /** 213 * Build editors for all current {@link #mState} rows. 214 */ rebuildFromState()215 private void rebuildFromState() { 216 // Remove any existing editors 217 mEditors.removeAllViews(); 218 219 // Check if we are displaying anything here 220 boolean hasEntries = mState.hasMimeEntries(mKind.mimeType); 221 222 if (hasEntries) { 223 for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) { 224 // Skip entries that aren't visible 225 if (!entry.isVisible()) continue; 226 if (isEmptyNoop(entry)) continue; 227 228 createEditorView(entry); 229 } 230 } 231 } 232 233 234 /** 235 * Creates an EditorView for the given entry. This function must be used while constructing 236 * the views corresponding to the the object-model. The resulting EditorView is also added 237 * to the end of mEditors 238 */ createEditorView(ValuesDelta entry)239 private View createEditorView(ValuesDelta entry) { 240 final View view; 241 final int layoutResId = EditorUiUtils.getLayoutResourceId(mKind.mimeType); 242 try { 243 view = mInflater.inflate(layoutResId, mEditors, false); 244 } catch (Exception e) { 245 throw new RuntimeException( 246 "Cannot allocate editor with layout resource ID " + 247 layoutResId + " for MIME type " + mKind.mimeType + 248 " with error " + e.toString()); 249 } 250 // Hide the types drop downs until the associated edit field is focused 251 if (view instanceof LabeledEditorView) { 252 ((LabeledEditorView) view).setHideTypeInitially(true); 253 } 254 255 view.setEnabled(isEnabled()); 256 257 if (view instanceof Editor) { 258 Editor editor = (Editor) view; 259 editor.setDeletable(true); 260 editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator); 261 editor.setEditorListener(this); 262 } 263 mEditors.addView(view); 264 return view; 265 } 266 267 /** 268 * Tests whether the given item has no changes (so it exists in the database) but is empty 269 */ isEmptyNoop(ValuesDelta item)270 private boolean isEmptyNoop(ValuesDelta item) { 271 if (!item.isNoop()) return false; 272 final int fieldCount = mKind.fieldList.size(); 273 for (int i = 0; i < fieldCount; i++) { 274 final String column = mKind.fieldList.get(i).column; 275 final String value = item.getAsString(column); 276 if (!TextUtils.isEmpty(value)) return false; 277 } 278 return true; 279 } 280 281 /** 282 * Updates the editors being displayed to the user removing extra empty 283 * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time. 284 */ updateEmptyEditors(boolean shouldAnimate)285 public void updateEmptyEditors(boolean shouldAnimate) { 286 287 final List<View> emptyEditors = getEmptyEditors(); 288 289 // If there is more than 1 empty editor, then remove it from the list of editors. 290 if (emptyEditors.size() > 1) { 291 for (final View emptyEditorView : emptyEditors) { 292 // If no child {@link View}s are being focused on within this {@link View}, then 293 // remove this empty editor. We can assume that at least one empty editor has focus. 294 // The only way to get two empty editors is by deleting characters from a non-empty 295 // editor, in which case this editor has focus. 296 if (emptyEditorView.findFocus() == null) { 297 final Editor editor = (Editor) emptyEditorView; 298 if (shouldAnimate) { 299 editor.deleteEditor(); 300 } else { 301 mEditors.removeView(emptyEditorView); 302 } 303 } 304 } 305 } else if (mKind == null) { 306 // There is nothing we can do. 307 return; 308 } else if (isReadOnly()) { 309 // We don't show empty editors for read only data kinds. 310 return; 311 } else if (mKind.typeOverallMax == getEditorCount() && mKind.typeOverallMax != 0) { 312 // We have already reached the maximum number of editors. Lets not add any more. 313 return; 314 } else if (emptyEditors.size() == 1) { 315 // We have already reached the maximum number of empty editors. Lets not add any more. 316 return; 317 } else if (mShowOneEmptyEditor) { 318 final ValuesDelta values = RawContactModifier.insertChild(mState, mKind); 319 final View newField = createEditorView(values); 320 if (shouldAnimate) { 321 newField.setVisibility(View.GONE); 322 EditorAnimator.getInstance().showFieldFooter(newField); 323 } 324 } 325 } 326 327 /** 328 * Returns a list of empty editor views in this section. 329 */ getEmptyEditors()330 private List<View> getEmptyEditors() { 331 List<View> emptyEditorViews = new ArrayList<View>(); 332 for (int i = 0; i < mEditors.getChildCount(); i++) { 333 View view = mEditors.getChildAt(i); 334 if (((Editor) view).isEmpty()) { 335 emptyEditorViews.add(view); 336 } 337 } 338 return emptyEditorViews; 339 } 340 areAllEditorsEmpty()341 public boolean areAllEditorsEmpty() { 342 for (int i = 0; i < mEditors.getChildCount(); i++) { 343 final View view = mEditors.getChildAt(i); 344 if (!((Editor) view).isEmpty()) { 345 return false; 346 } 347 } 348 return true; 349 } 350 getEditorCount()351 public int getEditorCount() { 352 return mEditors.getChildCount(); 353 } 354 getKind()355 public DataKind getKind() { 356 return mKind; 357 } 358 359 /** 360 * Return an icon that represents {@param mimeType}. 361 */ getMimeTypeDrawable(String mimeType)362 private Drawable getMimeTypeDrawable(String mimeType) { 363 switch (mimeType) { 364 case StructuredPostal.CONTENT_ITEM_TYPE: 365 return getResources().getDrawable(R.drawable.ic_place_24dp); 366 case SipAddress.CONTENT_ITEM_TYPE: 367 return getResources().getDrawable(R.drawable.ic_dialer_sip_black_24dp); 368 case Phone.CONTENT_ITEM_TYPE: 369 return getResources().getDrawable(R.drawable.ic_phone_24dp); 370 case Im.CONTENT_ITEM_TYPE: 371 return getResources().getDrawable(R.drawable.ic_message_24dp); 372 case Event.CONTENT_ITEM_TYPE: 373 return getResources().getDrawable(R.drawable.ic_event_24dp); 374 case Email.CONTENT_ITEM_TYPE: 375 return getResources().getDrawable(R.drawable.ic_email_24dp); 376 case Website.CONTENT_ITEM_TYPE: 377 return getResources().getDrawable(R.drawable.ic_public_black_24dp); 378 case Photo.CONTENT_ITEM_TYPE: 379 return getResources().getDrawable(R.drawable.ic_camera_alt_black_24dp); 380 case GroupMembership.CONTENT_ITEM_TYPE: 381 return getResources().getDrawable(R.drawable.ic_people_black_24dp); 382 case Organization.CONTENT_ITEM_TYPE: 383 return getResources().getDrawable(R.drawable.ic_business_black_24dp); 384 case Note.CONTENT_ITEM_TYPE: 385 return getResources().getDrawable(R.drawable.ic_insert_comment_black_24dp); 386 case Relation.CONTENT_ITEM_TYPE: 387 return getResources().getDrawable(R.drawable.ic_circles_extended_black_24dp); 388 default: 389 return null; 390 } 391 } 392 } 393