Home | History | Annotate | Download | only in editor
      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