/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * 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.silentcircle.contacts.editor; import android.content.Context; import android.os.Build; 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.silentcircle.contacts.R; import com.silentcircle.contacts.editor.Editor.EditorListener; import com.silentcircle.contacts.model.RawContactModifier; import com.silentcircle.contacts.model.RawContactDelta; import com.silentcircle.contacts.model.RawContactDelta.ValuesDelta; import com.silentcircle.contacts.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); private Context mContext; public KindSectionView(Context context) { this(context, null); } public KindSectionView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; } @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; try { view = mInflater.inflate(mKind.editorLayoutResourceId, mEditors, false); } catch (Exception e) { throw new RuntimeException( "Cannot allocate editor with layout resource ID " + mKind.editorLayoutResourceId + " 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 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { EditorAnimator.getInstance().showAddFieldFooter(mAddFieldFooter); } else { mAddFieldFooter.setVisibility(View.VISIBLE); } return; } } if (animate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 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; } }