LabeledEditorView.java revision 12a93637e406ba7f3da4db8d53cd035c2c1d6e6e
1/* 2 * Copyright (C) 2010 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 com.android.contacts.ContactsUtils; 20import com.android.contacts.R; 21import com.android.contacts.model.AccountType.EditType; 22import com.android.contacts.model.DataKind; 23import com.android.contacts.model.EntityDelta; 24import com.android.contacts.model.EntityDelta.ValuesDelta; 25import com.android.contacts.model.EntityModifier; 26import com.android.contacts.util.DialogManager; 27import com.android.contacts.util.DialogManager.DialogShowingView; 28 29import android.app.AlertDialog; 30import android.app.Dialog; 31import android.content.Context; 32import android.content.DialogInterface; 33import android.content.Entity; 34import android.os.Bundle; 35import android.os.Handler; 36import android.text.TextUtils; 37import android.util.AttributeSet; 38import android.view.LayoutInflater; 39import android.view.View; 40import android.view.ViewGroup; 41import android.view.inputmethod.EditorInfo; 42import android.widget.AdapterView; 43import android.widget.AdapterView.OnItemSelectedListener; 44import android.widget.ArrayAdapter; 45import android.widget.EditText; 46import android.widget.ImageView; 47import android.widget.LinearLayout; 48import android.widget.Spinner; 49import android.widget.TextView; 50 51import java.util.List; 52 53/** 54 * Base class for editors that handles labels and values. Uses 55 * {@link ValuesDelta} to read any existing {@link Entity} values, and to 56 * correctly write any changes values. 57 */ 58public abstract class LabeledEditorView extends LinearLayout implements Editor, DialogShowingView { 59 protected static final String DIALOG_ID_KEY = "dialog_id"; 60 private static final int DIALOG_ID_CUSTOM = 1; 61 62 private static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT 63 | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; 64 65 private TextView mTitle; 66 private Spinner mLabel; 67 private EditTypeAdapter mEditTypeAdapter; 68 private View mDeleteContainer; 69 private ImageView mDelete; 70 71 private DataKind mKind; 72 private ValuesDelta mEntry; 73 private EntityDelta mState; 74 private boolean mReadOnly; 75 private boolean mWasEmpty = true; 76 77 private EditType mType; 78 79 private ViewIdGenerator mViewIdGenerator; 80 private DialogManager mDialogManager = null; 81 private EditorListener mListener; 82 protected int mMinLineItemHeight; 83 84 /** 85 * A marker in the spinner adapter of the currently selected custom type. 86 */ 87 public static final EditType CUSTOM_SELECTION = new EditType(0, 0); 88 89 private OnItemSelectedListener mSpinnerListener = new OnItemSelectedListener() { 90 91 @Override 92 public void onItemSelected( 93 AdapterView<?> parent, View view, int position, long id) { 94 onTypeSelectionChange(position); 95 } 96 97 @Override 98 public void onNothingSelected(AdapterView<?> parent) { 99 } 100 }; 101 102 public LabeledEditorView(Context context) { 103 super(context); 104 init(context); 105 } 106 107 public LabeledEditorView(Context context, AttributeSet attrs) { 108 super(context, attrs); 109 init(context); 110 } 111 112 public LabeledEditorView(Context context, AttributeSet attrs, int defStyle) { 113 super(context, attrs, defStyle); 114 init(context); 115 } 116 117 private void init(Context context) { 118 mMinLineItemHeight = context.getResources().getDimensionPixelSize( 119 R.dimen.editor_min_line_item_height); 120 } 121 122 /** {@inheritDoc} */ 123 @Override 124 protected void onFinishInflate() { 125 126 mTitle = (TextView) findViewById(R.id.title); 127 128 mLabel = (Spinner) findViewById(R.id.spinner); 129 mLabel.setOnItemSelectedListener(mSpinnerListener); 130 131 mDelete = (ImageView) findViewById(R.id.delete_button); 132 mDeleteContainer = findViewById(R.id.delete_button_container); 133 mDeleteContainer.setOnClickListener(new OnClickListener() { 134 @Override 135 public void onClick(View v) { 136 // defer removal of this button so that the pressed state is visible shortly 137 new Handler().post(new Runnable() { 138 @Override 139 public void run() { 140 // Keep around in model, but mark as deleted 141 mEntry.markDeleted(); 142 143 ((ViewGroup) getParent()).removeView(LabeledEditorView.this); 144 145 if (mListener != null) { 146 // Notify listener when present 147 mListener.onDeleted(LabeledEditorView.this); 148 } 149 } 150 }); 151 } 152 }); 153 } 154 155 public boolean isReadOnly() { 156 return mReadOnly; 157 } 158 159 public int getBaseline(int row) { 160 if (row == 0 && mLabel != null) { 161 return mLabel.getBaseline(); 162 } 163 return -1; 164 } 165 166 /** 167 * Creates or removes the type/label button. Doesn't do anything if already correctly configured 168 */ 169 private void setupLabelButton(boolean shouldExist) { 170 if (shouldExist) { 171 mLabel.setEnabled(!mReadOnly && isEnabled()); 172 mLabel.setVisibility(View.VISIBLE); 173 174 // Since there's a spinner for this editor, use this as the title 175 // instead of the title TextView. 176 mTitle.setVisibility(View.GONE); 177 } else { 178 mLabel.setVisibility(View.GONE); 179 } 180 } 181 182 /** 183 * Creates or removes the remove button. Doesn't do anything if already correctly configured 184 */ 185 private void setupDeleteButton(boolean shouldExist) { 186 if (shouldExist) { 187 mDeleteContainer.setVisibility(View.VISIBLE); 188 mDelete.setEnabled(!mReadOnly && isEnabled()); 189 } else { 190 mDeleteContainer.setVisibility(View.GONE); 191 } 192 } 193 194 protected void onOptionalFieldVisibilityChange() { 195 if (mListener != null) { 196 mListener.onRequest(EditorListener.EDITOR_FORM_CHANGED); 197 } 198 } 199 200 @Override 201 public void setEditorListener(EditorListener listener) { 202 mListener = listener; 203 } 204 205 @Override 206 public void setDeletable(boolean deletable) { 207 setupDeleteButton(deletable); 208 } 209 210 @Override 211 public void setEnabled(boolean enabled) { 212 super.setEnabled(enabled); 213 mLabel.setEnabled(!mReadOnly && enabled); 214 mDelete.setEnabled(!mReadOnly && enabled); 215 } 216 217 public Spinner getLabel() { 218 return mLabel; 219 } 220 221 public ImageView getDelete() { 222 return mDelete; 223 } 224 225 protected DataKind getKind() { 226 return mKind; 227 } 228 229 protected ValuesDelta getEntry() { 230 return mEntry; 231 } 232 233 protected EditType getType() { 234 return mType; 235 } 236 237 /** 238 * Build the current label state based on selected {@link EditType} and 239 * possible custom label string. 240 */ 241 private void rebuildLabel() { 242 mEditTypeAdapter = new EditTypeAdapter(mContext); 243 mLabel.setAdapter(mEditTypeAdapter); 244 if (mEditTypeAdapter.hasCustomSelection()) { 245 mLabel.setSelection(mEditTypeAdapter.getPosition(CUSTOM_SELECTION)); 246 } else { 247 mLabel.setSelection(mEditTypeAdapter.getPosition(mType)); 248 } 249 } 250 251 @Override 252 public void onFieldChanged(String column, String value) { 253 if (!isFieldChanged(column, value)) { 254 return; 255 } 256 257 // Field changes are saved directly 258 mEntry.put(column, value); 259 if (mListener != null) { 260 mListener.onRequest(EditorListener.FIELD_CHANGED); 261 } 262 263 boolean isEmpty = isEmpty(); 264 if (mWasEmpty != isEmpty) { 265 if (isEmpty) { 266 mListener.onRequest(EditorListener.FIELD_TURNED_EMPTY); 267 } else { 268 mListener.onRequest(EditorListener.FIELD_TURNED_NON_EMPTY); 269 } 270 mWasEmpty = isEmpty; 271 } 272 } 273 274 protected boolean isFieldChanged(String column, String value) { 275 final String dbValue = mEntry.getAsString(column); 276 // nullable fields (e.g. Middle Name) are usually represented as empty columns, 277 // so lets treat null and empty space equivalently here 278 final String dbValueNoNull = dbValue == null ? "" : dbValue; 279 final String valueNoNull = value == null ? "" : value; 280 return !TextUtils.equals(dbValueNoNull, valueNoNull); 281 } 282 283 protected void rebuildValues() { 284 setValues(mKind, mEntry, mState, mReadOnly, mViewIdGenerator); 285 } 286 287 /** 288 * Prepare this editor using the given {@link DataKind} for defining 289 * structure and {@link ValuesDelta} describing the content to edit. 290 */ 291 @Override 292 public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly, 293 ViewIdGenerator vig) { 294 mKind = kind; 295 mEntry = entry; 296 mState = state; 297 mReadOnly = readOnly; 298 mViewIdGenerator = vig; 299 setId(vig.getId(state, kind, entry, ViewIdGenerator.NO_VIEW_INDEX)); 300 301 if (!entry.isVisible()) { 302 // Hide ourselves entirely if deleted 303 setVisibility(View.GONE); 304 return; 305 } 306 setVisibility(View.VISIBLE); 307 308 // TODO: handle resources from remote packages 309 String titleString = (kind.titleRes == -1 || kind.titleRes == 0) 310 ? "" 311 : getResources().getString(kind.titleRes); 312 313 // Setup title (may not be shown if there is a Spinner setup later). 314 mTitle.setText(titleString.toUpperCase()); 315 316 // Display label selector if multiple types available 317 final boolean hasTypes = EntityModifier.hasEditTypes(kind); 318 setupLabelButton(hasTypes); 319 mLabel.setEnabled(!readOnly && isEnabled()); 320 if (hasTypes) { 321 mType = EntityModifier.getCurrentType(entry, kind); 322 rebuildLabel(); 323 } 324 } 325 326 public ValuesDelta getValues() { 327 return mEntry; 328 } 329 330 /** 331 * Prepare dialog for entering a custom label. The input value is trimmed: white spaces before 332 * and after the input text is removed. 333 * <p> 334 * If the final value is empty, this change request is ignored; 335 * no empty text is allowed in any custom label. 336 */ 337 private Dialog createCustomDialog() { 338 final EditText customType = new EditText(mContext); 339 customType.setId(R.id.custom_dialog_content); 340 customType.setInputType(INPUT_TYPE_CUSTOM); 341 customType.setSaveEnabled(true); 342 customType.requestFocus(); 343 344 final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 345 builder.setTitle(R.string.customLabelPickerTitle); 346 builder.setView(customType); 347 348 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 349 @Override 350 public void onClick(DialogInterface dialog, int which) { 351 final String customText = customType.getText().toString().trim(); 352 if (ContactsUtils.isGraphic(customText)) { 353 final List<EditType> allTypes = 354 EntityModifier.getValidTypes(mState, mKind, null); 355 mType = null; 356 for (EditType editType : allTypes) { 357 if (editType.customColumn != null) { 358 mType = editType; 359 break; 360 } 361 } 362 if (mType == null) return; 363 364 mEntry.put(mKind.typeColumn, mType.rawValue); 365 mEntry.put(mType.customColumn, customText); 366 rebuildLabel(); 367 requestFocusForFirstEditField(); 368 onLabelRebuilt(); 369 } 370 } 371 }); 372 373 builder.setNegativeButton(android.R.string.cancel, null); 374 375 return builder.create(); 376 } 377 378 /** 379 * Called after the label has changed (either chosen from the list or entered in the Dialog) 380 */ 381 protected void onLabelRebuilt() { 382 } 383 384 protected void onTypeSelectionChange(int position) { 385 EditType selected = mEditTypeAdapter.getItem(position); 386 // See if the selection has in fact changed 387 if (mEditTypeAdapter.hasCustomSelection() && selected == CUSTOM_SELECTION) { 388 return; 389 } 390 391 if (mType == selected && mType.customColumn == null) { 392 return; 393 } 394 395 if (selected.customColumn != null) { 396 showDialog(DIALOG_ID_CUSTOM); 397 } else { 398 // User picked type, and we're sure it's ok to actually write the entry. 399 mType = selected; 400 mEntry.put(mKind.typeColumn, mType.rawValue); 401 rebuildLabel(); 402 requestFocusForFirstEditField(); 403 onLabelRebuilt(); 404 } 405 } 406 407 /* package */ 408 void showDialog(int bundleDialogId) { 409 Bundle bundle = new Bundle(); 410 bundle.putInt(DIALOG_ID_KEY, bundleDialogId); 411 getDialogManager().showDialogInView(this, bundle); 412 } 413 414 private DialogManager getDialogManager() { 415 if (mDialogManager == null) { 416 Context context = getContext(); 417 if (!(context instanceof DialogManager.DialogShowingViewActivity)) { 418 throw new IllegalStateException( 419 "View must be hosted in an Activity that implements " + 420 "DialogManager.DialogShowingViewActivity"); 421 } 422 mDialogManager = ((DialogManager.DialogShowingViewActivity)context).getDialogManager(); 423 } 424 return mDialogManager; 425 } 426 427 @Override 428 public Dialog createDialog(Bundle bundle) { 429 if (bundle == null) throw new IllegalArgumentException("bundle must not be null"); 430 int dialogId = bundle.getInt(DIALOG_ID_KEY); 431 switch (dialogId) { 432 case DIALOG_ID_CUSTOM: 433 return createCustomDialog(); 434 default: 435 throw new IllegalArgumentException("Invalid dialogId: " + dialogId); 436 } 437 } 438 439 protected abstract void requestFocusForFirstEditField(); 440 441 private class EditTypeAdapter extends ArrayAdapter<EditType> { 442 private final LayoutInflater mInflater; 443 private boolean mHasCustomSelection; 444 private int mTextSize; 445 446 public EditTypeAdapter(Context context) { 447 super(context, 0); 448 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 449 mTextSize = getResources().getDimensionPixelSize( 450 R.dimen.editor_field_spinner_text_size); 451 452 if (mType != null && mType.customColumn != null) { 453 454 // Use custom label string when present 455 final String customText = mEntry.getAsString(mType.customColumn); 456 if (customText != null) { 457 add(CUSTOM_SELECTION); 458 mHasCustomSelection = true; 459 } 460 } 461 462 addAll(EntityModifier.getValidTypes(mState, mKind, mType)); 463 } 464 465 public boolean hasCustomSelection() { 466 return mHasCustomSelection; 467 } 468 469 @Override 470 public View getView(int position, View convertView, ViewGroup parent) { 471 return createViewFromResource( 472 position, convertView, parent, android.R.layout.simple_spinner_item); 473 } 474 475 @Override 476 public View getDropDownView(int position, View convertView, ViewGroup parent) { 477 return createViewFromResource( 478 position, convertView, parent, android.R.layout.simple_spinner_dropdown_item); 479 } 480 481 private View createViewFromResource(int position, View convertView, ViewGroup parent, 482 int resource) { 483 View view; 484 TextView textView; 485 486 if (convertView == null) { 487 view = mInflater.inflate(resource, parent, false); 488 } else { 489 view = convertView; 490 } 491 492 textView = (TextView) view; 493 textView.setTextSize(mTextSize); 494 495 EditType type = getItem(position); 496 String text; 497 if (type == CUSTOM_SELECTION) { 498 text = mEntry.getAsString(mType.customColumn); 499 } else { 500 text = getContext().getString(type.labelRes); 501 } 502 textView.setText(text.toUpperCase()); 503 return view; 504 } 505 } 506} 507