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 com.android.contacts.R; 20 import com.android.contacts.editor.Editor.EditorListener; 21 import com.android.contacts.model.DataKind; 22 import com.android.contacts.model.EntityDelta; 23 import com.android.contacts.model.EntityDelta.ValuesDelta; 24 import com.android.contacts.model.EntityModifier; 25 26 import android.content.Context; 27 import android.provider.ContactsContract.Data; 28 import android.text.TextUtils; 29 import android.util.AttributeSet; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.LinearLayout; 34 import android.widget.TextView; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** 40 * Custom view for an entire section of data as segmented by 41 * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a 42 * section header and a trigger for adding new {@link Data} rows. 43 */ 44 public class KindSectionView extends LinearLayout implements EditorListener { 45 private static final String TAG = "KindSectionView"; 46 47 private TextView mTitle; 48 private ViewGroup mEditors; 49 private View mAddFieldFooter; 50 private String mTitleString; 51 52 private DataKind mKind; 53 private EntityDelta mState; 54 private boolean mReadOnly; 55 56 private ViewIdGenerator mViewIdGenerator; 57 58 private LayoutInflater mInflater; 59 60 private final ArrayList<Runnable> mRunWhenWindowFocused = new ArrayList<Runnable>(1); 61 KindSectionView(Context context)62 public KindSectionView(Context context) { 63 this(context, null); 64 } 65 KindSectionView(Context context, AttributeSet attrs)66 public KindSectionView(Context context, AttributeSet attrs) { 67 super(context, attrs); 68 } 69 70 @Override setEnabled(boolean enabled)71 public void setEnabled(boolean enabled) { 72 super.setEnabled(enabled); 73 if (mEditors != null) { 74 int childCount = mEditors.getChildCount(); 75 for (int i = 0; i < childCount; i++) { 76 mEditors.getChildAt(i).setEnabled(enabled); 77 } 78 } 79 80 if (enabled && !mReadOnly) { 81 mAddFieldFooter.setVisibility(View.VISIBLE); 82 } else { 83 mAddFieldFooter.setVisibility(View.GONE); 84 } 85 } 86 isReadOnly()87 public boolean isReadOnly() { 88 return mReadOnly; 89 } 90 91 /** {@inheritDoc} */ 92 @Override onFinishInflate()93 protected void onFinishInflate() { 94 setDrawingCacheEnabled(true); 95 setAlwaysDrawnWithCacheEnabled(true); 96 97 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 98 99 mTitle = (TextView) findViewById(R.id.kind_title); 100 mEditors = (ViewGroup) findViewById(R.id.kind_editors); 101 mAddFieldFooter = findViewById(R.id.add_field_footer); 102 mAddFieldFooter.setOnClickListener(new OnClickListener() { 103 @Override 104 public void onClick(View v) { 105 // Setup click listener to add an empty field when the footer is clicked. 106 mAddFieldFooter.setVisibility(View.GONE); 107 addItem(); 108 } 109 }); 110 } 111 112 @Override onDeleteRequested(Editor editor)113 public void onDeleteRequested(Editor editor) { 114 // If there is only 1 editor in the section, then don't allow the user to delete it. 115 // Just clear the fields in the editor. 116 final boolean animate; 117 if (getEditorCount() == 1) { 118 editor.clearAllFields(); 119 animate = true; 120 } else { 121 // Otherwise it's okay to delete this {@link Editor} 122 editor.deleteEditor(); 123 124 // This is already animated, don't do anything further here 125 animate = false; 126 } 127 updateAddFooterVisible(animate); 128 } 129 130 @Override onRequest(int request)131 public void onRequest(int request) { 132 // If a field has become empty or non-empty, then check if another row 133 // can be added dynamically. 134 if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) { 135 updateAddFooterVisible(true); 136 } 137 } 138 setState(DataKind kind, EntityDelta state, boolean readOnly, ViewIdGenerator vig)139 public void setState(DataKind kind, EntityDelta state, boolean readOnly, ViewIdGenerator vig) { 140 mKind = kind; 141 mState = state; 142 mReadOnly = readOnly; 143 mViewIdGenerator = vig; 144 145 setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX)); 146 147 // TODO: handle resources from remote packages 148 mTitleString = (kind.titleRes == -1 || kind.titleRes == 0) 149 ? "" 150 : getResources().getString(kind.titleRes); 151 mTitle.setText(mTitleString); 152 153 rebuildFromState(); 154 updateAddFooterVisible(false); 155 updateSectionVisible(); 156 } 157 getTitle()158 public String getTitle() { 159 return mTitleString; 160 } 161 setTitleVisible(boolean visible)162 public void setTitleVisible(boolean visible) { 163 findViewById(R.id.kind_title_layout).setVisibility(visible ? View.VISIBLE : View.GONE); 164 } 165 166 /** 167 * Build editors for all current {@link #mState} rows. 168 */ rebuildFromState()169 public void rebuildFromState() { 170 // Remove any existing editors 171 mEditors.removeAllViews(); 172 173 // Check if we are displaying anything here 174 boolean hasEntries = mState.hasMimeEntries(mKind.mimeType); 175 176 if (hasEntries) { 177 for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) { 178 // Skip entries that aren't visible 179 if (!entry.isVisible()) continue; 180 if (isEmptyNoop(entry)) continue; 181 182 createEditorView(entry); 183 } 184 } 185 } 186 187 188 /** 189 * Creates an EditorView for the given entry. This function must be used while constructing 190 * the views corresponding to the the object-model. The resulting EditorView is also added 191 * to the end of mEditors 192 */ createEditorView(ValuesDelta entry)193 private View createEditorView(ValuesDelta entry) { 194 final View view; 195 try { 196 view = mInflater.inflate(mKind.editorLayoutResourceId, mEditors, false); 197 } catch (Exception e) { 198 throw new RuntimeException( 199 "Cannot allocate editor with layout resource ID " + 200 mKind.editorLayoutResourceId + " for MIME type " + mKind.mimeType + 201 " with error " + e.toString()); 202 } 203 204 view.setEnabled(isEnabled()); 205 206 if (view instanceof Editor) { 207 Editor editor = (Editor) view; 208 editor.setDeletable(true); 209 editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator); 210 editor.setEditorListener(this); 211 } 212 mEditors.addView(view); 213 return view; 214 } 215 216 /** 217 * Tests whether the given item has no changes (so it exists in the database) but is empty 218 */ isEmptyNoop(ValuesDelta item)219 private boolean isEmptyNoop(ValuesDelta item) { 220 if (!item.isNoop()) return false; 221 final int fieldCount = mKind.fieldList.size(); 222 for (int i = 0; i < fieldCount; i++) { 223 final String column = mKind.fieldList.get(i).column; 224 final String value = item.getAsString(column); 225 if (!TextUtils.isEmpty(value)) return false; 226 } 227 return true; 228 } 229 updateSectionVisible()230 private void updateSectionVisible() { 231 setVisibility(getEditorCount() != 0 ? VISIBLE : GONE); 232 } 233 updateAddFooterVisible(boolean animate)234 protected void updateAddFooterVisible(boolean animate) { 235 if (!mReadOnly && (mKind.typeOverallMax != 1)) { 236 // First determine whether there are any existing empty editors. 237 updateEmptyEditors(); 238 // If there are no existing empty editors and it's possible to add 239 // another field, then make the "add footer" field visible. 240 if (!hasEmptyEditor() && EntityModifier.canInsert(mState, mKind)) { 241 if (animate) { 242 EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter); 243 } else { 244 mAddFieldFooter.setVisibility(View.VISIBLE); 245 } 246 return; 247 } 248 } 249 if (animate) { 250 EditorAnimator.getInstance().hideAddFieldFooter(mAddFieldFooter); 251 } else { 252 mAddFieldFooter.setVisibility(View.GONE); 253 } 254 } 255 256 /** 257 * Updates the editors being displayed to the user removing extra empty 258 * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time. 259 */ updateEmptyEditors()260 private void updateEmptyEditors() { 261 List<View> emptyEditors = getEmptyEditors(); 262 263 // If there is more than 1 empty editor, then remove it from the list of editors. 264 if (emptyEditors.size() > 1) { 265 for (View emptyEditorView : emptyEditors) { 266 // If no child {@link View}s are being focused on within 267 // this {@link View}, then remove this empty editor. 268 if (emptyEditorView.findFocus() == null) { 269 mEditors.removeView(emptyEditorView); 270 } 271 } 272 } 273 } 274 275 /** 276 * Returns a list of empty editor views in this section. 277 */ getEmptyEditors()278 private List<View> getEmptyEditors() { 279 List<View> emptyEditorViews = new ArrayList<View>(); 280 for (int i = 0; i < mEditors.getChildCount(); i++) { 281 View view = mEditors.getChildAt(i); 282 if (((Editor) view).isEmpty()) { 283 emptyEditorViews.add(view); 284 } 285 } 286 return emptyEditorViews; 287 } 288 289 /** 290 * Returns true if one of the editors has all of its fields empty, or false 291 * otherwise. 292 */ hasEmptyEditor()293 private boolean hasEmptyEditor() { 294 return getEmptyEditors().size() > 0; 295 } 296 297 /** 298 * Returns true if all editors are empty. 299 */ isEmpty()300 public boolean isEmpty() { 301 for (int i = 0; i < mEditors.getChildCount(); i++) { 302 View view = mEditors.getChildAt(i); 303 if (!((Editor) view).isEmpty()) { 304 return false; 305 } 306 } 307 return true; 308 } 309 310 /** 311 * Extends superclass implementation to also run tasks 312 * enqueued by {@link #runWhenWindowFocused}. 313 */ 314 @Override onWindowFocusChanged(boolean hasWindowFocus)315 public void onWindowFocusChanged(boolean hasWindowFocus) { 316 super.onWindowFocusChanged(hasWindowFocus); 317 if (hasWindowFocus) { 318 for (Runnable r: mRunWhenWindowFocused) { 319 r.run(); 320 } 321 mRunWhenWindowFocused.clear(); 322 } 323 } 324 325 /** 326 * Depending on whether we are in the currently-focused window, either run 327 * the argument immediately, or stash it until our window becomes focused. 328 */ runWhenWindowFocused(Runnable r)329 private void runWhenWindowFocused(Runnable r) { 330 if (hasWindowFocus()) { 331 r.run(); 332 } else { 333 mRunWhenWindowFocused.add(r); 334 } 335 } 336 337 /** 338 * Simple wrapper around {@link #runWhenWindowFocused} 339 * to ensure that it runs in the UI thread. 340 */ postWhenWindowFocused(final Runnable r)341 private void postWhenWindowFocused(final Runnable r) { 342 post(new Runnable() { 343 @Override 344 public void run() { 345 runWhenWindowFocused(r); 346 } 347 }); 348 } 349 addItem()350 public void addItem() { 351 ValuesDelta values = null; 352 // If this is a list, we can freely add. If not, only allow adding the first. 353 if (mKind.typeOverallMax == 1) { 354 if (getEditorCount() == 1) { 355 return; 356 } 357 358 // If we already have an item, just make it visible 359 ArrayList<ValuesDelta> entries = mState.getMimeEntries(mKind.mimeType); 360 if (entries != null && entries.size() > 0) { 361 values = entries.get(0); 362 } 363 } 364 365 // Insert a new child, create its view and set its focus 366 if (values == null) { 367 values = EntityModifier.insertChild(mState, mKind); 368 } 369 370 final View newField = createEditorView(values); 371 if (newField instanceof Editor) { 372 postWhenWindowFocused(new Runnable() { 373 @Override 374 public void run() { 375 newField.requestFocus(); 376 ((Editor)newField).editNewlyAddedField(); 377 } 378 }); 379 } 380 381 // Hide the "add field" footer because there is now a blank field. 382 mAddFieldFooter.setVisibility(View.GONE); 383 384 // Ensure we are visible 385 updateSectionVisible(); 386 } 387 getEditorCount()388 public int getEditorCount() { 389 return mEditors.getChildCount(); 390 } 391 getKind()392 public DataKind getKind() { 393 return mKind; 394 } 395 } 396