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 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.provider.ContactsContract.Data;
     28 import android.text.TextUtils;
     29 import android.util.AttributeSet;
     30 import android.view.LayoutInflater;
     31 import android.view.View;
     32 import android.view.ViewGroup;
     33 import android.widget.LinearLayout;
     34 import android.widget.TextView;
     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 EntityDelta 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         final boolean animate;
    117         if (getEditorCount() == 1) {
    118             editor.clearAllFields();
    119             animate = true;
    120         } else {
    121             // Otherwise it's okay to delete this {@link Editor}
    122             editor.deleteEditor();
    123 
    124             // This is already animated, don't do anything further here
    125             animate = false;
    126         }
    127         updateAddFooterVisible(animate);
    128     }
    129 
    130     @Override
    131     public void onRequest(int request) {
    132         // If a field has become empty or non-empty, then check if another row
    133         // can be added dynamically.
    134         if (request == FIELD_TURNED_EMPTY || request == FIELD_TURNED_NON_EMPTY) {
    135             updateAddFooterVisible(true);
    136         }
    137     }
    138 
    139     public void setState(DataKind kind, EntityDelta state, boolean readOnly, ViewIdGenerator vig) {
    140         mKind = kind;
    141         mState = state;
    142         mReadOnly = readOnly;
    143         mViewIdGenerator = vig;
    144 
    145         setId(mViewIdGenerator.getId(state, kind, null, ViewIdGenerator.NO_VIEW_INDEX));
    146 
    147         // TODO: handle resources from remote packages
    148         mTitleString = (kind.titleRes == -1 || kind.titleRes == 0)
    149                 ? ""
    150                 : getResources().getString(kind.titleRes);
    151         mTitle.setText(mTitleString);
    152 
    153         rebuildFromState();
    154         updateAddFooterVisible(false);
    155         updateSectionVisible();
    156     }
    157 
    158     public String getTitle() {
    159         return mTitleString;
    160     }
    161 
    162     public void setTitleVisible(boolean visible) {
    163         findViewById(R.id.kind_title_layout).setVisibility(visible ? View.VISIBLE : View.GONE);
    164     }
    165 
    166     /**
    167      * Build editors for all current {@link #mState} rows.
    168      */
    169     public void rebuildFromState() {
    170         // Remove any existing editors
    171         mEditors.removeAllViews();
    172 
    173         // Check if we are displaying anything here
    174         boolean hasEntries = mState.hasMimeEntries(mKind.mimeType);
    175 
    176         if (hasEntries) {
    177             for (ValuesDelta entry : mState.getMimeEntries(mKind.mimeType)) {
    178                 // Skip entries that aren't visible
    179                 if (!entry.isVisible()) continue;
    180                 if (isEmptyNoop(entry)) continue;
    181 
    182                 createEditorView(entry);
    183             }
    184         }
    185     }
    186 
    187 
    188     /**
    189      * Creates an EditorView for the given entry. This function must be used while constructing
    190      * the views corresponding to the the object-model. The resulting EditorView is also added
    191      * to the end of mEditors
    192      */
    193     private View createEditorView(ValuesDelta entry) {
    194         final View view;
    195         try {
    196             view = mInflater.inflate(mKind.editorLayoutResourceId, mEditors, false);
    197         } catch (Exception e) {
    198             throw new RuntimeException(
    199                     "Cannot allocate editor with layout resource ID " +
    200                     mKind.editorLayoutResourceId + " for MIME type " + mKind.mimeType +
    201                     " with error " + e.toString());
    202         }
    203 
    204         view.setEnabled(isEnabled());
    205 
    206         if (view instanceof Editor) {
    207             Editor editor = (Editor) view;
    208             editor.setDeletable(true);
    209             editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
    210             editor.setEditorListener(this);
    211         }
    212         mEditors.addView(view);
    213         return view;
    214     }
    215 
    216     /**
    217      * Tests whether the given item has no changes (so it exists in the database) but is empty
    218      */
    219     private boolean isEmptyNoop(ValuesDelta item) {
    220         if (!item.isNoop()) return false;
    221         final int fieldCount = mKind.fieldList.size();
    222         for (int i = 0; i < fieldCount; i++) {
    223             final String column = mKind.fieldList.get(i).column;
    224             final String value = item.getAsString(column);
    225             if (!TextUtils.isEmpty(value)) return false;
    226         }
    227         return true;
    228     }
    229 
    230     private void updateSectionVisible() {
    231         setVisibility(getEditorCount() != 0 ? VISIBLE : GONE);
    232     }
    233 
    234     protected void updateAddFooterVisible(boolean animate) {
    235         if (!mReadOnly && (mKind.typeOverallMax != 1)) {
    236             // First determine whether there are any existing empty editors.
    237             updateEmptyEditors();
    238             // If there are no existing empty editors and it's possible to add
    239             // another field, then make the "add footer" field visible.
    240             if (!hasEmptyEditor() && EntityModifier.canInsert(mState, mKind)) {
    241                 if (animate) {
    242                     EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter);
    243                 } else {
    244                     mAddFieldFooter.setVisibility(View.VISIBLE);
    245                 }
    246                 return;
    247             }
    248         }
    249         if (animate) {
    250             EditorAnimator.getInstance().hideAddFieldFooter(mAddFieldFooter);
    251         } else {
    252             mAddFieldFooter.setVisibility(View.GONE);
    253         }
    254     }
    255 
    256     /**
    257      * Updates the editors being displayed to the user removing extra empty
    258      * {@link Editor}s, so there is only max 1 empty {@link Editor} view at a time.
    259      */
    260     private void updateEmptyEditors() {
    261         List<View> emptyEditors = getEmptyEditors();
    262 
    263         // If there is more than 1 empty editor, then remove it from the list of editors.
    264         if (emptyEditors.size() > 1) {
    265             for (View emptyEditorView : emptyEditors) {
    266                 // If no child {@link View}s are being focused on within
    267                 // this {@link View}, then remove this empty editor.
    268                 if (emptyEditorView.findFocus() == null) {
    269                     mEditors.removeView(emptyEditorView);
    270                 }
    271             }
    272         }
    273     }
    274 
    275     /**
    276      * Returns a list of empty editor views in this section.
    277      */
    278     private List<View> getEmptyEditors() {
    279         List<View> emptyEditorViews = new ArrayList<View>();
    280         for (int i = 0; i < mEditors.getChildCount(); i++) {
    281             View view = mEditors.getChildAt(i);
    282             if (((Editor) view).isEmpty()) {
    283                 emptyEditorViews.add(view);
    284             }
    285         }
    286         return emptyEditorViews;
    287     }
    288 
    289     /**
    290      * Returns true if one of the editors has all of its fields empty, or false
    291      * otherwise.
    292      */
    293     private boolean hasEmptyEditor() {
    294         return getEmptyEditors().size() > 0;
    295     }
    296 
    297     /**
    298      * Returns true if all editors are empty.
    299      */
    300     public boolean isEmpty() {
    301         for (int i = 0; i < mEditors.getChildCount(); i++) {
    302             View view = mEditors.getChildAt(i);
    303             if (!((Editor) view).isEmpty()) {
    304                 return false;
    305             }
    306         }
    307         return true;
    308     }
    309 
    310     /**
    311      * Extends superclass implementation to also run tasks
    312      * enqueued by {@link #runWhenWindowFocused}.
    313      */
    314     @Override
    315     public void onWindowFocusChanged(boolean hasWindowFocus) {
    316         super.onWindowFocusChanged(hasWindowFocus);
    317         if (hasWindowFocus) {
    318             for (Runnable r: mRunWhenWindowFocused) {
    319                 r.run();
    320             }
    321             mRunWhenWindowFocused.clear();
    322         }
    323     }
    324 
    325     /**
    326      * Depending on whether we are in the currently-focused window, either run
    327      * the argument immediately, or stash it until our window becomes focused.
    328      */
    329     private void runWhenWindowFocused(Runnable r) {
    330         if (hasWindowFocus()) {
    331             r.run();
    332         } else {
    333             mRunWhenWindowFocused.add(r);
    334         }
    335     }
    336 
    337     /**
    338      * Simple wrapper around {@link #runWhenWindowFocused}
    339      * to ensure that it runs in the UI thread.
    340      */
    341     private void postWhenWindowFocused(final Runnable r) {
    342         post(new Runnable() {
    343             @Override
    344             public void run() {
    345                 runWhenWindowFocused(r);
    346             }
    347         });
    348     }
    349 
    350     public void addItem() {
    351         ValuesDelta values = null;
    352         // If this is a list, we can freely add. If not, only allow adding the first.
    353         if (mKind.typeOverallMax == 1) {
    354             if (getEditorCount() == 1) {
    355                 return;
    356             }
    357 
    358             // If we already have an item, just make it visible
    359             ArrayList<ValuesDelta> entries = mState.getMimeEntries(mKind.mimeType);
    360             if (entries != null && entries.size() > 0) {
    361                 values = entries.get(0);
    362             }
    363         }
    364 
    365         // Insert a new child, create its view and set its focus
    366         if (values == null) {
    367             values = EntityModifier.insertChild(mState, mKind);
    368         }
    369 
    370         final View newField = createEditorView(values);
    371         if (newField instanceof Editor) {
    372             postWhenWindowFocused(new Runnable() {
    373                 @Override
    374                 public void run() {
    375                     newField.requestFocus();
    376                     ((Editor)newField).editNewlyAddedField();
    377                 }
    378             });
    379         }
    380 
    381         // Hide the "add field" footer because there is now a blank field.
    382         mAddFieldFooter.setVisibility(View.GONE);
    383 
    384         // Ensure we are visible
    385         updateSectionVisible();
    386     }
    387 
    388     public int getEditorCount() {
    389         return mEditors.getChildCount();
    390     }
    391 
    392     public DataKind getKind() {
    393         return mKind;
    394     }
    395 }
    396