de.spiritcroc.ownlog.ui.fragment.LogItemEditFragment.java Source code

Java tutorial

Introduction

Here is the source code for de.spiritcroc.ownlog.ui.fragment.LogItemEditFragment.java

Source

/*
 * Copyright (C) 2017 SpiritCroc
 * Email: spiritcroc@gmail.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package de.spiritcroc.ownlog.ui.fragment;

import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetBehavior;
import android.support.v4.content.LocalBroadcastManager;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.Gravity;
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.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.TimePicker;
import android.widget.Toast;

import net.sqlcipher.database.SQLiteDatabase;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;

import de.spiritcroc.ownlog.Constants;
import de.spiritcroc.ownlog.DateFormatter;
import de.spiritcroc.ownlog.PasswdHelper;
import de.spiritcroc.ownlog.R;
import de.spiritcroc.ownlog.Settings;
import de.spiritcroc.ownlog.data.DbContract;
import de.spiritcroc.ownlog.data.DbHelper;
import de.spiritcroc.ownlog.data.LoadLogItemsTask;
import de.spiritcroc.ownlog.data.LoadTagItemsTask;
import de.spiritcroc.ownlog.data.LogItem;
import de.spiritcroc.ownlog.data.TagItem;
import de.spiritcroc.ownlog.ui.activity.SingleFragmentActivity;
import de.spiritcroc.ownlog.ui.view.EditTagsView;

public class LogItemEditFragment extends BaseFragment
        implements View.OnClickListener, View.OnLongClickListener, DatePickerDialog.OnDateSetListener,
        TimePickerDialog.OnTimeSetListener, PasswdHelper.RequestDbListener, EditTagsView.EditTagsProvider {

    private static final String TAG = LogItemEditFragment.class.getSimpleName();

    // Saved instance state bundle keys
    private static final String KEY_ADD_ITEM = LogItemEditFragment.class.getName() + ".add_item";
    private static final String KEY_TEMPORARY_EXISTENCE = LogItemEditFragment.class.getName()
            + ".temporary_existence";
    private static final String KEY_INIT_TITLE = LogItemEditFragment.class.getName() + ".init_title";
    private static final String KEY_SET_TITLE = LogItemEditFragment.class.getName() + ".set_title";
    private static final String KEY_INIT_CONTENT = LogItemEditFragment.class.getName() + ".init_content";
    private static final String KEY_SET_CONTENT = LogItemEditFragment.class.getName() + ".set_content";
    private static final String KEY_INIT_TIME = LogItemEditFragment.class.getName() + ".init_time";
    private static final String KEY_SET_TIME = LogItemEditFragment.class.getName() + ".set_time";
    private static final String KEY_INIT_TAGS = LogItemEditFragment.class.getName() + ".init_tags";
    private static final String KEY_SET_TAGS = LogItemEditFragment.class.getName() + ".set_tags";
    private static final String KEY_AVAILABLE_TAGS = LogItemEditFragment.class.getName() + ".available_tags";

    private static final String FRAGMENT_TAG_ATTACHMENTS = TAG + ".attachments";

    // DB request codes
    private static final int DB_REQUEST_LOAD = 1;
    private static final int DB_REQUEST_SAVE = 2;
    private static final int DB_REQUEST_SAVE_FOR_ATTACHMENTS = 3;
    private static final int DB_REQUEST_DELETE = 4;

    private boolean mAddItem = true;
    private boolean mTemporaryExistence = false;
    private long mEditItemId = LogItem.ID_NONE;

    // Remember the initial values to check whether anything needs saving
    private long mInitTime = System.currentTimeMillis();
    private String mInitTitle = "";
    private String mInitContent = "";
    private ArrayList<TagItem> mInitTags = new ArrayList<>();
    private ArrayList<TagItem> mAvailableTags = new ArrayList<>();
    private ArrayList<TagItem> mSetTags = null;

    private Calendar mTime;

    private View mEditTime;
    private EditText mEditLogTitle;
    private EditText mEditLogContent;
    private EditTagsView mEditTagsView;
    private BottomSheetBehavior mBottomSheetBehavior;
    private LogAttachmentsEditFragment mAttachmentsFragment;

    /**
     * @param logItem
     * The LogItem to edit. If null, a new item will be created.
     */
    public static void show(Context context, @Nullable LogItem logItem) {
        Intent intent = new Intent(context, SingleFragmentActivity.class).putExtra(Constants.EXTRA_FRAGMENT_CLASS,
                LogItemEditFragment.class.getName());
        if (logItem != null) {
            Bundle fragmentArgs = new Bundle();
            fragmentArgs.putLong(Constants.EXTRA_LOG_ITEM_ID, logItem.id);
            intent.putExtra(Constants.EXTRA_FRAGMENT_BUNDLE, fragmentArgs);
        }
        context.startActivity(intent);
    }

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

        if (savedInstanceState != null) {
            mEditItemId = savedInstanceState.getLong(Constants.EXTRA_LOG_ITEM_ID, -1);
            mAddItem = savedInstanceState.getBoolean(KEY_ADD_ITEM, mEditItemId == -1);
            mTemporaryExistence = savedInstanceState.getBoolean(KEY_TEMPORARY_EXISTENCE, false);
        } else if (getArguments() == null) {
            mAddItem = true;
        } else {
            mEditItemId = getArguments().getLong(Constants.EXTRA_LOG_ITEM_ID, -1);
            mAddItem = mEditItemId == -1;
        }

        // We will be using the the keyboard more in this screen. Don't do any awkward content
        // moving around; just resize the content to fit into the visible window.
        getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.log_edit_item, container, false);
        mEditTime = view.findViewById(R.id.date_button);
        mEditLogTitle = (EditText) view.findViewById(R.id.title_edit);
        mEditLogContent = (EditText) view.findViewById(R.id.content_edit);
        mEditTagsView = (EditTagsView) view.findViewById(R.id.edit_tags_view);
        View attachmentsStubView = view.findViewById(R.id.attachments_stub);
        mBottomSheetBehavior = BottomSheetBehavior.from(attachmentsStubView);
        mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
        mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                if (newState == BottomSheetBehavior.STATE_HIDDEN
                        || newState == BottomSheetBehavior.STATE_COLLAPSED) {
                    // Allow editing, no bottom sheet in the way
                    mEditLogTitle.setFocusableInTouchMode(true);
                    mEditLogContent.setFocusableInTouchMode(true);
                    mEditLogTitle.setFocusable(true);
                    mEditLogContent.setFocusable(true);
                } else {
                    // Don't allow editing covered text
                    mEditLogTitle.setFocusable(false);
                    mEditLogContent.setFocusable(false);
                }
            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {
            }
        });

        mEditTagsView.setTagsProvider(this);

        mEditTime.setOnClickListener(this);
        mEditTime.setOnLongClickListener(this);

        if (Settings.getBoolean(getActivity(), Settings.EASTEREGG)) {
            mEditLogTitle.setOnLongClickListener(mEastereggLongclickListener);
            mEditLogContent.setOnLongClickListener(mEastereggLongclickListener);
        }

        if (!restoreValues(savedInstanceState)) {
            loadContent();
            if (mEditItemId == -1) {
                // If we have an item id, initValues is called when we have the values for it
                initValues();
            }
        }

        if (savedInstanceState == null) {
            mAttachmentsFragment = new LogAttachmentsEditFragment();
            Bundle attachmentArgs = new Bundle();
            attachmentArgs.putLong(Constants.EXTRA_LOG_ITEM_ID, mEditItemId);
            mAttachmentsFragment.setArguments(attachmentArgs);
            getFragmentManager().beginTransaction()
                    .add(R.id.attachments_stub, mAttachmentsFragment, FRAGMENT_TAG_ATTACHMENTS).commit();
        } else {
            mAttachmentsFragment = (LogAttachmentsEditFragment) getFragmentManager()
                    .findFragmentByTag(FRAGMENT_TAG_ATTACHMENTS);
        }
        mAttachmentsFragment.setTitleOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
            }
        });

        return view;
    }

    private void initValues() {
        mEditLogTitle.setText(mInitTitle);
        mEditLogContent.setText(mInitContent);

        mTime = Calendar.getInstance();
        mTime.setTimeInMillis(mInitTime);

        mSetTags = (ArrayList<TagItem>) mInitTags.clone();

        updateTitle();
        mEditTagsView.updateContent();
    }

    private boolean restoreValues(Bundle savedInstanceState) {
        if (savedInstanceState == null) {
            return false;
        }
        try {
            if (savedInstanceState.containsKey(KEY_INIT_TITLE)) {
                mInitTitle = savedInstanceState.getString(KEY_INIT_TITLE);
                mEditLogTitle.setText(savedInstanceState.getString(KEY_SET_TITLE));
                mInitContent = savedInstanceState.getString(KEY_INIT_CONTENT);
                mEditLogContent.setText(savedInstanceState.getString(KEY_SET_CONTENT));
                mInitTime = savedInstanceState.getLong(KEY_INIT_TIME);
                mTime = Calendar.getInstance();
                mTime.setTimeInMillis(savedInstanceState.getLong(KEY_SET_TIME));
                mInitTags = new ArrayList<>(
                        Arrays.asList((TagItem[]) savedInstanceState.getParcelableArray(KEY_INIT_TAGS)));
                mSetTags = new ArrayList<>(
                        Arrays.asList((TagItem[]) savedInstanceState.getParcelableArray(KEY_SET_TAGS)));
                mAvailableTags = new ArrayList<>(
                        Arrays.asList((TagItem[]) savedInstanceState.getParcelableArray(KEY_AVAILABLE_TAGS)));

                updateTitle();
                mEditTagsView.updateContent();
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(KEY_ADD_ITEM, mAddItem);
        outState.putBoolean(KEY_TEMPORARY_EXISTENCE, mTemporaryExistence);
        outState.putString(KEY_INIT_TITLE, mInitTitle);
        outState.putString(KEY_SET_TITLE, mEditLogTitle.getText().toString());
        outState.putString(KEY_INIT_CONTENT, mInitContent);
        outState.putString(KEY_SET_CONTENT, mEditLogContent.getText().toString());
        outState.putLong(KEY_INIT_TIME, mInitTime);
        outState.putLong(KEY_SET_TIME, mTime.getTimeInMillis());
        outState.putParcelableArray(KEY_INIT_TAGS, mInitTags.toArray(new TagItem[mInitTags.size()]));
        outState.putParcelableArray(KEY_SET_TAGS, mSetTags.toArray(new TagItem[mSetTags.size()]));
        outState.putParcelableArray(KEY_AVAILABLE_TAGS, mAvailableTags.toArray(new TagItem[mAvailableTags.size()]));
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(mAddItem ? R.menu.fragment_add_log_item : R.menu.fragment_edit_log_item, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.action_save:
            saveChanges();
            return true;
        case R.id.action_attachments:
            toggleAttachments();
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onClick(View view) {
        if (view == mEditTime) {
            new DatePickerDialog(getActivity(), this, mTime.get(Calendar.YEAR), mTime.get(Calendar.MONTH),
                    mTime.get(Calendar.DAY_OF_MONTH)).show();
        }
    }

    @Override
    public boolean onLongClick(View view) {
        if (view == mEditTime) {
            int[] viewPos = new int[2];
            view.getLocationInWindow(viewPos);
            int[] offsetPos = new int[2];
            view.getRootView().findViewById(android.R.id.content).getLocationInWindow(offsetPos);
            Toast toast = Toast.makeText(getActivity(), view.getContentDescription(), Toast.LENGTH_SHORT);

            toast.setGravity(Gravity.END | Gravity.TOP, 0, viewPos[1] - offsetPos[1] + view.getHeight());
            toast.show();
            return true;
        }
        return false;
    }

    @Override
    public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) {
        mTime.set(year, month, dayOfMonth);

        new TimePickerDialog(getActivity(), this, mTime.get(Calendar.HOUR_OF_DAY), mTime.get(Calendar.MINUTE),
                DateFormat.is24HourFormat(getActivity())).show();
        updateTitle();
    }

    @Override
    public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
        mTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
        mTime.set(Calendar.MINUTE, minute);
        updateTitle();
    }

    private void loadContent() {
        PasswdHelper.getWritableDatabase(getActivity(), this, DB_REQUEST_LOAD);
    }

    private void loadContent(SQLiteDatabase db) {
        new LoadContentTask(db).execute();
    }

    private void saveChanges() {
        if (noChangesPresent()) {
            finish();
        } else {
            PasswdHelper.getWritableDatabase(getActivity(), this, DB_REQUEST_SAVE);
        }
    }

    private void toggleAttachments() {
        if (mBottomSheetBehavior.getState() != BottomSheetBehavior.STATE_EXPANDED) {
            if (mAddItem) {
                PasswdHelper.getWritableDatabase(getActivity(), this, DB_REQUEST_SAVE_FOR_ATTACHMENTS);
            } else {
                showAttachments();
            }
        } else {
            hideAttachments();
        }
    }

    private void showAttachments() {
        // Hide keyboard
        View view = getActivity().getCurrentFocus();
        if (view != null) {
            InputMethodManager imm = (InputMethodManager) getActivity()
                    .getSystemService(Context.INPUT_METHOD_SERVICE);
            if (imm != null) {
                imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
            }
            // If we don't delay this, bottom sheet might position itself over the
            // former keyboard position, which is then gone
            view.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
                }
            }, 200);
        } else {
            mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
        }
    }

    private void hideAttachments() {
        mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
    }

    private void saveChanges(SQLiteDatabase db) {
        LogItem logItem = new LogItem(mEditItemId, mTime.getTimeInMillis(), 0, // Timestamp updated while saving
                mEditLogTitle.getText().toString(), mEditLogContent.getText().toString());
        logItem.id = DbHelper.saveLogItemToDb(logItem, db, mAddItem, false);
        ArrayList<TagItem> addedTags = new ArrayList<>();
        ArrayList<TagItem> removedTags = new ArrayList<>();
        TagItem.checkTagListDiff(mInitTags, mSetTags, addedTags, removedTags);
        DbHelper.saveLogTagsToDb(logItem, db, addedTags, removedTags);
        db.close();
        finish();
        // Notify about added/edited item
        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(
                new Intent(Constants.EVENT_LOG_UPDATE).putExtra(Constants.EXTRA_LOG_ITEM_ID, logItem.id));
    }

    /**
     * Attachments need to be saved immediately, so log entry needs to already exist.
     * In case we're in add mode, add an empty entry and convert to edit mode
     */
    private void ensureExistence(SQLiteDatabase db) {
        if (mAddItem) {
            LogItem logItem = new LogItem(LogItem.generateId(), mTime.getTimeInMillis(), 0, // Timestamp updated while saving
                    "", "");
            mEditItemId = DbHelper.saveLogItemToDb(logItem, db, mAddItem, false);
            db.close();
            mAddItem = false;
            mTemporaryExistence = true;
            mAttachmentsFragment.setLogId(mEditItemId);
            // Notify about added/edited item
            LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(
                    new Intent(Constants.EVENT_LOG_UPDATE).putExtra(Constants.EXTRA_LOG_ITEM_ID, mEditItemId));
            // TODO invalidate options menu? currenlty both are the same either way though
        } else {
            db.close();
        }
    }

    private void discardExistence() {
        if (mTemporaryExistence && noChangesPresent() && !mAttachmentsFragment.hasAttachments()) {
            PasswdHelper.getWritableDatabase(getActivity(), this, DB_REQUEST_DELETE);
        } else {
            finish();
        }
    }

    private void deleteEntry(SQLiteDatabase db) {
        DbHelper.removeLogItemsFromDb(getActivity(), db, new LogItem(mEditItemId));
        db.close();
        // Notify about deleted item
        LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(
                new Intent(Constants.EVENT_LOG_UPDATE).putExtra(Constants.EXTRA_LOG_ITEM_ID, mEditItemId));
        finish();
    }

    @Override
    public void receiveWritableDatabase(SQLiteDatabase db, int requestId) {
        switch (requestId) {
        case DB_REQUEST_LOAD:
            loadContent(db);
            break;
        case DB_REQUEST_SAVE:
            saveChanges(db);
            break;
        case DB_REQUEST_SAVE_FOR_ATTACHMENTS:
            ensureExistence(db);
            showAttachments();
            break;
        case DB_REQUEST_DELETE:
            deleteEntry(db);
            break;
        }
    }

    private void updateTitle() {
        if (!isAdded()) {
            return;
        }
        getActivity().setTitle(getString(mAddItem ? R.string.title_log_item_add : R.string.title_log_item_edit,
                DateFormatter.getDateForTitle(getActivity(), mTime.getTimeInMillis())));
    }

    @Override
    public boolean onUpOrBackPressed(boolean backPress) {
        if (backPress && mBottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
            mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
        } else if (noChangesPresent()) {
            discardExistence();
        } else {
            // User made some changes; make sure he gets what he wants after exiting the screen
            new AlertDialog.Builder(getActivity()).setMessage(R.string.dialog_unsaved_changes)
                    .setPositiveButton(R.string.dialog_save_changes, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            saveChanges();
                        }
                    }).setNegativeButton(R.string.dialog_discard_changes, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            finish();
                        }
                    }).setNeutralButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i) {
                            // Only close dialog
                        }
                    }).show();
        }
        return true;
    }

    private boolean noChangesPresent() {
        return mInitTitle.equals(mEditLogTitle.getText().toString())
                && mInitContent.equals(mEditLogContent.getText().toString()) && mInitTime == mTime.getTimeInMillis()
                && TagItem.checkTagListDiff(mInitTags, mSetTags, null, null);
    }

    private void finish() {
        getActivity().finish();
    }

    private class LoadContentTask extends LoadLogItemsTask {

        LoadContentTask(SQLiteDatabase db) {
            super(db);
        }

        @Override
        protected ArrayList<LogItem> doInBackground(Void... params) {
            // Request available tags
            mAvailableTags = LoadTagItemsTask.loadAvailableTags(mDb, null);
            if (mAddItem) {
                // We're done
                mDb.close();
                return new ArrayList<>();
            } else {
                // Request log item
                return super.doInBackground(params);
            }
        }

        @Override
        protected void onPostExecute(ArrayList<LogItem> result) {
            if (result.isEmpty()) {
                if (!mAddItem) {
                    Log.e(TAG, "DB response is empty");
                    finish();
                } // else: we're done
            } else if (result.size() > 1) {
                Log.e(TAG, "Too many objects found");
                finish();
            } else {
                LogItem logItem = result.get(0);
                mInitTime = logItem.time;
                mInitTitle = logItem.title;
                mInitContent = logItem.content;
                mInitTags = logItem.tags;
                initValues();
            }
        }

        @Override
        protected String getSelection() {
            return DbContract.Log._ID + " = " + mEditItemId;
        }
    }

    @Override
    public List<TagItem> getAvailableTags() {
        return mAvailableTags;
    }

    @Override
    public List<TagItem> getSetTags() {
        return mSetTags;
    }

    @Override
    public void onDeleteTag(TagItem item) {
        mInitTags.remove(item);
    }

    @Override
    public void onSetTagsChanged() {
    }

    private View.OnLongClickListener mEastereggLongclickListener = new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View view) {
            if (view instanceof EditText) {
                ((EditText) view).setText(((EditText) view).getText().toString() + getString(R.string.easteregg));
                return true;
            }
            return false;
        }
    };
}