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.ui.widget; 18 19 import com.android.contacts.ContactsUtils; 20 import com.android.contacts.R; 21 import com.android.contacts.model.Editor; 22 import com.android.contacts.model.EntityDelta; 23 import com.android.contacts.model.EntityModifier; 24 import com.android.contacts.model.ContactsSource.DataKind; 25 import com.android.contacts.model.ContactsSource.EditField; 26 import com.android.contacts.model.ContactsSource.EditType; 27 import com.android.contacts.model.EntityDelta.ValuesDelta; 28 import com.android.contacts.ui.ViewIdGenerator; 29 30 import android.app.AlertDialog; 31 import android.app.Dialog; 32 import android.content.Context; 33 import android.content.DialogInterface; 34 import android.content.Entity; 35 import android.os.Parcel; 36 import android.os.Parcelable; 37 import android.telephony.PhoneNumberFormattingTextWatcher; 38 import android.text.Editable; 39 import android.text.InputType; 40 import android.text.TextUtils; 41 import android.text.TextWatcher; 42 import android.util.AttributeSet; 43 import android.view.ContextThemeWrapper; 44 import android.view.LayoutInflater; 45 import android.view.View; 46 import android.view.ViewGroup; 47 import android.view.inputmethod.EditorInfo; 48 import android.widget.ArrayAdapter; 49 import android.widget.EditText; 50 import android.widget.ListAdapter; 51 import android.widget.RelativeLayout; 52 import android.widget.TextView; 53 54 import java.util.List; 55 56 /** 57 * Simple editor that handles labels and any {@link EditField} defined for 58 * the entry. Uses {@link ValuesDelta} to read any existing 59 * {@link Entity} values, and to correctly write any changes values. 60 */ 61 public class GenericEditorView extends RelativeLayout implements Editor, View.OnClickListener { 62 protected static final int RES_FIELD = R.layout.item_editor_field; 63 protected static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1; 64 65 protected LayoutInflater mInflater; 66 67 protected static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT 68 | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS; 69 70 protected TextView mLabel; 71 protected ViewGroup mFields; 72 protected View mDelete; 73 protected View mMore; 74 protected View mLess; 75 76 protected DataKind mKind; 77 protected ValuesDelta mEntry; 78 protected EntityDelta mState; 79 protected boolean mReadOnly; 80 81 protected boolean mHideOptional = true; 82 83 protected EditType mType; 84 // Used only when a user tries to use custom label. 85 private EditType mPendingType; 86 87 private ViewIdGenerator mViewIdGenerator; 88 GenericEditorView(Context context)89 public GenericEditorView(Context context) { 90 super(context); 91 } 92 GenericEditorView(Context context, AttributeSet attrs)93 public GenericEditorView(Context context, AttributeSet attrs) { 94 super(context, attrs); 95 } 96 97 /** {@inheritDoc} */ 98 @Override onFinishInflate()99 protected void onFinishInflate() { 100 mInflater = (LayoutInflater)getContext().getSystemService( 101 Context.LAYOUT_INFLATER_SERVICE); 102 103 mLabel = (TextView)findViewById(R.id.edit_label); 104 mLabel.setOnClickListener(this); 105 106 mFields = (ViewGroup)findViewById(R.id.edit_fields); 107 108 mDelete = findViewById(R.id.edit_delete); 109 mDelete.setOnClickListener(this); 110 111 mMore = findViewById(R.id.edit_more); 112 mMore.setOnClickListener(this); 113 114 mLess = findViewById(R.id.edit_less); 115 mLess.setOnClickListener(this); 116 } 117 118 protected EditorListener mListener; 119 setEditorListener(EditorListener listener)120 public void setEditorListener(EditorListener listener) { 121 mListener = listener; 122 } 123 setDeletable(boolean deletable)124 public void setDeletable(boolean deletable) { 125 mDelete.setVisibility(deletable ? View.VISIBLE : View.INVISIBLE); 126 } 127 128 @Override setEnabled(boolean enabled)129 public void setEnabled(boolean enabled) { 130 mLabel.setEnabled(enabled); 131 final int count = mFields.getChildCount(); 132 for (int pos = 0; pos < count; pos++) { 133 final View v = mFields.getChildAt(pos); 134 v.setEnabled(enabled); 135 } 136 mMore.setEnabled(enabled); 137 mLess.setEnabled(enabled); 138 } 139 140 /** 141 * Build the current label state based on selected {@link EditType} and 142 * possible custom label string. 143 */ rebuildLabel()144 private void rebuildLabel() { 145 // Handle undetected types 146 if (mType == null) { 147 mLabel.setText(R.string.unknown); 148 return; 149 } 150 151 if (mType.customColumn != null) { 152 // Use custom label string when present 153 final String customText = mEntry.getAsString(mType.customColumn); 154 if (customText != null) { 155 mLabel.setText(customText); 156 return; 157 } 158 } 159 160 // Otherwise fall back to using default label 161 mLabel.setText(mType.labelRes); 162 } 163 164 /** {@inheritDoc} */ onFieldChanged(String column, String value)165 public void onFieldChanged(String column, String value) { 166 // Field changes are saved directly 167 mEntry.put(column, value); 168 if (mListener != null) { 169 mListener.onRequest(EditorListener.FIELD_CHANGED); 170 } 171 } 172 isAnyFieldFilledOut()173 public boolean isAnyFieldFilledOut() { 174 int childCount = mFields.getChildCount(); 175 for (int i = 0; i < childCount; i++) { 176 EditText editorView = (EditText) mFields.getChildAt(i); 177 if (!TextUtils.isEmpty(editorView.getText())) { 178 return true; 179 } 180 } 181 return false; 182 } 183 rebuildValues()184 private void rebuildValues() { 185 setValues(mKind, mEntry, mState, mReadOnly, mViewIdGenerator); 186 } 187 188 /** 189 * Prepare this editor using the given {@link DataKind} for defining 190 * structure and {@link ValuesDelta} describing the content to edit. 191 */ setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly, ViewIdGenerator vig)192 public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly, 193 ViewIdGenerator vig) { 194 mKind = kind; 195 mEntry = entry; 196 mState = state; 197 mReadOnly = readOnly; 198 mViewIdGenerator = vig; 199 setId(vig.getId(state, kind, entry, ViewIdGenerator.NO_VIEW_INDEX)); 200 201 final boolean enabled = !readOnly; 202 203 if (!entry.isVisible()) { 204 // Hide ourselves entirely if deleted 205 setVisibility(View.GONE); 206 return; 207 } else { 208 setVisibility(View.VISIBLE); 209 } 210 211 // Display label selector if multiple types available 212 final boolean hasTypes = EntityModifier.hasEditTypes(kind); 213 mLabel.setVisibility(hasTypes ? View.VISIBLE : View.GONE); 214 mLabel.setEnabled(enabled); 215 if (hasTypes) { 216 mType = EntityModifier.getCurrentType(entry, kind); 217 rebuildLabel(); 218 } 219 220 // Build out set of fields 221 mFields.removeAllViews(); 222 boolean hidePossible = false; 223 int n = 0; 224 for (EditField field : kind.fieldList) { 225 // Inflate field from definition 226 EditText fieldView = (EditText)mInflater.inflate(RES_FIELD, mFields, false); 227 fieldView.setId(vig.getId(state, kind, entry, n++)); 228 if (field.titleRes > 0) { 229 fieldView.setHint(field.titleRes); 230 } 231 int inputType = field.inputType; 232 fieldView.setInputType(inputType); 233 if (inputType == InputType.TYPE_CLASS_PHONE) { 234 fieldView.addTextChangedListener(new PhoneNumberFormattingTextWatcher()); 235 } 236 fieldView.setMinLines(field.minLines); 237 238 // Read current value from state 239 final String column = field.column; 240 final String value = entry.getAsString(column); 241 fieldView.setText(value); 242 243 // Prepare listener for writing changes 244 fieldView.addTextChangedListener(new TextWatcher() { 245 public void afterTextChanged(Editable s) { 246 // Trigger event for newly changed value 247 onFieldChanged(column, s.toString()); 248 } 249 250 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 251 } 252 253 public void onTextChanged(CharSequence s, int start, int before, int count) { 254 } 255 }); 256 257 // Hide field when empty and optional value 258 final boolean couldHide = (!ContactsUtils.isGraphic(value) && field.optional); 259 final boolean willHide = (mHideOptional && couldHide); 260 fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE); 261 fieldView.setEnabled(enabled); 262 hidePossible = hidePossible || couldHide; 263 264 mFields.addView(fieldView); 265 } 266 267 // When hiding fields, place expandable 268 if (hidePossible) { 269 mMore.setVisibility(mHideOptional ? View.VISIBLE : View.GONE); 270 mLess.setVisibility(mHideOptional ? View.GONE : View.VISIBLE); 271 } else { 272 mMore.setVisibility(View.GONE); 273 mLess.setVisibility(View.GONE); 274 } 275 mMore.setEnabled(enabled); 276 mLess.setEnabled(enabled); 277 } 278 279 /** 280 * Prepare dialog for entering a custom label. The input value is trimmed: white spaces before 281 * and after the input text is removed. 282 * <p> 283 * If the final value is empty, this change request is ignored; 284 * no empty text is allowed in any custom label. 285 */ createCustomDialog()286 private Dialog createCustomDialog() { 287 final EditText customType = new EditText(mContext); 288 customType.setInputType(INPUT_TYPE_CUSTOM); 289 customType.requestFocus(); 290 291 final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 292 builder.setTitle(R.string.customLabelPickerTitle); 293 builder.setView(customType); 294 295 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 296 public void onClick(DialogInterface dialog, int which) { 297 final String customText = customType.getText().toString().trim(); 298 if (ContactsUtils.isGraphic(customText)) { 299 // Now we're sure it's ok to actually change the type value. 300 mType = mPendingType; 301 mPendingType = null; 302 mEntry.put(mKind.typeColumn, mType.rawValue); 303 mEntry.put(mType.customColumn, customText); 304 rebuildLabel(); 305 if (!mFields.hasFocus()) 306 mFields.requestFocus(); 307 } 308 } 309 }); 310 311 builder.setNegativeButton(android.R.string.cancel, null); 312 313 return builder.create(); 314 } 315 316 /** 317 * Prepare dialog for picking a new {@link EditType} or entering a 318 * custom label. This dialog is limited to the valid types as determined 319 * by {@link EntityModifier}. 320 */ createLabelDialog()321 public Dialog createLabelDialog() { 322 // Build list of valid types, including the current value 323 final List<EditType> validTypes = EntityModifier.getValidTypes(mState, mKind, mType); 324 325 // Wrap our context to inflate list items using correct theme 326 final Context dialogContext = new ContextThemeWrapper(mContext, 327 android.R.style.Theme_Light); 328 final LayoutInflater dialogInflater = mInflater.cloneInContext(dialogContext); 329 330 final ListAdapter typeAdapter = new ArrayAdapter<EditType>(mContext, RES_LABEL_ITEM, 331 validTypes) { 332 @Override 333 public View getView(int position, View convertView, ViewGroup parent) { 334 if (convertView == null) { 335 convertView = dialogInflater.inflate(RES_LABEL_ITEM, parent, false); 336 } 337 338 final EditType type = this.getItem(position); 339 final TextView textView = (TextView)convertView; 340 textView.setText(type.labelRes); 341 return textView; 342 } 343 }; 344 345 final DialogInterface.OnClickListener clickListener = 346 new DialogInterface.OnClickListener() { 347 public void onClick(DialogInterface dialog, int which) { 348 dialog.dismiss(); 349 350 final EditType selected = validTypes.get(which); 351 if (selected.customColumn != null) { 352 // Show custom label dialog if requested by type. 353 // 354 // Only when the custum value input in the next step is correct one. 355 // this method also set the type value to what the user requested here. 356 mPendingType = selected; 357 createCustomDialog().show(); 358 } else { 359 // User picked type, and we're sure it's ok to actually write the entry. 360 mType = selected; 361 mEntry.put(mKind.typeColumn, mType.rawValue); 362 rebuildLabel(); 363 if (!mFields.hasFocus()) 364 mFields.requestFocus(); 365 } 366 } 367 }; 368 369 final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 370 builder.setTitle(R.string.selectLabel); 371 builder.setSingleChoiceItems(typeAdapter, 0, clickListener); 372 return builder.create(); 373 } 374 375 /** {@inheritDoc} */ onClick(View v)376 public void onClick(View v) { 377 switch (v.getId()) { 378 case R.id.edit_label: { 379 createLabelDialog().show(); 380 break; 381 } 382 case R.id.edit_delete: { 383 // Keep around in model, but mark as deleted 384 mEntry.markDeleted(); 385 386 // Remove editor from parent view 387 final ViewGroup parent = (ViewGroup)getParent(); 388 parent.removeView(this); 389 390 if (mListener != null) { 391 // Notify listener when present 392 mListener.onDeleted(this); 393 } 394 break; 395 } 396 case R.id.edit_more: 397 case R.id.edit_less: { 398 mHideOptional = !mHideOptional; 399 rebuildValues(); 400 break; 401 } 402 } 403 } 404 405 private static class SavedState extends BaseSavedState { 406 public boolean mHideOptional; 407 public int[] mVisibilities; 408 SavedState(Parcelable superState)409 SavedState(Parcelable superState) { 410 super(superState); 411 } 412 SavedState(Parcel in)413 private SavedState(Parcel in) { 414 super(in); 415 mVisibilities = new int[in.readInt()]; 416 in.readIntArray(mVisibilities); 417 } 418 419 @Override writeToParcel(Parcel out, int flags)420 public void writeToParcel(Parcel out, int flags) { 421 super.writeToParcel(out, flags); 422 out.writeInt(mVisibilities.length); 423 out.writeIntArray(mVisibilities); 424 } 425 426 public static final Parcelable.Creator<SavedState> CREATOR 427 = new Parcelable.Creator<SavedState>() { 428 public SavedState createFromParcel(Parcel in) { 429 return new SavedState(in); 430 } 431 432 public SavedState[] newArray(int size) { 433 return new SavedState[size]; 434 } 435 }; 436 } 437 438 /** 439 * Saves the visibility of the child EditTexts, and mHideOptional. 440 */ 441 @Override onSaveInstanceState()442 protected Parcelable onSaveInstanceState() { 443 Parcelable superState = super.onSaveInstanceState(); 444 SavedState ss = new SavedState(superState); 445 446 ss.mHideOptional = mHideOptional; 447 448 final int numChildren = mFields.getChildCount(); 449 ss.mVisibilities = new int[numChildren]; 450 for (int i = 0; i < numChildren; i++) { 451 ss.mVisibilities[i] = mFields.getChildAt(i).getVisibility(); 452 } 453 454 return ss; 455 } 456 457 /** 458 * Restores the visibility of the child EditTexts, and mHideOptional. 459 */ 460 @Override onRestoreInstanceState(Parcelable state)461 protected void onRestoreInstanceState(Parcelable state) { 462 SavedState ss = (SavedState) state; 463 super.onRestoreInstanceState(ss.getSuperState()); 464 465 mHideOptional = ss.mHideOptional; 466 467 int numChildren = Math.min(mFields.getChildCount(), ss.mVisibilities.length); 468 for (int i = 0; i < numChildren; i++) { 469 mFields.getChildAt(i).setVisibility(ss.mVisibilities[i]); 470 } 471 } 472 } 473