com.ultramegasoft.flavordex2.fragment.EditCatFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.ultramegasoft.flavordex2.fragment.EditCatFragment.java

Source

/*
 * The MIT License (MIT)
 * Copyright  2016 Steve Guidetti
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the Software?), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED AS IS?, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.ultramegasoft.flavordex2.fragment;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Paint;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TableLayout;

import com.ultramegasoft.flavordex2.FlavordexApp;
import com.ultramegasoft.flavordex2.R;
import com.ultramegasoft.flavordex2.dialog.CatDeleteDialog;
import com.ultramegasoft.flavordex2.provider.Tables;
import com.ultramegasoft.flavordex2.util.EntryUtils;
import com.ultramegasoft.radarchart.RadarHolder;
import com.ultramegasoft.radarchart.RadarView;

import java.lang.ref.WeakReference;
import java.util.ArrayList;

/**
 * Fragment for editing or creating an entry category.
 *
 * @author Steve Guidetti
 */
public class EditCatFragment extends LoadingProgressFragment
        implements LoaderManager.LoaderCallbacks<EditCatFragment.DataLoader.Holder> {
    /**
     * Keys for the Fragment arguments
     */
    public static final String ARG_CAT_ID = "cat_id";
    public static final String ARG_CAT_NAME = "cat_name";

    /**
     * Request codes for external Activities
     */
    private static final int REQUEST_DELETE_CAT = 200;

    /**
     * Keys for the saved state
     */
    private static final String STATE_EXTRA_FIELDS = "extra_fields";
    private static final String STATE_FLAVOR_FIELDS = "flavor_fields";

    /**
     * Views from the layout
     */
    private EditText mTxtTitle;
    private TableLayout mTableExtras;
    private TableLayout mTableFlavors;
    private RadarView mRadarView;

    /**
     * The status of the extra fields
     */
    @NonNull
    private ArrayList<Field> mExtraFields = new ArrayList<>();

    /**
     * The status of the flavors
     */
    @NonNull
    private ArrayList<Field> mFlavorFields = new ArrayList<>();

    /**
     * The category ID from the arguments
     */
    private long mCatId;

    /**
     * True while data is loading
     */
    private boolean mIsLoading;

    /**
     * Interface for field listeners.
     */
    private interface CatFieldListener {
        /**
         * Should the undo delete function be allowed?
         *
         * @return Whether undo is allowed
         */
        @SuppressWarnings("BooleanMethodIsAlwaysInverted")
        boolean allowUndo();

        /**
         * Called when the field is deleted.
         */
        void onDelete();

        /**
         * Called when the field is undeleted.
         */
        void onUndoDelete();

        /**
         * Called when the name of the field is changed.
         *
         * @param name The new name of the field
         */
        void onNameChange(String name);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final Activity activity = getActivity();
        if (activity == null) {
            return;
        }

        final Bundle args = getArguments();
        mCatId = args != null ? args.getLong(ARG_CAT_ID) : 0;
        setHasOptionsMenu(true);
        if (mCatId > 0) {
            activity.setTitle(getString(R.string.title_edit_cat));
        }
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (savedInstanceState == null) {
            if (mCatId > 0) {
                getLoaderManager().initLoader(0, null, this).forceLoad();
            } else {
                createFlavor();
                hideLoadingIndicator(false);
                if (mTxtTitle != null) {
                    mTxtTitle.requestFocus();
                }
            }
        } else {
            final ArrayList<Field> extraFields = savedInstanceState.getParcelableArrayList(STATE_EXTRA_FIELDS);
            if (extraFields != null) {
                mExtraFields = extraFields;
            }
            final ArrayList<Field> flavorFields = savedInstanceState.getParcelableArrayList(STATE_FLAVOR_FIELDS);
            if (flavorFields != null) {
                mFlavorFields = flavorFields;
            }
            populateFields();
            hideLoadingIndicator(false);
        }
    }

    @NonNull
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View root = super.onCreateView(inflater, container, savedInstanceState);

        mTxtTitle = root.findViewById(R.id.cat_name);

        mTableExtras = root.findViewById(R.id.cat_extras);
        mTableFlavors = root.findViewById(R.id.cat_flavor);
        mRadarView = root.findViewById(R.id.radar);

        root.findViewById(R.id.button_add_extra).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                createExtra();
            }
        });

        root.findViewById(R.id.button_add_flavor).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                createFlavor();
            }
        });

        return root;
    }

    @Override
    protected int getLayoutId() {
        final Bundle args = getArguments();
        final String cat = args != null ? args.getString(ARG_CAT_NAME) : null;

        if (FlavordexApp.CAT_BEER.equals(cat)) {
            return R.layout.fragment_edit_cat_beer;
        }
        if (FlavordexApp.CAT_WINE.equals(cat)) {
            return R.layout.fragment_edit_cat_wine;
        }
        if (FlavordexApp.CAT_WHISKEY.equals(cat)) {
            return R.layout.fragment_edit_cat_whiskey;
        }
        if (FlavordexApp.CAT_COFFEE.equals(cat)) {
            return R.layout.fragment_edit_cat_coffee;
        }

        return R.layout.fragment_edit_cat;
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelableArrayList(STATE_EXTRA_FIELDS, mExtraFields);
        outState.putParcelableArrayList(STATE_FLAVOR_FIELDS, mFlavorFields);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.cat_edit_menu, menu);
        menu.findItem(R.id.menu_delete).setVisible(mCatId > 0);
    }

    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);
        menu.findItem(R.id.menu_save).setEnabled(!mIsLoading);
    }

    @SuppressWarnings("SameReturnValue")
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_save:
            saveData();
            return true;
        case R.id.menu_delete:
            confirmDeleteCat();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (resultCode == Activity.RESULT_OK) {
            switch (requestCode) {
            case REQUEST_DELETE_CAT:
                final Activity activity = getActivity();
                if (activity != null) {
                    activity.finish();
                }
                break;
            }
        }
    }

    /**
     * Add all the fields to the layout.
     */
    private void populateFields() {
        for (Field field : mExtraFields) {
            addExtraField(field);
        }
        for (Field field : mFlavorFields) {
            addFlavorField(field);
        }
    }

    /**
     * Create a new extra field.
     */
    private void createExtra() {
        final int count = mExtraFields.size();
        if (count > 0 && TextUtils.isEmpty(mExtraFields.get(count - 1).name)) {
            return;
        }

        final Field field = new Field(0, null);
        mExtraFields.add(field);
        addExtraField(field);
    }

    /**
     * Add the Views associated with an extra field to the layout.
     *
     * @param field The field
     */
    private void addExtraField(@NonNull final Field field) {
        final CatFieldListener listener = new CatFieldListener() {
            @Override
            public boolean allowUndo() {
                return !field.isEmpty();
            }

            @Override
            public void onDelete() {
                if (field.isEmpty()) {
                    mExtraFields.remove(field);
                } else {
                    field.delete = true;
                }
            }

            @Override
            public void onUndoDelete() {
                field.delete = false;
            }

            @Override
            public void onNameChange(String name) {
                field.name = name;
            }
        };

        addTableRow(mTableExtras, field.name, 20, R.string.hint_extra_name, R.string.button_remove_extra,
                field.delete, listener);
    }

    /**
     * Create a new flavor.
     */
    private void createFlavor() {
        final int count = mFlavorFields.size();
        if (count > 0 && TextUtils.isEmpty(mFlavorFields.get(count - 1).name)) {
            return;
        }

        final Field field = new Field(0, null);
        mFlavorFields.add(field);
        addFlavorField(field);
    }

    /**
     * Add the Views associated with a flavor to the layout.
     *
     * @param field The field
     */
    private void addFlavorField(@NonNull final Field field) {
        final CatFieldListener listener = new CatFieldListener() {
            @Override
            public boolean allowUndo() {
                return !field.isEmpty();
            }

            @Override
            public void onDelete() {
                if (field.isEmpty()) {
                    mFlavorFields.remove(field);
                } else {
                    field.delete = true;
                    mRadarView.setData(getRadarData());
                }
            }

            @Override
            public void onUndoDelete() {
                field.delete = false;
                mRadarView.setData(getRadarData());
            }

            @Override
            public void onNameChange(String name) {
                field.name = name;
                mRadarView.setData(getRadarData());
            }
        };

        addTableRow(mTableFlavors, field.name, 12, R.string.hint_flavor_name, R.string.button_remove_flavor,
                field.delete, listener);
    }

    /**
     * Add a row to the provided TableLayout. The row contains an EditText for the field name with a
     * delete button. The delete button becomes an undo button if the provided listener allows undo.
     * If undo is not allowed, the delete button will remove the row.
     *
     * @param tableLayout The TableLayout to add a row to
     * @param text        The text to fill the text field
     * @param maxLength   The maximum allowed length of the text field
     * @param hint        The hint for the EditText
     * @param deleteHint  The contentDescription for the delete Button
     * @param deleted     The initial deleted status of the field
     * @param listener    The event listener for the field
     */
    private void addTableRow(@NonNull final TableLayout tableLayout, @Nullable String text, int maxLength, int hint,
            final int deleteHint, final boolean deleted, @NonNull final CatFieldListener listener) {
        final LayoutInflater inflater = LayoutInflater.from(getContext());
        final View root = inflater.inflate(R.layout.edit_cat_field, tableLayout, false);

        final InputFilter[] filters = new InputFilter[] { new InputFilter.LengthFilter(maxLength) };
        final EditText editText = root.findViewById(R.id.field_name);
        editText.setFilters(filters);
        editText.setHint(hint);
        editText.setText(text);
        editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            @Override
            public void afterTextChanged(Editable s) {
                listener.onNameChange(s.toString());
            }
        });

        final ImageButton deleteButton = root.findViewById(R.id.button_delete);
        deleteButton.setContentDescription(getString(deleteHint));
        deleteButton.setOnClickListener(new View.OnClickListener() {
            private boolean mDeleted = deleted;

            @Override
            public void onClick(View v) {
                if (mDeleted) {
                    setDeleted(false);
                    listener.onUndoDelete();
                } else {
                    if (!listener.allowUndo()) {
                        tableLayout.removeView(root);
                    } else {
                        setDeleted(true);
                    }
                    listener.onDelete();
                }
            }

            private void setDeleted(boolean deleted) {
                mDeleted = deleted;
                editText.setEnabled(!deleted);
                if (deleted) {
                    deleteButton.setImageResource(R.drawable.ic_undo);
                    deleteButton.setContentDescription(getString(R.string.button_undo));
                    editText.setPaintFlags(editText.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
                } else {
                    deleteButton.setImageResource(R.drawable.ic_clear);
                    deleteButton.setContentDescription(getString(deleteHint));
                    editText.setPaintFlags(editText.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
                }
            }
        });

        if (deleted) {
            editText.setEnabled(false);
            deleteButton.setImageResource(R.drawable.ic_undo);
            deleteButton.setContentDescription(getString(R.string.button_undo));
            editText.setPaintFlags(editText.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
        }

        tableLayout.addView(root);
        if (TextUtils.isEmpty(text)) {
            editText.requestFocus();
        }
    }

    /**
     * Get the active flavor data for the RadarView.
     *
     * @return List of RadarHolders for the RadarView
     */
    @NonNull
    private ArrayList<RadarHolder> getRadarData() {
        final ArrayList<RadarHolder> data = new ArrayList<>();
        for (Field field : mFlavorFields) {
            if (field.isEmpty() || field.delete) {
                continue;
            }
            assert field.name != null;
            data.add(new RadarHolder(field.name, 0));
        }
        return data;
    }

    /**
     * Check if the form is properly filled out.
     *
     * @return Whether all the form fields are valid
     */
    private boolean validateForm() {
        if (mTxtTitle != null && TextUtils.isEmpty(mTxtTitle.getText().toString())) {
            mTxtTitle.setError(getString(R.string.error_required));
            mTxtTitle.requestFocus();
            return false;
        }
        return !mIsLoading;
    }

    /**
     * Save the category data and close the Activity.
     */
    private void saveData() {
        final Activity activity = getActivity();
        if (activity == null || !validateForm()) {
            return;
        }

        final ContentValues info = new ContentValues();
        if (mTxtTitle != null) {
            final String title = EntryUtils.filterName(mTxtTitle.getText().toString());
            info.put(Tables.Cats.NAME, title);
        }

        new DataSaver(activity, info, mExtraFields, mFlavorFields, mCatId).execute();

        activity.finish();
    }

    /**
     * Open the delete confirmation dialog.
     */
    private void confirmDeleteCat() {
        final FragmentManager fm = getFragmentManager();
        if (fm != null && mCatId > 0) {
            CatDeleteDialog.showDialog(fm, this, REQUEST_DELETE_CAT, mCatId);
        }
    }

    @SuppressWarnings("ConstantConditions")
    @NonNull
    @Override
    public Loader<DataLoader.Holder> onCreateLoader(int id, Bundle args) {
        final Activity activity = getActivity();
        if (activity == null) {
            return null;
        }

        mIsLoading = true;
        activity.invalidateOptionsMenu();
        return new DataLoader(activity, mCatId);
    }

    @Override
    public void onLoadFinished(@NonNull Loader<DataLoader.Holder> loader, DataLoader.Holder data) {
        mIsLoading = false;
        getLoaderManager().destroyLoader(0);
        hideLoadingIndicator(true);

        final Activity activity = getActivity();
        if (mTxtTitle != null) {
            mTxtTitle.setText(FlavordexApp.getRealCatName(activity, data.catName));
            mTxtTitle.setSelection(mTxtTitle.length());
        }
        mExtraFields = data.extras;
        mFlavorFields = data.flavors;
        populateFields();
        mRadarView.setData(getRadarData());

        if (activity != null) {
            activity.invalidateOptionsMenu();
        }
    }

    @SuppressWarnings("EmptyMethod")
    @Override
    public void onLoaderReset(@NonNull Loader<DataLoader.Holder> loader) {
    }

    /**
     * Custom Loader to load everything in one task
     */
    public static class DataLoader extends AsyncTaskLoader<DataLoader.Holder> {
        /**
         * The ContentResolver to use
         */
        @NonNull
        private final ContentResolver mResolver;

        /**
         * The category ID
         */
        private final long mCatId;

        /**
         * @param context The Context
         * @param catId   The category ID
         */
        DataLoader(@NonNull Context context, long catId) {
            super(context);
            mResolver = context.getContentResolver();
            mCatId = catId;
        }

        @Override
        public Holder loadInBackground() {
            final Holder holder = new Holder();
            final Uri uri = ContentUris.withAppendedId(Tables.Cats.CONTENT_ID_URI_BASE, mCatId);
            loadCat(holder, uri);
            loadExtras(holder, uri);
            loadFlavors(holder, uri);
            return holder;
        }

        /**
         * Load data from the categories table.
         *
         * @param holder The Holder
         * @param catUri The Uri for the category
         */
        private void loadCat(@NonNull Holder holder, @NonNull Uri catUri) {
            final Cursor cursor = mResolver.query(catUri, null, null, null, null);
            if (cursor != null) {
                try {
                    if (cursor.moveToFirst()) {
                        holder.catName = cursor.getString(cursor.getColumnIndex(Tables.Cats.NAME));
                    }
                } finally {
                    cursor.close();
                }
            }
        }

        /**
         * Load the custom extra fields for the category.
         *
         * @param holder The Holder
         * @param catUri The Uri for the category
         */
        private void loadExtras(@NonNull Holder holder, @NonNull Uri catUri) {
            final Uri uri = Uri.withAppendedPath(catUri, "extras");
            final String where = Tables.Extras.PRESET + " = 0";
            final String sort = Tables.Extras.POS + " ASC";
            final Cursor cursor = mResolver.query(uri, null, where, null, sort);
            if (cursor != null) {
                try {
                    long id;
                    String name;
                    boolean deleted;
                    while (cursor.moveToNext()) {
                        id = cursor.getLong(cursor.getColumnIndex(Tables.Extras._ID));
                        name = cursor.getString(cursor.getColumnIndex(Tables.Extras.NAME));
                        deleted = cursor.getInt(cursor.getColumnIndex(Tables.Extras.DELETED)) == 1;
                        holder.extras.add(new Field(id, name, deleted));
                    }
                } finally {
                    cursor.close();
                }
            }
        }

        /**
         * Load the flavors for the category.
         *
         * @param holder The Holder
         * @param catUri The Uri for the category
         */
        private void loadFlavors(@NonNull Holder holder, @NonNull Uri catUri) {
            final Uri uri = Uri.withAppendedPath(catUri, "flavor");
            final String sort = Tables.Flavors.POS + " ASC";
            final Cursor cursor = mResolver.query(uri, null, null, null, sort);
            if (cursor != null) {
                try {
                    long id;
                    String name;
                    while (cursor.moveToNext()) {
                        id = cursor.getLong(cursor.getColumnIndex(Tables.Flavors._ID));
                        name = cursor.getString(cursor.getColumnIndex(Tables.Flavors.NAME));
                        holder.flavors.add(new Field(id, name));
                    }
                } finally {
                    cursor.close();
                }
            }
        }

        /**
         * The holder for return data
         */
        static class Holder {
            /**
             * The name of the category
             */
            @Nullable
            String catName;

            /**
             * The list of extra fields for the category
             */
            final ArrayList<Field> extras = new ArrayList<>();

            /**
             * The list of flavors for the category
             */
            final ArrayList<Field> flavors = new ArrayList<>();
        }
    }

    /**
     * Task for saving category data in the background.
     */
    private static class DataSaver extends AsyncTask<Void, Void, Void> {
        /**
         * The Context reference
         */
        @NonNull
        private final WeakReference<Context> mContext;

        /**
         * The ContentResolver to use
         */
        @NonNull
        private final ContentResolver mResolver;

        /**
         * The basic information for the cats table
         */
        @NonNull
        private final ContentValues mCatInfo;

        /**
         * The extra fields for the category
         */
        @NonNull
        private final ArrayList<Field> mExtras;

        /**
         * The flavors for the category
         */
        @NonNull
        private final ArrayList<Field> mFlavors;

        /**
         * The category database ID, if updating
         */
        private final long mCatId;

        /**
         * @param context The Context
         * @param catInfo The basic information for the cats table
         * @param extras  The extra fields for the category
         * @param flavors The flavors for the category
         * @param catId   The category database ID, if updating
         */
        DataSaver(@NonNull Context context, @NonNull ContentValues catInfo, @NonNull ArrayList<Field> extras,
                @NonNull ArrayList<Field> flavors, long catId) {
            mContext = new WeakReference<>(context.getApplicationContext());
            mResolver = context.getContentResolver();
            mCatInfo = catInfo;
            mExtras = extras;
            mFlavors = flavors;
            mCatId = catId;
        }

        @Override
        protected Void doInBackground(Void... params) {
            final Context context = mContext.get();
            if (context == null) {
                return null;
            }

            final Uri catUri = updateCat();
            if (catUri != null) {
                updateExtras(catUri);
                updateFlavors(catUri);
            }

            return null;
        }

        /**
         * Insert or update the basic information about the category.
         *
         * @return The base Uri for the category record
         */
        @Nullable
        private Uri updateCat() {
            final Uri uri;
            if (mCatId > 0) {
                uri = ContentUris.withAppendedId(Tables.Cats.CONTENT_ID_URI_BASE, mCatId);
                if (mCatInfo.size() > 0) {
                    mResolver.update(uri, mCatInfo, null, null);
                }
            } else {
                uri = mResolver.insert(Tables.Cats.CONTENT_URI, mCatInfo);
            }
            return uri;
        }

        /**
         * Insert, update, or delete the extra fields for the category.
         *
         * @param catUri The base Uri for the category
         */
        private void updateExtras(@NonNull Uri catUri) {
            final Uri insertUri = Uri.withAppendedPath(catUri, "extras");
            Uri uri;
            final ContentValues values = new ContentValues();
            int pos = 0;
            for (Field field : mExtras) {
                if (field.id > 0) {
                    uri = ContentUris.withAppendedId(Tables.Extras.CONTENT_ID_URI_BASE, field.id);
                    if (field.delete) {
                        mResolver.delete(uri, null, null);
                    } else {
                        values.put(Tables.Extras.NAME, EntryUtils.filterName(field.name));
                        values.put(Tables.Extras.POS, pos++);
                        values.put(Tables.Extras.DELETED, false);
                        mResolver.update(uri, values, null, null);
                    }
                } else if (!field.isEmpty()) {
                    values.put(Tables.Extras.NAME, EntryUtils.filterName(field.name));
                    values.put(Tables.Extras.POS, pos++);
                    mResolver.insert(insertUri, values);
                }
            }
        }

        /**
         * Insert, update, or delete the flavors for the category.
         *
         * @param catUri The base Uri for the category
         */
        private void updateFlavors(@NonNull Uri catUri) {
            final Uri insertUri = Uri.withAppendedPath(catUri, "flavor");
            Uri uri;
            final ContentValues values = new ContentValues();
            int pos = 0;
            for (Field field : mFlavors) {
                if (field.id > 0) {
                    uri = ContentUris.withAppendedId(Tables.Flavors.CONTENT_ID_URI_BASE, field.id);
                    if (field.delete) {
                        mResolver.delete(uri, null, null);
                    } else {
                        values.put(Tables.Flavors.NAME, field.name);
                        values.put(Tables.Flavors.POS, pos++);
                        mResolver.update(uri, values, null, null);
                    }
                } else if (!field.isEmpty()) {
                    values.put(Tables.Flavors.NAME, field.name);
                    values.put(Tables.Flavors.POS, pos++);
                    mResolver.insert(insertUri, values);
                }
            }
        }
    }

    /**
     * Holder for field data.
     */
    private static class Field implements Parcelable {
        public static final Creator<Field> CREATOR = new Creator<Field>() {
            @Override
            public Field createFromParcel(Parcel in) {
                return new Field(in);
            }

            @Override
            public Field[] newArray(int size) {
                return new Field[size];
            }
        };

        /**
         * The database ID for this field
         */
        final long id;

        /**
         * The name of this field
         */
        @Nullable
        String name;

        /**
         * Whether this field is marked for deletion
         */
        boolean delete;

        /**
         * @param id   The database ID for this field, or 0 if new
         * @param name The name of this field
         */
        Field(long id, @Nullable String name) {
            this(id, name, false);
        }

        /**
         * @param id     The database ID for this field, or 0 if new
         * @param name   The name of this field
         * @param delete The initial deleted status of the field
         */
        Field(long id, @Nullable String name, boolean delete) {
            this.id = id;
            this.name = name;
            this.delete = delete;
        }

        Field(Parcel in) {
            this.id = in.readLong();
            this.name = in.readString();
            this.delete = in.readInt() == 1;
        }

        /**
         * Is this field empty?
         *
         * @return True if the ID is 0 and the name is blank
         */
        boolean isEmpty() {
            return id == 0 && TextUtils.isEmpty(name);
        }

        @SuppressWarnings("SameReturnValue")
        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeLong(id);
            dest.writeString(name);
            dest.writeInt(delete ? 1 : 0);
        }
    }
}