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.provider.ContactsContract.Data; 21 import android.text.TextUtils; 22 import android.util.AttributeSet; 23 import android.view.LayoutInflater; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.widget.LinearLayout; 27 import android.widget.TextView; 28 29 import com.android.contacts.R; 30 import com.android.contacts.editor.Editor.EditorListener; 31 import com.android.contacts.model.RawContactModifier; 32 import com.android.contacts.model.RawContactDelta; 33 import com.android.contacts.common.model.ValuesDelta; 34 import com.android.contacts.common.model.dataitem.DataKind; 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 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 final int layoutResId = EditorUiUtils.getLayoutResourceId(mKind.mimeType); 190 try { 191 view = mInflater.inflate(layoutResId, mEditors, false); 192 } catch (Exception e) { 193 throw new RuntimeException( 194 "Cannot allocate editor with layout resource ID " + 195 layoutResId + " for MIME type " + mKind.mimeType + 196 " with error " + e.toString()); 197 } 198 199 view.setEnabled(isEnabled()); 200 201 if (view instanceof Editor) { 202 Editor editor = (Editor) view; 203 editor.setDeletable(true); 204 editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator); 205 editor.setEditorListener(this); 206 } 207 mEditors.addView(view); 208 return view; 209 } 210 211 /** 212 * Tests whether the given item has no changes (so it exists in the database) but is empty 213 */ 214 private boolean isEmptyNoop(ValuesDelta item) { 215 if (!item.isNoop()) return false; 216 final int fieldCount = mKind.fieldList.size(); 217 for (int i = 0; i < fieldCount; i++) { 218 final String column = mKind.fieldList.get(i).column; 219 final String value = item.getAsString(column); 220 if (!TextUtils.isEmpty(value)) return false; 221 } 222 return true; 223 } 224 225 private void updateSectionVisible() { 226 setVisibility(getEditorCount() != 0 ? VISIBLE : GONE); 227 } 228 229 protected void updateAddFooterVisible(boolean animate) { 230 if (!mReadOnly && (mKind.typeOverallMax != 1)) { 231 // First determine whether there are any existing empty editors. 232 updateEmptyEditors(); 233 // If there are no existing empty editors and it's possible to add 234 // another field, then make the "add footer" field visible. 235 if (!hasEmptyEditor() && RawContactModifier.canInsert(mState, mKind)) { 236 if (animate) { 237 EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter); 238 } else { 239 mAddFieldFooter.setVisibility(View.VISIBLE); 240 } 241 return; 242 } 243 } 244 if (animate) { 245 EditorAnimator.getInstance().hideAddFieldFooter(mAddFieldFooter); 246 } else { 247 mAddFieldFooter.setVisibility(View.GONE); 248 } 249 } 250 251 /** 252 * Updates the editors being displayed to the user removing extra empty 253 * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time. 254 */ 255 private void updateEmptyEditors() { 256 List<View> emptyEditors = getEmptyEditors(); 257 258 // If there is more than 1 empty editor, then remove it from the list of editors. 259 if (emptyEditors.size() > 1) { 260 for (View emptyEditorView : emptyEditors) { 261 // If no child {@link View}s are being focused on within 262 // this {@link View}, then remove this empty editor. 263 if (emptyEditorView.findFocus() == null) { 264 mEditors.removeView(emptyEditorView); 265 } 266 } 267 } 268 } 269 270 /** 271 * Returns a list of empty editor views in this section. 272 */ 273 private List<View> getEmptyEditors() { 274 List<View> emptyEditorViews = new ArrayList<View>(); 275 for (int i = 0; i < mEditors.getChildCount(); i++) { 276 View view = mEditors.getChildAt(i); 277 if (((Editor) view).isEmpty()) { 278 emptyEditorViews.add(view); 279 } 280 } 281 return emptyEditorViews; 282 } 283 284 /** 285 * Returns true if one of the editors has all of its fields empty, or false 286 * otherwise. 287 */ 288 private boolean hasEmptyEditor() { 289 return getEmptyEditors().size() > 0; 290 } 291 292 /** 293 * Returns true if all editors are empty. 294 */ 295 public boolean isEmpty() { 296 for (int i = 0; i < mEditors.getChildCount(); i++) { 297 View view = mEditors.getChildAt(i); 298 if (!((Editor) view).isEmpty()) { 299 return false; 300 } 301 } 302 return true; 303 } 304 305 /** 306 * Extends superclass implementation to also run tasks 307 * enqueued by {@link #runWhenWindowFocused}. 308 */ 309 @Override 310 public void onWindowFocusChanged(boolean hasWindowFocus) { 311 super.onWindowFocusChanged(hasWindowFocus); 312 if (hasWindowFocus) { 313 for (Runnable r: mRunWhenWindowFocused) { 314 r.run(); 315 } 316 mRunWhenWindowFocused.clear(); 317 } 318 } 319 320 /** 321 * Depending on whether we are in the currently-focused window, either run 322 * the argument immediately, or stash it until our window becomes focused. 323 */ 324 private void runWhenWindowFocused(Runnable r) { 325 if (hasWindowFocus()) { 326 r.run(); 327 } else { 328 mRunWhenWindowFocused.add(r); 329 } 330 } 331 332 /** 333 * Simple wrapper around {@link #runWhenWindowFocused} 334 * to ensure that it runs in the UI thread. 335 */ 336 private void postWhenWindowFocused(final Runnable r) { 337 post(new Runnable() { 338 @Override 339 public void run() { 340 runWhenWindowFocused(r); 341 } 342 }); 343 } 344 345 public void addItem() { 346 ValuesDelta values = null; 347 // If this is a list, we can freely add. If not, only allow adding the first. 348 if (mKind.typeOverallMax == 1) { 349 if (getEditorCount() == 1) { 350 return; 351 } 352 353 // If we already have an item, just make it visible 354 ArrayList<ValuesDelta> entries = mState.getMimeEntries(mKind.mimeType); 355 if (entries != null && entries.size() > 0) { 356 values = entries.get(0); 357 } 358 } 359 360 // Insert a new child, create its view and set its focus 361 if (values == null) { 362 values = RawContactModifier.insertChild(mState, mKind); 363 } 364 365 final View newField = createEditorView(values); 366 if (newField instanceof Editor) { 367 postWhenWindowFocused(new Runnable() { 368 @Override 369 public void run() { 370 newField.requestFocus(); 371 ((Editor)newField).editNewlyAddedField(); 372 } 373 }); 374 } 375 376 // Hide the "add field" footer because there is now a blank field. 377 mAddFieldFooter.setVisibility(View.GONE); 378 379 // Ensure we are visible 380 updateSectionVisible(); 381 } 382 383 public int getEditorCount() { 384 return mEditors.getChildCount(); 385 } 386 387 public DataKind getKind() { 388 return mKind; 389 } 390 } 391