/* * Copyright (c) 2015 Jonas Kalderstam. * * 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 com.nononsenseapps.notepad.ui.editor; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.ActionProvider; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.NestedScrollView; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; 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.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.Toast; import com.android.datetimepicker.date.DatePickerDialog; import com.android.datetimepicker.date.DatePickerDialog.OnDateSetListener; import com.android.datetimepicker.time.TimePickerDialog; import com.nononsenseapps.notepad.R; import com.nononsenseapps.notepad.data.model.sql.Notification; import com.nononsenseapps.notepad.data.model.sql.Task; import com.nononsenseapps.notepad.data.model.sql.TaskList; import com.nononsenseapps.notepad.ui.base.DialogConfirmBase.DialogConfirmedListener; import com.nononsenseapps.notepad.ui.common.DialogDeleteTask; import com.nononsenseapps.notepad.ui.common.DialogPassword; import com.nononsenseapps.notepad.ui.common.DialogPassword.PasswordConfirmedListener; import com.nononsenseapps.notepad.ui.common.MenuStateController; import com.nononsenseapps.notepad.ui.common.NotificationItemHelper; import com.nononsenseapps.notepad.ui.common.StyledEditText; import com.nononsenseapps.notepad.ui.settings.MainPrefs; import com.nononsenseapps.notepad.util.FragmentHelper; import com.nononsenseapps.notepad.util.SharedPreferencesHelper; import com.nononsenseapps.notepad.util.TimeFormatter; import java.util.Calendar; import static com.nononsenseapps.notepad.util.ListHelper.getARealList; /** * A fragment representing a single Note detail screen. */ public class TaskDetailFragment extends Fragment implements OnDateSetListener { // Id of task to open public static final String ARG_ITEM_ID = "item_id"; // If no id is given, a string can be accepted as initial state public static final String ARG_ITEM_CONTENT = "item_text"; // A list id is necessary public static final String ARG_ITEM_LIST_ID = "item_list_id"; // Random identifier private static final String DATE_DIALOG_TAG = "date_9374jf893jd893jt"; public static int LOADER_EDITOR_TASK = 3001; public static int LOADER_EDITOR_TASKLISTS = 3002; public static int LOADER_EDITOR_NOTIFICATIONS = 3003; StyledEditText taskText; CheckBox taskCompleted; Button dueDateBox; LinearLayout notificationList; View taskSection; NestedScrollView editScrollView; InputMethodManager inputManager; // To override intent values with // todo replace functionality of @InstanceState long stateId = -1; // todo replace functionality of @InstanceState long stateListId = -1; // Dao version of the object this fragment represents private Task mTask; // Version when task was opened private Task mTaskOrg; // To save orgState // TODO // AND with task.locked. If result is true, note is locked and has not been // unlocked, otherwise good to show private boolean mLocked = true; LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(final int id, final Bundle args) { if (LOADER_EDITOR_NOTIFICATIONS == id) { return new CursorLoader(getActivity(), Notification.URI, Notification.Columns .FIELDS, Notification.Columns.TASKID + " IS ?", new String[]{Long .toString(args.getLong(ARG_ITEM_ID, -1))}, Notification.Columns.TIME); } else if (LOADER_EDITOR_TASK == id) { return new CursorLoader(getActivity(), Task.getUri(args.getLong(ARG_ITEM_ID, -1)) , Task.Columns.FIELDS, null, null, null); } else if (LOADER_EDITOR_TASKLISTS == id) { return new CursorLoader(getActivity(), TaskList.getUri(args.getLong (ARG_ITEM_LIST_ID)), TaskList.Columns.FIELDS, null, null, null); } else { return null; } } @Override public void onLoadFinished(Loader<Cursor> ldr, Cursor c) { if (LOADER_EDITOR_TASK == ldr.getId()) { if (c != null && c.moveToFirst()) { if (mTask == null) { mTask = new Task(c); if (mTaskOrg == null) { mTaskOrg = new Task(c); } fillUIFromTask(); // Don't want updates while editing // getLoaderManager().destroyLoader(LOADER_EDITOR_TASK); } else { // Don't want updates while editing // getLoaderManager().destroyLoader(LOADER_EDITOR_TASK); // Only update the list if that changes Log.d("nononsenseapps listedit", "Updating list in task from " + mTask .dblist); mTask.dblist = new Task(c).dblist; Log.d("nononsenseapps listedit", "Updating list in task to " + mTask .dblist); if (mTaskOrg != null) { mTaskOrg.dblist = mTask.dblist; } } // Load the list to see if we should hide task bits Bundle args = new Bundle(); args.putLong(ARG_ITEM_LIST_ID, mTask.dblist); getLoaderManager().restartLoader(LOADER_EDITOR_TASKLISTS, args, this); args.clear(); args.putLong(ARG_ITEM_ID, getArguments().getLong(ARG_ITEM_ID, stateId)); getLoaderManager().restartLoader(LOADER_EDITOR_NOTIFICATIONS, args, loaderCallbacks); } else { // Should kill myself maybe? } } else if (LOADER_EDITOR_NOTIFICATIONS == ldr.getId()) { while (c != null && c.moveToNext()) { addNotification(new Notification(c)); } // Don't update while editing // TODO this allows updating of the location name etc getLoaderManager().destroyLoader(LOADER_EDITOR_NOTIFICATIONS); } else if (LOADER_EDITOR_TASKLISTS == ldr.getId()) { // At current only loading a single list if (c != null && c.moveToFirst()) { final TaskList list = new TaskList(c); hideTaskParts(list); } } } @Override public void onLoaderReset(Loader<Cursor> arg0) { } }; private TaskEditorCallbacks mListener; private ActionProvider mShareActionProvider; /* * If in tablet and added, rotating to portrait actually recreats the * fragment even though it isn't visible. So if this is true, don't load * anything. */ private boolean dontLoad = false; /** * Mandatory empty constructor for the fragment manager to instantiate the * fragment (e.g. upon screen orientation changes). */ public TaskDetailFragment() { // Make sure arguments are non-null setArguments(new Bundle()); } void setListeners() { if (dontLoad) { return; } taskText.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) { setShareIntent(s.toString()); } }); } void onDateClick() { final Calendar localTime = Calendar.getInstance(); if (mTask != null && mTask.due != null) { //datePicker = DialogCalendar.getInstance(mTask.due); localTime.setTimeInMillis(mTask.due); }// else { // datePicker = DialogCalendar.getInstance(); //} //final DialogCalendar datePicker; //datePicker.setListener(this); //datePicker.show(getFragmentManager(), DATE_DIALOG_TAG); final DatePickerDialog datedialog = DatePickerDialog.newInstance(this, localTime.get (Calendar.YEAR), localTime.get(Calendar.MONTH), localTime.get(Calendar .DAY_OF_MONTH)); datedialog.show(getFragmentManager(), DATE_DIALOG_TAG); } @Override public void onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth) { final Calendar localTime = Calendar.getInstance(); if (mTask.due != null) { localTime.setTimeInMillis(mTask.due); } localTime.set(Calendar.YEAR, year); localTime.set(Calendar.MONTH, monthOfYear); localTime.set(Calendar.DAY_OF_MONTH, dayOfMonth); // set to 23:59 to be more or less consistent with earlier date only // implementation localTime.set(Calendar.HOUR_OF_DAY, 23); localTime.set(Calendar.MINUTE, 59); mTask.due = localTime.getTimeInMillis(); setDueText(); // Dont ask for time for due date // final TimePickerDialogFragment picker = getTimePickerFragment(); // picker.setListener(this); // picker.show(getFragmentManager(), "time"); } private void setDueText() { if (mTask.due == null) { dueDateBox.setText(""); } else { // Due date dueDateBox.setText(TimeFormatter.getLocalDateOnlyStringLong(getActivity(), mTask.due)); } } void onDueRemoveClick() { if (!isLocked()) { if (mTask != null) { mTask.due = null; } setDueText(); } } // @Override // public void onDialogTimeSet(int hourOfDay, int minute) { // final Calendar localTime = Calendar.getInstance(); // if (mTask.due != null) { // localTime.setTimeInMillis(mTask.due); // } // localTime.set(Calendar.HOUR_OF_DAY, hourOfDay); // localTime.set(Calendar.MINUTE, minute); // // mTask.due = localTime.getTimeInMillis(); // setDueText(); // } // // @Override // public void onDialogTimeCancel() { // // TODO Auto-generated method stub // // } void onAddReminder() { if (mTask != null && !isLocked()) { // IF no id, have to save first if (mTask._id < 1) { saveTask(); } // Only allow if save succeeded if (mTask._id < 1) { Toast.makeText(getActivity(), R.string.please_type_before_reminder, Toast .LENGTH_SHORT).show(); return; } final Notification not = new Notification(mTask._id); not.save(getActivity(), true); // add item to UI addNotification(not); // And scroll to bottom. takes 300ms for item to appear. editScrollView.postDelayed(new Runnable() { @Override public void run() { editScrollView.fullScroll(ScrollView.FOCUS_DOWN); } }, 300); } } // @Override // public void onDateTimeSet(final long time) { // mTask.due = time; // setDueText(); // } /** * task.locked & mLocked */ public boolean isLocked() { return SharedPreferencesHelper.isPasswordSet(getActivity()) & mLocked; } void fillUIFromTask() { Log.d("nononsenseapps editor", "fillUI, act: " + getActivity()); if (isLocked()) { FragmentHelper.handle(new Runnable() { @Override public void run() { taskText.setText(mTask.title); DialogPassword pflock = new DialogPassword(); pflock.setListener(new PasswordConfirmedListener() { @Override public void onPasswordConfirmed() { mLocked = false; fillUIFromTask(); } }); pflock.show(getFragmentManager(), "read_verify"); } }); } else { taskText.setText(mTask.getText()); } setDueText(); taskCompleted.setChecked(mTask.completed != null); taskCompleted.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) mTask.completed = Calendar.getInstance().getTimeInMillis(); else mTask.completed = null; } }); // Lock fields setFieldStatus(); } /** * Set fields to enabled/disabled depending on lockstate */ void setFieldStatus() { final boolean status = !isLocked(); taskText.setEnabled(status); taskCompleted.setEnabled(status); dueDateBox.setEnabled(status); } void hideTaskParts(final TaskList list) { String type; if (list.listtype == null) { type = PreferenceManager.getDefaultSharedPreferences(getActivity()).getString (getString(R.string.pref_listtype), getString(R.string.default_listtype)); } else { type = list.listtype; } taskSection.setVisibility(type.equals(getString(R.string.const_listtype_notes)) ? View .GONE : View.VISIBLE); } // Call to update the share intent private void setShareIntent(final String text) { if (mShareActionProvider != null && taskText != null) { int titleEnd = text.indexOf("\n"); if (titleEnd < 0) { titleEnd = text.length(); } try { // Todo fix for support library version /*mShareActionProvider.setShareIntent(new Intent(Intent.ACTION_SEND).setType ("text/plain").putExtra(Intent.EXTRA_TEXT, text).putExtra(Intent .EXTRA_SUBJECT, text.substring(0, titleEnd)));*/ } catch (RuntimeException e) { // Can crash when too many transactions overflow the buffer Log.d("nononsensenotes", e.getLocalizedMessage()); } } } @Override public void onAttach(Context context) { super.onAttach(context); if (dontLoad) { return; } try { mListener = (TaskEditorCallbacks) getActivity(); } catch (ClassCastException e) { throw new ClassCastException("Activity must implement TaskEditorCallbacks"); } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); inputManager = (InputMethodManager) getContext().getSystemService(Context .INPUT_METHOD_SERVICE); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { setHasOptionsMenu(true); View rootView = inflater.inflate(R.layout.fragment_task_detail, container, false); taskText = (StyledEditText) rootView.findViewById(R.id.taskText); taskCompleted = (CheckBox) rootView.findViewById(R.id.taskCompleted); dueDateBox = (Button) rootView.findViewById(R.id.dueDateBox); notificationList = (LinearLayout) rootView.findViewById(R.id.notificationList); taskSection = rootView.findViewById(R.id.taskSection); editScrollView = (NestedScrollView) rootView.findViewById(R.id.editScrollView); dueDateBox.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onDateClick(); } }); rootView.findViewById(R.id.dueCancelButton).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onDueRemoveClick(); } }); rootView.findViewById(R.id.notificationAdd).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onAddReminder(); } }); setListeners(); return rootView; } @Override public void onActivityCreated(final Bundle state) { super.onActivityCreated(state); if (dontLoad) { return; } boolean openKb = false; final Bundle args = new Bundle(); long idToOpen = mListener.getEditorTaskId(); getArguments().putLong(ARG_ITEM_ID, idToOpen); if (idToOpen > 0) { // Load data from database args.putLong(ARG_ITEM_ID, idToOpen); getLoaderManager().restartLoader(LOADER_EDITOR_TASK, args, loaderCallbacks); } else { // If not valid, find a valid list long listId = mListener.getListOfTask(); if (listId < 1) { listId = getARealList(getActivity(), -1); } // Fail if still not valid if (listId < 1) { // throw new InvalidParameterException( // "Must specify a list id to create a note in!"); Toast.makeText(getActivity(), "Must specify a list id to create a note in!", Toast.LENGTH_SHORT).show(); getActivity().finish(); return; } getArguments().putLong(ARG_ITEM_LIST_ID, listId); args.putLong(ARG_ITEM_LIST_ID, listId); getLoaderManager().restartLoader(LOADER_EDITOR_TASKLISTS, args, loaderCallbacks); openKb = true; mTaskOrg = new Task(); mTask = new Task(); mTask.dblist = listId; // New note but start with the text given mTask.setText(mListener.getInitialTaskText()); fillUIFromTask(); } if (openKb) { /** * Only show keyboard for new/empty notes But not if the showcase * view is showing */ taskText.requestFocus(); inputManager.showSoftInput(taskText, InputMethodManager.SHOW_IMPLICIT); } } @Override public void onResume() { super.onResume(); if (dontLoad) { return; } // Hide data from snoopers if (mTask != null && isLocked()) { fillUIFromTask(); } // See if there was a dialog and set listener again Fragment dateDialog = getFragmentManager().findFragmentByTag(DATE_DIALOG_TAG); if (dateDialog != null) { ((DatePickerDialog) dateDialog).setOnDateSetListener(this); } } @Override public void onSaveInstanceState(final Bundle state) { super.onSaveInstanceState(state); } @Override public void onPause() { super.onPause(); if (dontLoad) { return; } saveTask(); // Set locked again mLocked = true; // If task is actually locked, remove text if (isLocked() && mTask != null && taskText != null) { taskText.setText(mTask.title); } } @Override public void onDetach() { super.onDetach(); mListener = null; } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.fragment_tasks_detail, menu); // Locate MenuItem with ShareActionProvider MenuItem item = menu.findItem(R.id.menu_share); if (item != null) { // Fetch and store ShareActionProvider mShareActionProvider = MenuItemCompat.getActionProvider(item); setShareIntent(""); } } @Override public void onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.menu_share).setEnabled(!isLocked()); if (getActivity() instanceof MenuStateController) { final boolean visible = ((MenuStateController) getActivity()).childItemsVisible(); // Outside group to allow for action bar placement if (menu.findItem(R.id.menu_delete) != null) menu.findItem(R.id.menu_delete).setVisible(visible); if (menu.findItem(R.id.menu_revert) != null) menu.findItem(R.id.menu_revert).setVisible(visible); if (menu.findItem(R.id.menu_share) != null) menu.findItem(R.id.menu_share).setVisible(visible); } } @Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); /*if (itemId == R.id.menu_add) { // TODO should not call if in tablet mode if (mListener != null && mTask != null && mTask.dblist > 0) { mListener.addTaskInList("", mTask.dblist); } return true; } else */ if (itemId == R.id.menu_revert) { // set to null to prevent modifications mTask = null; // Request a close from activity if (mListener != null) { mListener.closeEditor(this); } return true; } else if (itemId == R.id.menu_delete) { if (mTask != null) { if (isLocked()) { DialogPassword delpf = new DialogPassword(); delpf.setListener(new PasswordConfirmedListener() { @Override public void onPasswordConfirmed() { deleteAndClose(); } }); delpf.show(getFragmentManager(), "delete_verify"); } else { deleteAndClose(); } } return true; } return super.onOptionsItemSelected(item); } private void deleteAndClose() { if (mTask != null && mTask._id > 0 && !isLocked()) { DialogDeleteTask.showDialog(getFragmentManager(), mTask._id, new DialogConfirmedListener() { @Override public void onConfirm() { // Prevents save attempts mTask = null; // Request a close from activity // Todo let listener handle delete, and use Snack bar. if (mListener != null) { mListener.closeEditor(TaskDetailFragment.this); } } }); } else { // Prevents save attempts mTask = null; // Request a close from activity if (mListener != null) { mListener.closeEditor(TaskDetailFragment.this); } } } private void saveTask() { // if mTask is null, the task has been deleted or cancelled // If the task is not unlocked, editing is disabled if (mTask != null && !isLocked()) { // Needed for comparison mTask.setText(taskText.getText().toString()); // if new item, only save if something has been entered if ((mTask._id > 0 && !mTask.equals(mTaskOrg)) || (mTask._id == -1 && isThereContent ())) { // mTask.setText(taskText.getText().toString()); mTask.save(getActivity()); // Set the intent to open the task. // So we dont create a new one on rotation for example fixIntent(); // TODO, should restart notification loader for new tasks } } } void fixIntent() { stateId = mTask._id; stateListId = mTask.dblist; if (getActivity() == null) return; final Intent orgIntent = getActivity().getIntent(); if (orgIntent == null || orgIntent.getAction() == null || !orgIntent.getAction().equals (Intent.ACTION_INSERT)) return; if (mTask == null || mTask._id < 1) return; final Intent intent = new Intent().setAction(Intent.ACTION_EDIT).setClass(getActivity(), ActivityEditor.class).setData(mTask.getUri()).putExtra(TaskDetailFragment .ARG_ITEM_LIST_ID, mTask.dblist); getActivity().setIntent(intent); } boolean isThereContent() { boolean result = false; result |= taskText.getText().length() > 0; result |= dueDateBox.getText().length() > 0; return result; } /** * Inserts a notification item in the UI * * @param not */ void addNotification(final Notification not) { if (getActivity() != null) { View nv = LayoutInflater.from(getActivity()).inflate(R.layout.notification_view, null); // So we can update the view later not.view = nv; // Setup all the listeners etc NotificationItemHelper.setup(this, notificationList, nv, not, mTask); notificationList.addView(nv); } } /** * Returns an appropriately themed time picker fragment */ // public TimePickerDialogFragment getTimePickerFragment() { // final String theme = PreferenceManager.getDefaultSharedPreferences( // getActivity()).getString(MainPrefs.KEY_THEME, // getString(R.string.const_theme_light_ab)); // if (theme.contains("light")) { // return TimePickerDialogFragment // .newInstance(R.style.BetterPickersDialogFragment_Light); // } // else { // // dark // return TimePickerDialogFragment // .newInstance(R.style.BetterPickersDialogFragment); // } // } /** * Returns an appropriately themed time picker fragment. Up to caller to set * callback and desired starting time. */ public TimePickerDialog getTimePickerDialog() { final String theme = PreferenceManager.getDefaultSharedPreferences(getActivity()) .getString(MainPrefs.KEY_THEME, getString(R.string.const_theme_light_ab)); final TimePickerDialog timePickerDialog = TimePickerDialog.newInstance(null, 0, 0, android.text.format.DateFormat.is24HourFormat(getActivity())); timePickerDialog.setThemeDark(!theme.contains("light")); return timePickerDialog; } public interface TaskEditorCallbacks { /** * @return the id of the task to open. Negative number indicates a new task. In which * case, the fragment will call getListOfTask. */ long getEditorTaskId(); /** * @return The list where this task (should) live(s). Used for creating new tasks. */ long getListOfTask(); void closeEditor(Fragment fragment); /** * @return The text a new task should contain, or the empty string. */ @NonNull String getInitialTaskText(); } }