blob: 261286e47308a4721a47666bca53900405f51cc8 [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts.editor;
import android.content.Context;
import android.provider.ContactsContract.Data;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.contacts.R;
import com.android.contacts.editor.Editor.EditorListener;
import com.android.contacts.common.model.RawContactModifier;
import com.android.contacts.common.model.RawContactDelta;
import com.android.contacts.common.model.ValuesDelta;
import com.android.contacts.common.model.dataitem.DataKind;
import java.util.ArrayList;
import java.util.List;
/**
* Custom view for an entire section of data as segmented by
* {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
* section header and a trigger for adding new {@link Data} rows.
*/
public class KindSectionView extends LinearLayout implements EditorListener {
private static final String TAG = "KindSectionView";
private TextView mTitle;
private ViewGroup mEditors;
private View mAddFieldFooter;
private String mTitleString;
private DataKind mKind;
private RawContactDelta mState;
private boolean mReadOnly;
private ViewIdGenerator mViewIdGenerator;
private LayoutInflater mInflater;
private final ArrayList<Runnable> mRunWhenWindowFocused = new ArrayList<Runnable>(1);
public KindSectionView(Context context) {
this(context, null);
}
public KindSectionView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (mEditors != null) {
int childCount = mEditors.getChildCount();
for (int i = 0; i < childCount; i++) {
mEditors.getChildAt(i).setEnabled(enabled);
}
}
if (enabled && !mReadOnly) {
mAddFieldFooter.setVisibility(View.VISIBLE);
} else {
mAddFieldFooter.setVisibility(View.GONE);
}
}
public boolean isReadOnly() {
return mReadOnly;
}
/** {@inheritDoc} */
@Override
protected void onFinishInflate() {
setDrawingCacheEnabled(true);
setAlwaysDrawnWithCacheEnabled(true);
mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mTitle = (TextView) findViewById(R.id.kind_title);
mEditors = (ViewGroup) findViewById(R.id.kind_editors);
mAddFieldFooter = findViewById(R.id.add_field_footer);
mAddFieldFooter.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// Setup click listener to add an empty field when the footer is clicked.
mAddFieldFooter.setVisibility(View.GONE);
addItem();
}
});
}
@Override
public void onDeleteRequested(Editor editor) {
// If there is only 1 editor in the section, then don't allow the user to delete it.
// Just clear the fields in the editor.
if (getEditorCount() == 1) {
editor.clearAllFields();
} else {
// Otherwise it's okay to delete this {@link Editor}
editor.deleteEditor();
}
}
@Override
public void onRequest(int request) {
// If a field has become empty or non-empty, then check if another row
// can be added dynamically.
if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
updateAddFooterVisible(true);
}
}
public void setState(DataKind kind, RawContactDelta state, boolean readOnly, ViewIdGenerator vig) {
mKind = kind;
mState = state;
mReadOnly = readOnly;
mViewIdGenerator = vig;
setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX));
// TODO: handle resources from remote packages
mTitleString = (kind.titleRes == -1 || kind.titleRes == 0)
? ""
: getResources().getString(kind.titleRes);
mTitle.setText(mTitleString);
rebuildFromState();
updateAddFooterVisible(false);
updateSectionVisible();
}
public String getTitle() {
return mTitleString;
}
public void setTitleVisible(boolean visible) {
findViewById(R.id.kind_title_layout).setVisibility(visible ? View.VISIBLE : View.GONE);
}
/**
* Build editors for all current {@link #mState} rows.
*/
public void rebuildFromState() {
// Remove any existing editors
mEditors.removeAllViews();
// Check if we are displaying anything here
boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
if (hasEntries) {
for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
// Skip entries that aren't visible
if (!entry.isVisible()) continue;
if (isEmptyNoop(entry)) continue;
createEditorView(entry);
}
}
}
/**
* Creates an EditorView for the given entry. This function must be used while constructing
* the views corresponding to the the object-model. The resulting EditorView is also added
* to the end of mEditors
*/
private View createEditorView(ValuesDelta entry) {
final View view;
final int layoutResId = EditorUiUtils.getLayoutResourceId(mKind.mimeType);
try {
view = mInflater.inflate(layoutResId, mEditors, false);
} catch (Exception e) {
throw new RuntimeException(
"Cannot allocate editor with layout resource ID " +
layoutResId + " for MIME type " + mKind.mimeType +
" with error " + e.toString());
}
view.setEnabled(isEnabled());
if (view instanceof Editor) {
Editor editor = (Editor) view;
editor.setDeletable(true);
editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
editor.setEditorListener(this);
}
mEditors.addView(view);
return view;
}
/**
* Tests whether the given item has no changes (so it exists in the database) but is empty
*/
private boolean isEmptyNoop(ValuesDelta item) {
if (!item.isNoop()) return false;
final int fieldCount = mKind.fieldList.size();
for (int i = 0; i < fieldCount; i++) {
final String column = mKind.fieldList.get(i).column;
final String value = item.getAsString(column);
if (!TextUtils.isEmpty(value)) return false;
}
return true;
}
private void updateSectionVisible() {
setVisibility(getEditorCount() != 0 ? VISIBLE : GONE);
}
protected void updateAddFooterVisible(boolean animate) {
if (!mReadOnly && (mKind.typeOverallMax != 1)) {
// First determine whether there are any existing empty editors.
updateEmptyEditors();
// If there are no existing empty editors and it's possible to add
// another field, then make the "add footer" field visible.
if (!hasEmptyEditor() && RawContactModifier.canInsert(mState, mKind)) {
if (animate) {
EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter);
} else {
mAddFieldFooter.setVisibility(View.VISIBLE);
}
return;
}
}
if (animate) {
EditorAnimator.getInstance().hideAddFieldFooter(mAddFieldFooter);
} else {
mAddFieldFooter.setVisibility(View.GONE);
}
}
/**
* Updates the editors being displayed to the user removing extra empty
* {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
*/
private void updateEmptyEditors() {
List<View> emptyEditors = getEmptyEditors();
// If there is more than 1 empty editor, then remove it from the list of editors.
if (emptyEditors.size() > 1) {
for (View emptyEditorView : emptyEditors) {
// If no child {@link View}s are being focused on within
// this {@link View}, then remove this empty editor.
if (emptyEditorView.findFocus() == null) {
mEditors.removeView(emptyEditorView);
}
}
}
}
/**
* Returns a list of empty editor views in this section.
*/
private List<View> getEmptyEditors() {
List<View> emptyEditorViews = new ArrayList<View>();
for (int i = 0; i < mEditors.getChildCount(); i++) {
View view = mEditors.getChildAt(i);
if (((Editor) view).isEmpty()) {
emptyEditorViews.add(view);
}
}
return emptyEditorViews;
}
/**
* Returns true if one of the editors has all of its fields empty, or false
* otherwise.
*/
private boolean hasEmptyEditor() {
return getEmptyEditors().size() > 0;
}
/**
* Returns true if all editors are empty.
*/
public boolean isEmpty() {
for (int i = 0; i < mEditors.getChildCount(); i++) {
View view = mEditors.getChildAt(i);
if (!((Editor) view).isEmpty()) {
return false;
}
}
return true;
}
/**
* Extends superclass implementation to also run tasks
* enqueued by {@link #runWhenWindowFocused}.
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (hasWindowFocus) {
for (Runnable r: mRunWhenWindowFocused) {
r.run();
}
mRunWhenWindowFocused.clear();
}
}
/**
* Depending on whether we are in the currently-focused window, either run
* the argument immediately, or stash it until our window becomes focused.
*/
private void runWhenWindowFocused(Runnable r) {
if (hasWindowFocus()) {
r.run();
} else {
mRunWhenWindowFocused.add(r);
}
}
/**
* Simple wrapper around {@link #runWhenWindowFocused}
* to ensure that it runs in the UI thread.
*/
private void postWhenWindowFocused(final Runnable r) {
post(new Runnable() {
@Override
public void run() {
runWhenWindowFocused(r);
}
});
}
public void addItem() {
ValuesDelta values = null;
// If this is a list, we can freely add. If not, only allow adding the first.
if (mKind.typeOverallMax == 1) {
if (getEditorCount() == 1) {
return;
}
// If we already have an item, just make it visible
ArrayList<ValuesDelta> entries = mState.getMimeEntries(mKind.mimeType);
if (entries != null && entries.size() > 0) {
values = entries.get(0);
}
}
// Insert a new child, create its view and set its focus
if (values == null) {
values = RawContactModifier.insertChild(mState, mKind);
}
final View newField = createEditorView(values);
if (newField instanceof Editor) {
postWhenWindowFocused(new Runnable() {
@Override
public void run() {
newField.requestFocus();
((Editor)newField).editNewlyAddedField();
}
});
}
// Hide the "add field" footer because there is now a blank field.
mAddFieldFooter.setVisibility(View.GONE);
// Ensure we are visible
updateSectionVisible();
}
public int getEditorCount() {
return mEditors.getChildCount();
}
public DataKind getKind() {
return mKind;
}
}