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