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