package com.automattic.simplenote;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.RelativeSizeSpan;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.TypedValue;
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.webkit.WebView;
import android.widget.CursorAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.automattic.simplenote.analytics.AnalyticsTracker;
import com.automattic.simplenote.models.Note;
import com.automattic.simplenote.models.Tag;
import com.automattic.simplenote.utils.AutoBullet;
import com.automattic.simplenote.utils.DisplayUtils;
import com.automattic.simplenote.utils.DrawableUtils;
import com.automattic.simplenote.utils.MatchOffsetHighlighter;
import com.automattic.simplenote.utils.NoteUtils;
import com.automattic.simplenote.utils.PrefUtils;
import com.automattic.simplenote.utils.SimplenoteLinkify;
import com.automattic.simplenote.utils.SnackbarUtils;
import com.automattic.simplenote.utils.SpaceTokenizer;
import com.automattic.simplenote.utils.TagsMultiAutoCompleteTextView;
import com.automattic.simplenote.utils.TagsMultiAutoCompleteTextView.OnTagAddedListener;
import com.automattic.simplenote.utils.TextHighlighter;
import com.automattic.simplenote.widgets.SimplenoteEditText;
import com.commonsware.cwac.anddown.AndDown;
import com.simperium.client.Bucket;
import com.simperium.client.BucketObjectMissingException;
import com.simperium.client.Query;
import java.util.Calendar;
public class NoteEditorFragment extends Fragment implements Bucket.Listener<Note>,
TextWatcher, OnTagAddedListener, View.OnFocusChangeListener,
SimplenoteEditText.OnSelectionChangedListener,
ShareBottomSheetDialog.ShareSheetListener,
HistoryBottomSheetDialog.HistorySheetListener,
InfoBottomSheetDialog.InfoSheetListener {
public static final String ARG_ITEM_ID = "item_id";
public static final String ARG_NEW_NOTE = "new_note";
static public final String ARG_MATCH_OFFSETS = "match_offsets";
static public final String ARG_MARKDOWN_ENABLED = "markdown_enabled";
public static final int THEME_LIGHT = 0;
public static final int THEME_DARK = 1;
private static final int AUTOSAVE_DELAY_MILLIS = 2000;
private static final int MAX_REVISIONS = 30;
private static final int PUBLISH_TIMEOUT = 20000;
private static final int HISTORY_TIMEOUT = 10000;
private Note mNote;
private final Runnable mAutoSaveRunnable = new Runnable() {
@Override
public void run() {
saveAndSyncNote();
}
};
private Bucket<Note> mNotesBucket;
private SimplenoteEditText mContentEditText;
private TagsMultiAutoCompleteTextView mTagView;
private Handler mAutoSaveHandler;
private Handler mPublishTimeoutHandler;
private Handler mHistoryTimeoutHandler;
private LinearLayout mPlaceholderView;
private CursorAdapter mAutocompleteAdapter;
private boolean mIsNewNote, mIsLoadingNote, mIsMarkdownEnabled;
private ActionMode mActionMode;
private MenuItem mViewLinkMenuItem;
private String mLinkUrl;
private String mLinkText;
private MatchOffsetHighlighter mHighlighter;
private Drawable mEmailIcon, mWebIcon, mMapIcon, mCallIcon;
private MatchOffsetHighlighter.SpanFactory mMatchHighlighter;
private String mMatchOffsets;
private int mCurrentCursorPosition;
private HistoryBottomSheetDialog mHistoryBottomSheet;
// Hides the history bottom sheet if no revisions are loaded
private final Runnable mHistoryTimeoutRunnable = new Runnable() {
@Override
public void run() {
if (!isAdded()) return;
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (mHistoryBottomSheet.isShowing() && !mHistoryBottomSheet.isHistoryLoaded()) {
mHistoryBottomSheet.dismiss();
Toast.makeText(getActivity(), R.string.error_history, Toast.LENGTH_LONG).show();
}
}
});
}
};
private InfoBottomSheetDialog mInfoBottomSheet;
private ShareBottomSheetDialog mShareBottomSheet;
// Contextual action bar for dealing with links
private final ActionMode.Callback mActionModeCallback = new ActionMode.Callback() {
// Called when the action mode is created; startActionMode() was called
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate a menu resource providing context menu items
MenuInflater inflater = mode.getMenuInflater();
if (inflater != null) {
inflater.inflate(R.menu.view_link, menu);
mViewLinkMenuItem = menu.findItem(R.id.menu_view_link);
mode.setTitle(getString(R.string.link));
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
mode.setTitleOptionalHint(false);
}
DrawableUtils.tintMenuWithAttribute(getActivity(), menu, R.attr.actionModeTextColor);
}
return true;
}
// Called each time the action mode is shown. Always called after onCreateActionMode, but
// may be called multiple times if the mode is invalidated.
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false; // Return false if nothing is done
}
// Called when the user selects a contextual menu item
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_view_link:
if (mLinkUrl != null) {
try {
Uri uri = Uri.parse(mLinkUrl);
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(uri);
startActivity(i);
} catch (Exception e) {
e.printStackTrace();
}
mode.finish(); // Action picked, so close the CAB
}
return true;
case R.id.menu_copy:
if (mLinkText != null && getActivity() != null) {
copyToClipboard(mLinkText);
Toast.makeText(getActivity(), getString(R.string.link_copied), Toast.LENGTH_SHORT).show();
mode.finish();
}
return true;
case R.id.menu_share:
if (mLinkText != null) {
showShareSheet();
mode.finish();
}
return true;
default:
return false;
}
}
// Called when the user exits the action mode
@Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
}
};
private Snackbar mPublishingSnackbar;
private boolean mIsUndoingPublishing;
// Resets note publish status if Simperium never returned the new publish status
private final Runnable mPublishTimeoutRunnable = new Runnable() {
@Override
public void run() {
if (!isAdded()) return;
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mNote.setPublished(!mNote.isPublished());
mNote.save();
updatePublishedState(false);
}
});
}
};
private NoteMarkdownFragment mNoteMarkdownFragment;
private String mCss;
private WebView mMarkdown;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public NoteEditorFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getActivity() != null) {
Simplenote currentApp = (Simplenote) getActivity().getApplication();
mNotesBucket = currentApp.getNotesBucket();
}
mCallIcon = DrawableUtils.tintDrawableWithAttribute(getActivity(), R.drawable.ic_call_white_24dp, R.attr.actionModeTextColor);
mEmailIcon = DrawableUtils.tintDrawableWithAttribute(getActivity(), R.drawable.ic_email_white_24dp, R.attr.actionModeTextColor);
mMapIcon = DrawableUtils.tintDrawableWithAttribute(getActivity(), R.drawable.ic_map_white_24dp, R.attr.actionModeTextColor);
mWebIcon = DrawableUtils.tintDrawableWithAttribute(getActivity(), R.drawable.ic_web_white_24dp, R.attr.actionModeTextColor);
mAutoSaveHandler = new Handler();
mPublishTimeoutHandler = new Handler();
mHistoryTimeoutHandler = new Handler();
mMatchHighlighter = new TextHighlighter(getActivity(),
R.attr.editorSearchHighlightForegroundColor, R.attr.editorSearchHighlightBackgroundColor);
mAutocompleteAdapter = new CursorAdapter(getActivity(), null, 0x0) {
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
Activity activity = (Activity) context;
if (activity == null) return null;
return activity.getLayoutInflater().inflate(R.layout.tag_autocomplete_list_item, null);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
TextView textView = (TextView) view;
textView.setText(convertToString(cursor));
}
@Override
public CharSequence convertToString(Cursor cursor) {
return cursor.getString(cursor.getColumnIndex(Tag.NAME_PROPERTY));
}
@Override
public Cursor runQueryOnBackgroundThread(CharSequence filter) {
Activity activity = getActivity();
if (activity == null) return null;
Simplenote application = (Simplenote) activity.getApplication();
Query<Tag> query = application.getTagsBucket().query();
// make the tag name available to the cursor
query.include(Tag.NAME_PROPERTY);
// sort the tags by their names
query.order(Tag.NAME_PROPERTY);
// if there's a filter string find only matching tag names
if (filter != null)
query.where(Tag.NAME_PROPERTY, Query.ComparisonType.LIKE, String.format("%s%%", filter));
return query.execute();
}
};
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
setHasOptionsMenu(true);
View rootView = inflater.inflate(R.layout.fragment_note_editor, container, false);
mContentEditText = ((SimplenoteEditText) rootView.findViewById(R.id.note_content));
mContentEditText.addOnSelectionChangedListener(this);
mTagView = (TagsMultiAutoCompleteTextView) rootView.findViewById(R.id.tag_view);
mTagView.setTokenizer(new SpaceTokenizer());
mTagView.setOnFocusChangeListener(this);
mHighlighter = new MatchOffsetHighlighter(mMatchHighlighter, mContentEditText);
mPlaceholderView = (LinearLayout) rootView.findViewById(R.id.placeholder);
if (DisplayUtils.isLargeScreenLandscape(getActivity()) && mNote == null) {
mPlaceholderView.setVisibility(View.VISIBLE);
getActivity().invalidateOptionsMenu();
mMarkdown = (WebView) rootView.findViewById(R.id.markdown);
switch (PrefUtils.getIntPref(getActivity(), PrefUtils.PREF_THEME, THEME_LIGHT)) {
case THEME_DARK:
mCss = "<link rel=\"stylesheet\" type=\"text/css\" href=\"dark.css\" />";
break;
case THEME_LIGHT:
mCss = "<link rel=\"stylesheet\" type=\"text/css\" href=\"light.css\" />";
break;
}
}
mTagView.setAdapter(mAutocompleteAdapter);
// Load note if we were passed a note Id
Bundle arguments = getArguments();
if (arguments != null && arguments.containsKey(ARG_ITEM_ID)) {
String key = arguments.getString(ARG_ITEM_ID);
if (arguments.containsKey(ARG_MATCH_OFFSETS)) {
mMatchOffsets = arguments.getString(ARG_MATCH_OFFSETS);
}
new loadNoteTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, key);
setIsNewNote(getArguments().getBoolean(ARG_NEW_NOTE, false));
}
return rootView;
}
@Override
public void onResume() {
super.onResume();
mNotesBucket.start();
mNotesBucket.addListener(this);
mTagView.setOnTagAddedListener(this);
if (mContentEditText != null) {
mContentEditText.setTextSize(TypedValue.COMPLEX_UNIT_SP, PrefUtils.getIntPref(getActivity(), PrefUtils.PREF_FONT_SIZE, 14));
}
}
@Override
public void onPause() {
mNotesBucket.removeListener(this);
// Hide soft keyboard if it is showing...
if (getActivity() != null) {
InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
inputMethodManager.hideSoftInputFromWindow(mContentEditText.getWindowToken(), 0);
}
}
// Delete the note if it is new and has empty fields
if (mNote != null && mIsNewNote && noteIsEmpty()) {
mNote.delete();
} else {
saveNote();
}
mTagView.setOnTagAddedListener(null);
if (mAutoSaveHandler != null) {
mAutoSaveHandler.removeCallbacks(mAutoSaveRunnable);
}
if (mPublishTimeoutHandler != null) {
mPublishTimeoutHandler.removeCallbacks(mPublishTimeoutRunnable);
}
if (mHistoryTimeoutHandler != null) {
mHistoryTimeoutHandler.removeCallbacks(mHistoryTimeoutRunnable);
}
mHighlighter.stop();
super.onPause();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (!isAdded() || DisplayUtils.isLargeScreenLandscape(getActivity()) && mNoteMarkdownFragment == null) {
return;
}
inflater.inflate(R.menu.note_editor, menu);
if (mNote != null) {
MenuItem viewPublishedNoteItem = menu.findItem(R.id.menu_view_info);
viewPublishedNoteItem.setVisible(true);
MenuItem trashItem = menu.findItem(R.id.menu_delete).setTitle(R.string.undelete);
if (mNote.isDeleted()) {
trashItem.setTitle(R.string.undelete);
trashItem.setIcon(R.drawable.ic_trash_restore_24dp);
} else {
trashItem.setTitle(R.string.delete);
trashItem.setIcon(R.drawable.ic_trash_24dp);
}
}
DrawableUtils.tintMenuWithAttribute(getActivity(), menu, R.attr.actionBarTextColor);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_view_info:
showInfo();
return true;
case R.id.menu_history:
showHistory();
return true;
case R.id.menu_share:
shareNote();
return true;
case R.id.menu_delete:
if (!isAdded()) return false;
deleteNote();
return true;
case android.R.id.home:
if (!isAdded()) return false;
getActivity().finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void deleteNote() {
NoteUtils.deleteNote(mNote, getActivity());
getActivity().finish();
}
protected void clearMarkdown() {
mMarkdown.loadDataWithBaseURL("file:///android_asset/", mCss + "", "text/html", "utf-8", null);
}
protected void hideMarkdown() {
mMarkdown.setVisibility(View.INVISIBLE);
}
protected void showMarkdown() {
loadMarkdownData();
mMarkdown.setVisibility(View.VISIBLE);
}
private void shareNote() {
if (mNote != null) {
mContentEditText.clearFocus();
showShareSheet();
AnalyticsTracker.track(
AnalyticsTracker.Stat.EDITOR_NOTE_CONTENT_SHARED,
AnalyticsTracker.CATEGORY_NOTE,
"action_bar_share_button"
);
}
}
private void showHistory() {
if (mNote != null && mNote.getVersion() > 1) {
mContentEditText.clearFocus();
mHistoryTimeoutHandler.postDelayed(mHistoryTimeoutRunnable, HISTORY_TIMEOUT);
showHistorySheet();
} else {
Toast.makeText(getActivity(), R.string.error_history, Toast.LENGTH_LONG).show();
}
}
private void showInfo() {
if (mNote != null) {
mContentEditText.clearFocus();
saveNote();
showInfoSheet();
}
}
private boolean noteIsEmpty() {
return (getNoteContentString().trim().length() == 0 && getNoteTagsString().trim().length() == 0);
}
protected void setMarkdownEnabled(boolean enabled) {
mIsMarkdownEnabled = enabled;
if (mIsMarkdownEnabled) {
loadMarkdownData();
}
}
private void loadMarkdownData() {
mMarkdown.loadDataWithBaseURL("file:///android_asset/", mCss +
new AndDown().markdownToHtml(getNoteContentString()), "text/html", "utf-8", null);
}
public void setNote(String noteID, String matchOffsets) {
if (mAutoSaveHandler != null)
mAutoSaveHandler.removeCallbacks(mAutoSaveRunnable);
mPlaceholderView.setVisibility(View.GONE);
if (matchOffsets != null) {
mMatchOffsets = matchOffsets;
} else {
mMatchOffsets = null;
}
// If we have a note already (on a tablet in landscape), save the note.
if (mNote != null) {
if (mIsNewNote && noteIsEmpty())
mNote.delete();
else if (mNote != null)
saveNote();
}
new loadNoteTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, noteID);
}
private void updateNote(Note updatedNote) {
// update note if network change arrived
mNote = updatedNote;
refreshContent(true);
}
private void refreshContent(boolean isNoteUpdate) {
if (mNote != null) {
// Restore the cursor position if possible.
int cursorPosition = newCursorLocation(mNote.getContent(), getNoteContentString(), mContentEditText.getSelectionEnd());
mContentEditText.setText(mNote.getContent());
if (isNoteUpdate) {
// Save the note so any local changes get synced
mNote.save();
if (mContentEditText.hasFocus() && cursorPosition != mContentEditText.getSelectionEnd()) {
mContentEditText.setSelection(cursorPosition);
}
}
afterTextChanged(mContentEditText.getText());
updateTagList();
}
}
private void updateTagList() {
Activity activity = getActivity();
if (activity == null) return;
// Populate this note's tags in the tagView
mTagView.setChips(mNote.getTagString());
}
private int newCursorLocation(String newText, String oldText, int cursorLocation) {
// Ported from the iOS app :)
// Cases:
// 0. All text after cursor (and possibly more) was removed ==> put cursor at end
// 1. Text was added after the cursor ==> no change
// 2. Text was added before the cursor ==> location advances
// 3. Text was removed after the cursor ==> no change
// 4. Text was removed before the cursor ==> location retreats
// 5. Text was added/removed on both sides of the cursor ==> not handled
int newCursorLocation = cursorLocation;
int deltaLength = newText.length() - oldText.length();
// Case 0
if (newText.length() < cursorLocation)
return newText.length();
boolean beforeCursorMatches = false;
boolean afterCursorMatches = false;
try {
beforeCursorMatches = oldText.substring(0, cursorLocation).equals(newText.substring(0, cursorLocation));
afterCursorMatches = oldText.substring(cursorLocation, oldText.length()).equals(newText.substring(cursorLocation + deltaLength, newText.length()));
} catch (Exception e) {
e.printStackTrace();
}
// Cases 2 and 4
if (!beforeCursorMatches && afterCursorMatches)
newCursorLocation += deltaLength;
// Cases 1, 3 and 5 have no change
return newCursorLocation;
}
@Override
public void onTagsChanged(String tagString) {
if (mNote == null || !isAdded()) return;
if (mNote.getTagString() != null && tagString.length() > mNote.getTagString().length()) {
AnalyticsTracker.track(
AnalyticsTracker.Stat.EDITOR_TAG_ADDED,
AnalyticsTracker.CATEGORY_NOTE,
"tag_added_to_note"
);
} else {
AnalyticsTracker.track(
AnalyticsTracker.Stat.EDITOR_TAG_REMOVED,
AnalyticsTracker.CATEGORY_NOTE,
"tag_removed_from_note"
);
}
mNote.setTagString(tagString);
mNote.setModificationDate(Calendar.getInstance());
updateTagList();
mNote.save();
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
// Unused
}
@Override
public void afterTextChanged(Editable editable) {
attemptAutoList(editable);
setTitleSpan(editable);
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
// When text changes, start timer that will fire after AUTOSAVE_DELAY_MILLIS passes
if (mAutoSaveHandler != null) {
mAutoSaveHandler.removeCallbacks(mAutoSaveRunnable);
mAutoSaveHandler.postDelayed(mAutoSaveRunnable, AUTOSAVE_DELAY_MILLIS);
}
// Remove search highlight spans when note content changes
if (mMatchOffsets != null) {
mMatchOffsets = null;
mHighlighter.removeMatches();
}
}
private void setTitleSpan(Editable editable) {
// Set the note title to be a larger size
// Remove any existing size spans
RelativeSizeSpan spans[] = editable.getSpans(0, editable.length(), RelativeSizeSpan.class);
for (RelativeSizeSpan span : spans) {
editable.removeSpan(span);
}
int newLinePosition = getNoteContentString().indexOf("\n");
if (newLinePosition == 0)
return;
editable.setSpan(new RelativeSizeSpan(1.227f), 0, (newLinePosition > 0) ? newLinePosition : editable.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
private void attemptAutoList(Editable editable) {
int oldCursorPosition = mCurrentCursorPosition;
mCurrentCursorPosition = mContentEditText.getSelectionStart();
AutoBullet.apply(editable, oldCursorPosition, mCurrentCursorPosition);
mCurrentCursorPosition = mContentEditText.getSelectionStart();
}
private void saveAndSyncNote() {
if (mNote == null) {
return;
}
new saveNoteTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void setPlaceholderVisible(boolean isVisible) {
if (isVisible) {
mNote = null;
mContentEditText.setText("");
mTagView.setText("");
if (mPlaceholderView != null)
mPlaceholderView.setVisibility(View.VISIBLE);
} else {
if (mPlaceholderView != null)
mPlaceholderView.setVisibility(View.GONE);
}
}
public void setIsNewNote(boolean isNewNote) {
this.mIsNewNote = isNewNote;
}
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
String tagString = getNoteTagsString().trim();
if (tagString.length() > 0) {
mTagView.setChips(tagString);
}
}
}
private Note getNote() {
return mNote;
}
public void setNote(String noteID) {
setNote(noteID, null);
}
private String getNoteContentString() {
if (mContentEditText == null || mContentEditText.getText() == null) {
return "";
} else {
return mContentEditText.getText().toString();
}
}
private String getNoteTagsString() {
if (mTagView == null || mTagView.getText() == null) {
return "";
} else {
return mTagView.getText().toString();
}
}
/**
* Share bottom sheet callbacks
*/
@Override
public void onSharePublishClicked() {
publishNote();
if (mShareBottomSheet != null) {
mShareBottomSheet.dismiss();
}
}
@Override
public void onShareUnpublishClicked() {
unpublishNote();
if (mShareBottomSheet != null) {
mShareBottomSheet.dismiss();
}
}
@Override
public void onShareCollaborateClicked() {
Toast.makeText(getActivity(), R.string.collaborate_message, Toast.LENGTH_LONG).show();
}
@Override
public void onShareDismissed() {
}
/**
* History bottom sheet listeners
*/
@Override
public void onHistoryCancelClicked() {
mContentEditText.setText(mNote.getContent());
if (mHistoryBottomSheet != null) {
mHistoryBottomSheet.dismiss();
}
}
@Override
public void onHistoryRestoreClicked() {
if (mHistoryBottomSheet != null) {
mHistoryBottomSheet.dismiss();
}
saveAndSyncNote();
}
@Override
public void onHistoryDismissed() {
if (!mHistoryBottomSheet.didTapOnButton()) {
mContentEditText.setText(mNote.getContent());
}
if (mHistoryTimeoutHandler != null) {
mHistoryTimeoutHandler.removeCallbacks(mHistoryTimeoutRunnable);
}
}
@Override
public void onHistoryUpdateNote(String content) {
mContentEditText.setText(content);
}
/**
* Info bottom sheet listeners
*/
@Override
public void onInfoPinSwitchChanged(boolean isSwitchedOn) {
NoteUtils.setNotePin(mNote, isSwitchedOn);
}
@Override
public void onInfoMarkdownSwitchChanged(boolean isSwitchedOn) {
mIsMarkdownEnabled = isSwitchedOn;
Activity activity = getActivity();
if (activity instanceof NoteEditorActivity) {
NoteEditorActivity editorActivity = (NoteEditorActivity) activity;
if (mIsMarkdownEnabled) {
editorActivity.showTabs();
if (mNoteMarkdownFragment == null) {
// Get markdown fragment and update content
mNoteMarkdownFragment =
editorActivity.getNoteMarkdownFragment();
mNoteMarkdownFragment.updateMarkdown(getNoteContentString());
}
} else {
editorActivity.hideTabs();
}
} else if (activity instanceof NotesActivity) {
setMarkdownEnabled(mIsMarkdownEnabled);
((NotesActivity) getActivity()).setMarkdownShowing(false);
}
saveNote();
}
@Override
public void onInfoCopyLinkClicked() {
copyToClipboard(mNote.getPublishedUrl());
Toast.makeText(getActivity(), getString(R.string.link_copied), Toast.LENGTH_SHORT).show();
}
@Override
public void onInfoShareLinkClicked() {
if (mInfoBottomSheet != null) {
mInfoBottomSheet.dismiss();
}
showShareSheet();
}
@Override
public void onInfoDismissed() {
}
protected void saveNote() {
if (mNote == null || (mHistoryBottomSheet != null && mHistoryBottomSheet.isShowing())) {
return;
}
String content = getNoteContentString();
String tagString = getNoteTagsString();
if (mNote.hasChanges(content, tagString.trim(), mNote.isPinned(), mIsMarkdownEnabled)) {
mNote.setContent(content);
mNote.setTagString(tagString);
mNote.setModificationDate(Calendar.getInstance());
mNote.setMarkdownEnabled(mIsMarkdownEnabled);
// Send pinned event to google analytics if changed
mNote.save();
AnalyticsTracker.track(
AnalyticsTracker.Stat.EDITOR_NOTE_EDITED,
AnalyticsTracker.CATEGORY_NOTE,
"editor_save"
);
}
}
// Checks if cursor is at a URL when the selection changes
// If it is a URL, show the contextual action bar
@Override
public void onSelectionChanged(int selStart, int selEnd) {
if (selStart == selEnd) {
Editable noteContent = mContentEditText.getText();
if (noteContent == null)
return;
URLSpan[] urlSpans = noteContent.getSpans(selStart, selStart, URLSpan.class);
if (urlSpans.length > 0) {
URLSpan urlSpan = urlSpans[0];
mLinkUrl = urlSpan.getURL();
mLinkText = noteContent.subSequence(noteContent.getSpanStart(urlSpan), noteContent.getSpanEnd(urlSpan)).toString();
if (mActionMode != null) {
mActionMode.setSubtitle(mLinkText);
setLinkMenuItem();
return;
}
// Show the Contextual Action Bar
if (getActivity() != null) {
mActionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(mActionModeCallback);
if (mActionMode != null) {
mActionMode.setSubtitle(mLinkText);
}
setLinkMenuItem();
}
} else if (mActionMode != null) {
mActionMode.finish();
mActionMode = null;
}
} else if (mActionMode != null) {
mActionMode.finish();
mActionMode = null;
}
}
private void setLinkMenuItem() {
if (mViewLinkMenuItem != null && mLinkUrl != null) {
if (mLinkUrl.startsWith("tel:")) {
mViewLinkMenuItem.setIcon(mCallIcon);
mViewLinkMenuItem.setTitle(getString(R.string.call));
} else if (mLinkUrl.startsWith("mailto:")) {
mViewLinkMenuItem.setIcon(mEmailIcon);
mViewLinkMenuItem.setTitle(getString(R.string.email));
} else if (mLinkUrl.startsWith("geo:")) {
mViewLinkMenuItem.setIcon(mMapIcon);
mViewLinkMenuItem.setTitle(getString(R.string.view_map));
} else {
mViewLinkMenuItem.setIcon(mWebIcon);
mViewLinkMenuItem.setTitle(getString(R.string.view_in_browser));
}
}
}
private void setPublishedNote(boolean isPublished) {
if (mNote != null) {
mNote.setPublished(isPublished);
mNote.save();
// reset publish status in 20 seconds if we don't hear back from Simperium
mPublishTimeoutHandler.postDelayed(mPublishTimeoutRunnable, PUBLISH_TIMEOUT);
AnalyticsTracker.track(
(isPublished) ? AnalyticsTracker.Stat.EDITOR_NOTE_PUBLISHED :
AnalyticsTracker.Stat.EDITOR_NOTE_UNPUBLISHED,
AnalyticsTracker.CATEGORY_NOTE,
"publish_note_button"
);
}
}
private void updatePublishedState(boolean isSuccess) {
if (mPublishingSnackbar == null) {
return;
}
mPublishingSnackbar.dismiss();
mPublishingSnackbar = null;
if (isSuccess && isAdded()) {
if (mNote.isPublished()) {
if (mIsUndoingPublishing) {
SnackbarUtils.showSnackbar(getActivity(), R.string.publish_successful,
R.color.simplenote_positive_green,
Snackbar.LENGTH_LONG);
} else {
SnackbarUtils.showSnackbar(getActivity(), R.string.publish_successful,
R.color.simplenote_positive_green,
Snackbar.LENGTH_LONG, R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
mIsUndoingPublishing = true;
unpublishNote();
}
});
}
copyToClipboard(mNote.getPublishedUrl());
} else {
if (mIsUndoingPublishing) {
SnackbarUtils.showSnackbar(getActivity(), R.string.unpublish_successful,
R.color.simplenote_negative_red,
Snackbar.LENGTH_LONG);
} else {
SnackbarUtils.showSnackbar(getActivity(), R.string.unpublish_successful,
R.color.simplenote_negative_red,
Snackbar.LENGTH_LONG, R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
mIsUndoingPublishing = true;
publishNote();
}
});
}
}
} else {
if (mNote.isPublished()) {
SnackbarUtils.showSnackbar(getActivity(), R.string.unpublish_error,
R.color.simplenote_negative_red, Snackbar.LENGTH_LONG);
} else {
SnackbarUtils.showSnackbar(getActivity(), R.string.publish_error,
R.color.simplenote_negative_red, Snackbar.LENGTH_LONG);
}
}
mIsUndoingPublishing = false;
}
private void publishNote() {
if (isAdded()) {
mPublishingSnackbar = SnackbarUtils.showSnackbar(getActivity(), R.string.publishing,
R.color.simplenote_blue, Snackbar.LENGTH_INDEFINITE);
}
setPublishedNote(true);
}
private void unpublishNote() {
if (isAdded()) {
mPublishingSnackbar = SnackbarUtils.showSnackbar(getActivity(), R.string.unpublishing,
R.color.simplenote_blue, Snackbar.LENGTH_INDEFINITE);
}
setPublishedNote(false);
}
private void copyToClipboard(String text) {
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.app_name), text);
clipboard.setPrimaryClip(clip);
}
private void showShareSheet() {
if (isAdded()) {
mShareBottomSheet = new ShareBottomSheetDialog(this, this);
mShareBottomSheet.show(mNote);
}
}
private void showInfoSheet() {
if (isAdded()) {
mInfoBottomSheet = new InfoBottomSheetDialog(this, this);
mInfoBottomSheet.show(mNote);
}
}
private void showHistorySheet() {
if (isAdded()) {
mHistoryBottomSheet = new HistoryBottomSheetDialog(this, this);
// Request revisions for the current note
mNotesBucket.getRevisions(mNote, MAX_REVISIONS, mHistoryBottomSheet.getRevisionsRequestCallbacks());
saveNote();
mHistoryBottomSheet.show(mNote);
}
}
/**
* Simperium listeners
*/
@Override
public void onDeleteObject(Bucket<Note> noteBucket, Note note) {
}
@Override
public void onNetworkChange(Bucket<Note> noteBucket, Bucket.ChangeType changeType, final String key) {
if (changeType == Bucket.ChangeType.MODIFY) {
if (getNote() != null && getNote().getSimperiumKey().equals(key)) {
try {
final Note updatedNote = mNotesBucket.get(key);
if (getActivity() != null) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (mPublishTimeoutHandler != null) {
mPublishTimeoutHandler.removeCallbacks(mPublishTimeoutRunnable);
}
updateNote(updatedNote);
updatePublishedState(true);
}
});
}
} catch (BucketObjectMissingException e) {
e.printStackTrace();
}
}
}
}
@Override
public void onSaveObject(Bucket<Note> noteBucket, Note note) {
// noop
}
@Override
public void onBeforeUpdateObject(Bucket<Note> bucket, Note note) {
// Don't apply updates if we haven't loaded the note yet
if (mIsLoadingNote)
return;
Note openNote = getNote();
if (openNote == null || !openNote.getSimperiumKey().equals(note.getSimperiumKey()))
return;
note.setContent(getNoteContentString());
}
private class loadNoteTask extends AsyncTask<String, Void, Void> {
@Override
protected void onPreExecute() {
mContentEditText.removeTextChangedListener(NoteEditorFragment.this);
mIsLoadingNote = true;
}
@Override
protected Void doInBackground(String... args) {
if (getActivity() == null) {
return null;
}
String noteID = args[0];
Simplenote application = (Simplenote) getActivity().getApplication();
Bucket<Note> notesBucket = application.getNotesBucket();
try {
mNote = notesBucket.get(noteID);
// Set the current note in NotesActivity when on a tablet
if (getActivity() instanceof NotesActivity) {
((NotesActivity) getActivity()).setCurrentNote(mNote);
}
// Set markdown flag for current note
if (mNote != null) {
mIsMarkdownEnabled = mNote.isMarkdownEnabled();
}
} catch (BucketObjectMissingException e) {
// TODO: Handle a missing note
}
return null;
}
@Override
protected void onPostExecute(Void nada) {
if (getActivity() == null || getActivity().isFinishing())
return;
refreshContent(false);
if (mMatchOffsets != null) {
int columnIndex = mNote.getBucket().getSchema().getFullTextIndex().getColumnIndex(Note.CONTENT_PROPERTY);
mHighlighter.highlightMatches(mMatchOffsets, columnIndex);
}
mContentEditText.addTextChangedListener(NoteEditorFragment.this);
if (mNote != null && mNote.getContent().isEmpty()) {
// Show soft keyboard
mContentEditText.requestFocus();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null)
inputMethodManager.showSoftInput(mContentEditText, 0);
}
}, 100);
}
// Show tabs if markdown is enabled globally, for current note, and not tablet landscape
if (mIsMarkdownEnabled) {
// Get markdown view and update content
if (DisplayUtils.isLargeScreenLandscape(getActivity())) {
loadMarkdownData();
} else {
mNoteMarkdownFragment =
((NoteEditorActivity) getActivity()).getNoteMarkdownFragment();
mNoteMarkdownFragment.updateMarkdown(getNoteContentString());
((NoteEditorActivity) getActivity()).showTabs();
}
}
getActivity().invalidateOptionsMenu();
SimplenoteLinkify.addLinks(mContentEditText, Linkify.ALL);
mIsLoadingNote = false;
}
}
private class saveNoteTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... args) {
saveNote();
return null;
}
@Override
protected void onPostExecute(Void nada) {
if (getActivity() != null && !getActivity().isFinishing()) {
// Update links
SimplenoteLinkify.addLinks(mContentEditText, Linkify.ALL);
// Update markdown fragment
if (DisplayUtils.isLargeScreenLandscape(getActivity())) {
loadMarkdownData();
} else if (mNoteMarkdownFragment != null) {
mNoteMarkdownFragment.updateMarkdown(getNoteContentString());
}
}
}
}
}