diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentActivity.java b/app/src/main/java/net/gsantner/markor/activity/DocumentActivity.java index 048933c8b..7ab1dd500 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentActivity.java @@ -36,7 +36,6 @@ import net.gsantner.markor.model.Document; import net.gsantner.markor.util.ActivityUtils; import net.gsantner.markor.util.AppSettings; -import net.gsantner.markor.util.DocumentIO; import net.gsantner.markor.util.PermissionChecker; import net.gsantner.opoc.activity.GsFragmentBase; import net.gsantner.opoc.util.Callback; @@ -73,13 +72,13 @@ public static void launch(Activity activity, File path, Boolean isFolder, Boolea intent = new Intent(activity, DocumentActivity.class); } if (path != null) { - intent.putExtra(DocumentIO.EXTRA_PATH, path); + intent.putExtra(Document.EXTRA_PATH, path); } if (lineNumber != null && lineNumber >= 0) { - intent.putExtra(DocumentIO.EXTRA_FILE_LINE_NUMBER, lineNumber); + intent.putExtra(Document.EXTRA_FILE_LINE_NUMBER, lineNumber); } if (isFolder != null) { - intent.putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, isFolder); + intent.putExtra(Document.EXTRA_PATH_IS_FOLDER, isFolder); } if (doPreview != null) { intent.putExtra(DocumentActivity.EXTRA_DO_PREVIEW, doPreview); @@ -186,8 +185,8 @@ private void handleLaunchingIntent(Intent intent) { String intentAction = intent.getAction(); Uri intentData = intent.getData(); - File file = (File) intent.getSerializableExtra(DocumentIO.EXTRA_PATH); - boolean fileIsFolder = intent.getBooleanExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, false); + File file = (File) intent.getSerializableExtra(Document.EXTRA_PATH); + boolean fileIsFolder = intent.getBooleanExtra(Document.EXTRA_PATH_IS_FOLDER, false); boolean intentIsView = Intent.ACTION_VIEW.equals(intentAction); boolean intentIsSend = Intent.ACTION_SEND.equals(intentAction); @@ -204,7 +203,7 @@ private void handleLaunchingIntent(Intent intent) { } if (!intentIsSend && file != null) { - final int paramLineNumber = intent.getIntExtra(DocumentIO.EXTRA_FILE_LINE_NUMBER, (intentData != null ? StringUtils.tryParseInt(intentData.getQueryParameter("line"), -1) : -1)); + final int paramLineNumber = intent.getIntExtra(Document.EXTRA_FILE_LINE_NUMBER, (intentData != null ? StringUtils.tryParseInt(intentData.getQueryParameter("line"), -1) : -1)); final boolean paramPreview = (paramLineNumber < 0) && (intent.getBooleanExtra(EXTRA_DO_PREVIEW, false) || (file.exists() && file.isFile() && _appSettings.getDocumentPreviewState(file.getPath())) || file.getName().startsWith("index.")); diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentEditFragment.java b/app/src/main/java/net/gsantner/markor/activity/DocumentEditFragment.java index 457efc6ed..54ee4dd38 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentEditFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentEditFragment.java @@ -55,7 +55,6 @@ import net.gsantner.markor.ui.hleditor.HighlightingEditor; import net.gsantner.markor.util.AppSettings; import net.gsantner.markor.util.ContextUtils; -import net.gsantner.markor.util.DocumentIO; import net.gsantner.markor.util.MarkorWebViewClient; import net.gsantner.markor.util.ShareUtil; import net.gsantner.opoc.activity.GsFragmentBase; @@ -96,7 +95,7 @@ public class DocumentEditFragment extends GsFragmentBase implements TextFormat.T public static DocumentEditFragment newInstance(Document document) { DocumentEditFragment f = new DocumentEditFragment(); Bundle args = new Bundle(); - args.putSerializable(DocumentIO.EXTRA_DOCUMENT, document); + args.putSerializable(Document.EXTRA_DOCUMENT, document); f.setArguments(args); return f; } @@ -104,9 +103,9 @@ public static DocumentEditFragment newInstance(Document document) { public static DocumentEditFragment newInstance(File path, boolean pathIsFolder, final int lineNumber) { DocumentEditFragment f = new DocumentEditFragment(); Bundle args = new Bundle(); - args.putSerializable(DocumentIO.EXTRA_PATH, path); - args.putBoolean(DocumentIO.EXTRA_PATH_IS_FOLDER, pathIsFolder); - args.putInt(DocumentIO.EXTRA_FILE_LINE_NUMBER, lineNumber); + args.putSerializable(Document.EXTRA_PATH, path); + args.putBoolean(Document.EXTRA_PATH_IS_FOLDER, pathIsFolder); + args.putInt(Document.EXTRA_FILE_LINE_NUMBER, lineNumber); f.setArguments(args); return f; } @@ -135,7 +134,6 @@ public static DocumentEditFragment newInstance(File path, boolean pathIsFolder, private boolean _isPreviewVisible; private MarkorWebViewClient _webViewClient; private boolean _nextConvertToPrintMode = false; - private boolean _firstFileLoad = true; public DocumentEditFragment() { super(); @@ -181,9 +179,13 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { if (savedInstanceState != null && savedInstanceState.containsKey(SAVESTATE_DOCUMENT)) { _document = (Document) savedInstanceState.getSerializable(SAVESTATE_DOCUMENT); + _document.resetModTime(); // Ensure document is loaded on restore state + } else { + _document = Document.fromArguments(getActivity(), getArguments()); } - _document = loadDocument(); - loadDocumentIntoUi(); + + loadDocument(); + if (savedInstanceState != null && savedInstanceState.containsKey(SAVESTATE_CURSOR_POS)) { int cursor = savedInstanceState.getInt(SAVESTATE_CURSOR_POS); if (cursor >= 0 && cursor < _hlEditor.length()) { @@ -219,18 +221,18 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { @Override public void onResume() { super.onResume(); - checkReloadDisk(false); + + loadDocument(); + int cursor = _hlEditor.getSelectionStart(); cursor = Math.max(0, cursor); cursor = Math.min(_hlEditor.length(), cursor); _hlEditor.setSelection(cursor); _hlEditor.setGravity(_appSettings.isEditorStartEditingInCenter() ? Gravity.CENTER : Gravity.NO_GRAVITY); + if (_document != null && _document.getFile() != null) { - if (!_document.getFile().getParentFile().exists()) { - //noinspection ResultOfMethodCallIgnored - _document.getFile().getParentFile().mkdirs(); - } + _document.testCreateParent(); boolean permok = _shareUtil.canWriteFile(_document.getFile(), false); if (!permok && !_document.getFile().isDirectory() && _shareUtil.canWriteFile(_document.getFile(), _document.getFile().isDirectory())) { permok = true; @@ -242,13 +244,6 @@ public void onResume() { _textSdWarning.setVisibility(permok ? View.GONE : View.VISIBLE); } - /*if (_savedInstanceState != null && _savedInstanceState.containsKey("undoredopref")) { - _hlEditor.postDelayed(() -> { - SharedPreferences sp = getContext().getSharedPreferences("unforedopref", 0); - _editTextUndoRedoHelper.restorePersistentState(sp, _editTextUndoRedoHelper.undoRedoPrefKeyForFile(_document.getFile())); - }, 100); - }*/ - if (_document != null && _document.getFile() != null && _document.getFile().getAbsolutePath().contains("mordor/1-epub-experiment.md") && getActivity() instanceof DocumentActivity) { _hlEditor.setText(CoolExperimentalStuff.convertEpubToText(_document.getFile(), getString(R.string.page))); } @@ -319,18 +314,25 @@ public boolean onQueryTextChange(String text) { updateMenuToggleStates(_document.getFormat()); } - public void loadDocumentIntoUi() { + public void loadDocument() { int editorpos = _hlEditor.getSelectionStart(); - _hlEditor.setText(_document.getContent()); + + // Load document if mod time newer than that recorded on last load + if (_document.hasNewerModTime()) { + _hlEditor.setText(_document.loadContent(getContext())); + } + editorpos = editorpos > _hlEditor.length() ? _hlEditor.length() - 1 : editorpos; _hlEditor.setSelection(Math.max(editorpos, 0)); Activity activity = getActivity(); + if (activity instanceof DocumentActivity) { DocumentActivity da = ((DocumentActivity) activity); da.setDocumentTitle(_document.getTitle()); da.setDocument(_document); } - // At this stage the document format has been determined from extension etc + + // Upon construction, the document format has been determined from extension etc // Here we replace it with the last saved format. _document.setFormat(_appSettings.getDocumentFormat(getPath(), _document.getFormat())); applyTextFormat(_document.getFormat()); @@ -369,13 +371,11 @@ public boolean onOptionsItemSelected(final MenuItem item) { return true; } case R.id.action_save: { - DocumentIO.SAVE_IGNORE_EMTPY_NEXT_TIME = true; saveDocument(); - DocumentIO.SAVE_IGNORE_EMTPY_NEXT_TIME = false; return true; } case R.id.action_reload: { - checkReloadDisk(true); + loadDocument(); return true; } case R.id.action_preview: { @@ -396,7 +396,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { } case R.id.action_share_text: { if (saveDocument()) { - _shareUtil.shareText(_document.getContent(), "text/plain"); + _shareUtil.shareText(_hlEditor.getText().toString(), "text/plain"); } return true; } @@ -410,14 +410,14 @@ public boolean onOptionsItemSelected(final MenuItem item) { case R.id.action_share_html_source: { if (saveDocument()) { TextConverter converter = TextFormat.getFormat(_document.getFormat(), getActivity(), _document, _hlEditor).getConverter(); - _shareUtil.shareText(converter.convertMarkup(_document.getContent(), _hlEditor.getContext(), false, _document.getFile()), + _shareUtil.shareText(converter.convertMarkup(_hlEditor.getText().toString(), _hlEditor.getContext(), false, _document.getFile()), "text/" + (item.getItemId() == R.id.action_share_html ? "html" : "plain")); } return true; } case R.id.action_share_calendar_event: { if (saveDocument()) { - if (!_shareUtil.createCalendarAppointment(_document.getTitle(), _document.getContent(), null)) { + if (!_shareUtil.createCalendarAppointment(_document.getTitle(), _hlEditor.getText().toString(), null)) { Toast.makeText(getActivity(), R.string.no_calendar_app_is_installed, Toast.LENGTH_SHORT).show(); } } @@ -425,7 +425,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { } case android.R.id.home: { final Activity activity = getActivity(); - if ((saveDocument() || (_hlEditor.length() < 10 && TextUtils.getTrimmedLength(_hlEditor.getEditableText()) == 0)) && activity != null) { + if (activity != null && saveDocument()) { activity.onBackPressed(); } return true; @@ -440,7 +440,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { Toast.makeText(getActivity(), R.string.please_wait, Toast.LENGTH_LONG).show(); _webView.postDelayed(() -> { if (item.getItemId() == R.id.action_share_pdf && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - _shareUtil.printOrCreatePdfFromWebview(_webView, _document, _document.getContent().contains("beamer\n")); + _shareUtil.printOrCreatePdfFromWebview(_webView, _document, _hlEditor.getText().toString().contains("beamer\n")); } else if (item.getItemId() != R.id.action_share_pdf) { _shareUtil.shareImage(net.gsantner.opoc.util.ShareUtil.getBitmapFromWebView(_webView, item.getItemId() == R.id.action_share_image)); } @@ -467,7 +467,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { return true; } case R.id.action_send_debug_log: { - final String text = AppSettings.getDebugLog() + "\n\n------------------------\n\n\n\n" + DocumentIO.getMaskedContent(_document); + final String text = AppSettings.getDebugLog() + "\n\n------------------------\n\n\n\n" + Document.getMaskedContent(_hlEditor.getText().toString()); _shareUtil.draftEmail("Debug Log " + getString(R.string.app_name_real), text, "debug@localhost.lan"); return true; } @@ -506,7 +506,7 @@ public void onFsViewerConfig(FilesystemViewerData.Options dopt) { return true; } case R.id.action_speed_read: { - CoolExperimentalStuff.showSpeedReadDialog(getActivity(), _document.getContent()); + CoolExperimentalStuff.showSpeedReadDialog(getActivity(), _hlEditor.getText().toString()); return true; } case R.id.action_wrap_words: { @@ -537,8 +537,10 @@ public void onFsViewerConfig(FilesystemViewerData.Options dopt) { _appSettings.setDocumentFontSize(getPath(), newSize); }); } + default: { + return super.onOptionsItemSelected(item); + } } - return super.onOptionsItemSelected(item); } private long _lastChangedThreadStart = 0; @@ -548,7 +550,6 @@ public void onContentEditValueChanged(CharSequence text) { if ((_lastChangedThreadStart + HISTORY_DELTA) < System.currentTimeMillis()) { _lastChangedThreadStart = System.currentTimeMillis(); _hlEditor.postDelayed(() -> { - _document.setContent(text.toString()); Activity activity = getActivity(); if (activity instanceof AppCompatActivity) { ((AppCompatActivity) activity).supportInvalidateOptionsMenu(); @@ -562,20 +563,6 @@ public void onContentEditValueChanged(CharSequence text) { } - @SuppressWarnings({"ConstantConditions", "ResultOfMethodCallIgnored"}) - private Document loadDocument() { - Document document = DocumentIO.loadDocument(getActivity(), getArguments(), _document); - if (document != null) { - document.setDoHistory(_appSettings.isEditorHistoryEnabled()); - } - if (document.getHistory().isEmpty()) { - document.forceAddNextChangeToHistory(); - document.addToHistory(); - } - - return document; - } - public void applyTextFormat(final int textFormatId) { _textActionsBar.removeAllViews(); _textFormat = TextFormat.getFormat(textFormatId, getActivity(), _document, _hlEditor); @@ -662,7 +649,6 @@ public boolean onBackPressed() { getActivity().getIntent().getBooleanExtra(DocumentActivity.EXTRA_DO_PREVIEW, false) || _appSettings.getDocumentPreviewState(getPath()) || _document.getFile().getName().startsWith("index.")); - saveDocument(); if (_isPreviewVisible && !preview) { setDocumentViewVisibility(false); return true; @@ -676,20 +662,21 @@ public boolean onBackPressed() { return false; } + // Save the file // Only supports java.io.File. TODO: Android Content public boolean saveDocument() { - boolean ret = false; if (isAdded() && _hlEditor != null && _hlEditor.getText() != null) { - ret = DocumentIO.saveDocument(_document, _hlEditor.getText().toString(), _shareUtil, getContext()); - updateLauncherWidgets(); if (_document != null && _document.getFile() != null) { _appSettings.setLastEditPosition(_document.getFile(), _hlEditor.getSelectionStart()); _appSettings.setDocumentPreviewState(getPath(), _isPreviewVisible); } + + updateLauncherWidgets(); + return _document.saveContent(getContext(), _hlEditor.getText().toString(), _shareUtil); } - return ret; + return false; } public void restoreDocumentPositions() { @@ -709,31 +696,22 @@ private boolean isDisplayedAtMainActivity() { @Override public void onSaveInstanceState(@NonNull Bundle outState) { saveDocument(); - if ((_hlEditor.length() * _document.getHistory().size() * 1.05) < 9200 && false) { - outState.putSerializable(SAVESTATE_DOCUMENT, _document); - } - if (getArguments() != null && _document.getFile() != null) { - getArguments().putSerializable(DocumentIO.EXTRA_PATH, _document.getFile()); - getArguments().putSerializable(DocumentIO.EXTRA_PATH_IS_FOLDER, false); - } if (_hlEditor != null) { outState.putSerializable(SAVESTATE_CURSOR_POS, _hlEditor.getSelectionStart()); } + outState.putSerializable(SAVESTATE_DOCUMENT, _document); outState.putBoolean(SAVESTATE_PREVIEW_ON, _isPreviewVisible); - /*SharedPreferences sp = getContext().getSharedPreferences("unforedopref", 0); - _editTextUndoRedoHelper.storePersistentState(sp.edit(), _editTextUndoRedoHelper.undoRedoPrefKeyForFile(_document.getFile())); - outState.putString("undoredopref", "put");*/ super.onSaveInstanceState(outState); } @Override public void onPause() { - super.onPause(); saveDocument(); if (_document != null && _document.getFile() != null) { _appSettings.addRecentDocument(_document.getFile()); } + super.onPause(); } private void updateLauncherWidgets() { @@ -747,7 +725,7 @@ private void updateLauncherWidgets() { public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (isVisibleToUser && isDisplayedAtMainActivity()) { - checkReloadDisk(false); + loadDocument(); } else if (!isVisibleToUser && _document != null) { saveDocument(); } @@ -762,20 +740,6 @@ public void setUserVisibleHint(boolean isVisibleToUser) { } } - private void checkReloadDisk(boolean forceReload) { - if (_firstFileLoad) { - _firstFileLoad = false; - return; - } - Document cmp = DocumentIO.loadDocument(getActivity(), getArguments(), null); - if (forceReload || (_document != null && cmp != null && cmp.getContent() != null && !cmp.getContent().equals(_document.getContent()))) { - _editTextUndoRedoHelper.clearHistory(); - _document = cmp; - loadDocument(); - loadDocumentIntoUi(); - } - } - @Override public void onFragmentFirstTimeVisible() { final boolean initPreview = _appSettings.getDocumentPreviewState(getPath()); @@ -800,8 +764,7 @@ public void setDocumentViewVisibility(boolean show) { _webViewClient.setRestoreScrollY(_webView.getScrollY()); } if (show) { - _document.setContent(_hlEditor.getText().toString()); - _textFormat.getConverter().convertMarkupShowInWebView(_document, _webView, _nextConvertToPrintMode, _document.getFile()); + _textFormat.getConverter().convertMarkupShowInWebView(_document, _hlEditor.getText().toString(), _webView, _nextConvertToPrintMode); new ActivityUtils(getActivity()).hideSoftKeyboard().freeContextRef(); _hlEditor.clearFocus(); _hlEditor.postDelayed(() -> new ActivityUtils(getActivity()).hideSoftKeyboard().freeContextRef(), 300); diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java b/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java index 583a1c6b1..4d39b577b 100644 --- a/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java +++ b/app/src/main/java/net/gsantner/markor/activity/DocumentShareIntoFragment.java @@ -32,7 +32,6 @@ import net.gsantner.markor.ui.hleditor.HighlightingEditor; import net.gsantner.markor.util.AppSettings; import net.gsantner.markor.util.ContextUtils; -import net.gsantner.markor.util.DocumentIO; import net.gsantner.markor.util.PermissionChecker; import net.gsantner.markor.util.ShareUtil; import net.gsantner.opoc.activity.GsFragmentBase; @@ -59,8 +58,8 @@ public static DocumentShareIntoFragment newInstance(Intent intent) { final String sharedText = formatLink(intent.getStringExtra(Intent.EXTRA_SUBJECT), intent.getStringExtra(Intent.EXTRA_TEXT)); - Object intentFile = intent.getSerializableExtra(DocumentIO.EXTRA_PATH); - if (intentFile != null && intent.getBooleanExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, false)) { + Object intentFile = intent.getSerializableExtra(Document.EXTRA_PATH); + if (intentFile != null && intent.getBooleanExtra(Document.EXTRA_PATH_IS_FOLDER, false)) { f.workingDir = (File) intentFile; } @@ -203,12 +202,14 @@ public void setText(String text) { private void appendToExistingDocument(final File file, final String separator, final boolean showEditor) { Bundle args = new Bundle(); - args.putSerializable(DocumentIO.EXTRA_PATH, file); - args.putBoolean(DocumentIO.EXTRA_PATH_IS_FOLDER, false); - Document document = DocumentIO.loadDocument(getContext(), args, null); - String trimmedContent = document.getContent().trim(); + args.putSerializable(Document.EXTRA_PATH, file); + args.putBoolean(Document.EXTRA_PATH_IS_FOLDER, false); + final Context context = getContext(); + final Document document = Document.fromArguments(context, args); + String trimmedContent = document.loadContent(context).trim(); String currentContent = TextUtils.isEmpty(trimmedContent) ? "" : (trimmedContent + "\n"); - DocumentIO.saveDocument(document, currentContent + separator + _sharedText, new ShareUtil(getContext()), getContext()); + document.saveContent(context, currentContent + separator + _sharedText); + if (showEditor) { showInDocumentActivity(document); } diff --git a/app/src/main/java/net/gsantner/markor/activity/openeditor/OpenEditorActivity.java b/app/src/main/java/net/gsantner/markor/activity/openeditor/OpenEditorActivity.java index 039d94386..80dabb90f 100644 --- a/app/src/main/java/net/gsantner/markor/activity/openeditor/OpenEditorActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/openeditor/OpenEditorActivity.java @@ -14,8 +14,8 @@ import net.gsantner.markor.R; import net.gsantner.markor.activity.DocumentActivity; +import net.gsantner.markor.model.Document; import net.gsantner.markor.util.AppSettings; -import net.gsantner.markor.util.DocumentIO; import net.gsantner.opoc.util.ActivityUtils; import net.gsantner.opoc.util.FileUtils; import net.gsantner.opoc.util.PermissionChecker; @@ -26,8 +26,8 @@ public class OpenEditorActivity extends AppCompatActivity { protected void openEditorForFile(File file) { Intent openIntent = new Intent(getApplicationContext(), DocumentActivity.class) .setAction(Intent.ACTION_CALL_BUTTON) - .putExtra(DocumentIO.EXTRA_PATH, file) - .putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, false); + .putExtra(Document.EXTRA_PATH, file) + .putExtra(Document.EXTRA_PATH_IS_FOLDER, false); openActivityAndClose(openIntent, file); } @@ -43,8 +43,8 @@ protected void openActivityAndClose(final Intent openIntent, File file) { if (!file.exists() && !file.isDirectory()) { FileUtils.writeFile(file, ""); } - openIntent.putExtra(DocumentIO.EXTRA_PATH, openIntent.hasExtra(DocumentIO.EXTRA_PATH) ? openIntent.getSerializableExtra(DocumentIO.EXTRA_PATH) : file); - openIntent.putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, openIntent.hasExtra(DocumentIO.EXTRA_PATH_IS_FOLDER) ? openIntent.getSerializableExtra(DocumentIO.EXTRA_PATH_IS_FOLDER) : file.isDirectory()); + openIntent.putExtra(Document.EXTRA_PATH, openIntent.hasExtra(Document.EXTRA_PATH) ? openIntent.getSerializableExtra(Document.EXTRA_PATH) : file); + openIntent.putExtra(Document.EXTRA_PATH_IS_FOLDER, openIntent.hasExtra(Document.EXTRA_PATH_IS_FOLDER) ? openIntent.getSerializableExtra(Document.EXTRA_PATH_IS_FOLDER) : file.isDirectory()); ActivityUtils au = new ActivityUtils(this); au.animateToActivity(openIntent, true, 1); } diff --git a/app/src/main/java/net/gsantner/markor/activity/openeditor/OpenShareIntoActivity.java b/app/src/main/java/net/gsantner/markor/activity/openeditor/OpenShareIntoActivity.java index 4a2325db5..32eea59d6 100644 --- a/app/src/main/java/net/gsantner/markor/activity/openeditor/OpenShareIntoActivity.java +++ b/app/src/main/java/net/gsantner/markor/activity/openeditor/OpenShareIntoActivity.java @@ -14,7 +14,7 @@ import android.support.annotation.Nullable; import net.gsantner.markor.activity.DocumentRelayActivity; -import net.gsantner.markor.util.DocumentIO; +import net.gsantner.markor.model.Document; public class OpenShareIntoActivity extends OpenEditorActivity { @@ -24,7 +24,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { Intent openShare = new Intent(this, DocumentRelayActivity.class) .setAction(Intent.ACTION_SEND) - .putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, true) + .putExtra(Document.EXTRA_PATH_IS_FOLDER, true) .putExtra(Intent.EXTRA_TEXT, ""); openActivityAndClose(openShare, null); } diff --git a/app/src/main/java/net/gsantner/markor/format/TextConverter.java b/app/src/main/java/net/gsantner/markor/format/TextConverter.java index 9afdc65e3..969ab758f 100644 --- a/app/src/main/java/net/gsantner/markor/format/TextConverter.java +++ b/app/src/main/java/net/gsantner/markor/format/TextConverter.java @@ -74,11 +74,11 @@ public abstract class TextConverter { * @param webView The WebView content to be shown in * @return Copy of converted html */ - public String convertMarkupShowInWebView(Document document, WebView webView, boolean isExportInLightMode, File file) { + public String convertMarkupShowInWebView(Document document, String content, WebView webView, boolean isExportInLightMode) { Context context = webView.getContext(); String html; try { - html = convertMarkup(document.getContent(), context, isExportInLightMode, file); + html = convertMarkup(content, context, isExportInLightMode, document.getFile()); } catch (Exception e) { html = "Please report at project issue tracker: " + e.toString(); } diff --git a/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtTextActions.java b/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtTextActions.java index 2065f8439..93d4ee2aa 100644 --- a/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtTextActions.java +++ b/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtTextActions.java @@ -14,6 +14,7 @@ import android.app.DatePickerDialog; import android.content.DialogInterface; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; @@ -28,8 +29,6 @@ import net.gsantner.markor.ui.SearchOrCustomTextDialogCreator; import net.gsantner.markor.ui.hleditor.TextActions; import net.gsantner.markor.util.AppSettings; -import net.gsantner.markor.util.DocumentIO; -import net.gsantner.markor.util.ShareUtil; import net.gsantner.opoc.util.FileUtils; import net.gsantner.opoc.util.StringUtils; @@ -171,7 +170,8 @@ public void onClick(View view) { } case R.string.tmaid_todotxt_archive_done_tasks: { SearchOrCustomTextDialogCreator.showSttArchiveDialog(_activity, (callbackPayload) -> { - // Don't do parse tasks in this case, performance wise + callbackPayload = Document.normalizeFilename(callbackPayload); + final ArrayList keep = new ArrayList<>(); final ArrayList move = new ArrayList<>(); final TodoTxtTask[] allTasks = TodoTxtTask.getAllTasks(_hlEditor.getText()); @@ -191,25 +191,22 @@ public void onClick(View view) { keep.add(task); } } - if (!move.isEmpty()) { - File todoFile = _document.getFile(); - if (todoFile != null && (todoFile.getParentFile().exists() || todoFile.getParentFile().mkdirs())) { - File doneFile = new File(todoFile.getParentFile(), callbackPayload); - String doneFileContents = ""; - if (doneFile.exists() && doneFile.canRead()) { - doneFileContents = FileUtils.readTextFileFast(doneFile).trim() + "\n"; - } - doneFileContents += TodoTxtTask.tasksToString(move) + "\n"; - - // Write to do done file - if (DocumentIO.saveDocument(new Document(doneFile), doneFileContents, new ShareUtil(_activity), getContext())) { - final String tasksString = TodoTxtTask.tasksToString(keep); - _hlEditor.setText(tasksString); - _hlEditor.setSelection( - StringUtils.getIndexFromLineOffset(tasksString, selStart), - StringUtils.getIndexFromLineOffset(tasksString, selEnd) - ); - } + if (!move.isEmpty() && _document.testCreateParent()) { + File doneFile = new File(_document.getFile().getParentFile(), callbackPayload); + String doneFileContents = ""; + if (doneFile.exists() && doneFile.canRead()) { + doneFileContents = FileUtils.readTextFileFast(doneFile).trim() + "\n"; + } + doneFileContents += TodoTxtTask.tasksToString(move) + "\n"; + + // Write to done file + if (new Document(doneFile).saveContent(getContext(), doneFileContents)) { + final String tasksString = TodoTxtTask.tasksToString(keep); + _hlEditor.setText(tasksString); + _hlEditor.setSelection( + StringUtils.getIndexFromLineOffset(tasksString, selStart), + StringUtils.getIndexFromLineOffset(tasksString, selEnd) + ); } } new AppSettings(_activity).setLastTodoUsedArchiveFilename(callbackPayload); @@ -455,6 +452,7 @@ public DateFragment setCalendar(Calendar calendar) { return this; } + @NonNull @Override public DatePickerDialog onCreateDialog(Bundle savedInstanceState) { super.onCreateDialog(savedInstanceState); @@ -472,8 +470,4 @@ public DatePickerDialog onCreateDialog(Bundle savedInstanceState) { return dialog; } } - - private void doBasicHighlights(final Spannable spannable) { - TodoTxtHighlighter.basicTodoTxtHighlights(spannable, true, new TodoTxtHighlighterColors(), _appSettings.isDarkThemeEnabled(), null); - } } diff --git a/app/src/main/java/net/gsantner/markor/model/Document.java b/app/src/main/java/net/gsantner/markor/model/Document.java index b1fedcb21..662f55daf 100644 --- a/app/src/main/java/net/gsantner/markor/model/Document.java +++ b/app/src/main/java/net/gsantner/markor/model/Document.java @@ -9,249 +9,362 @@ #########################################################*/ package net.gsantner.markor.model; +import static java.lang.System.currentTimeMillis; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.RequiresApi; +import android.text.TextUtils; +import android.util.Log; +import android.widget.Toast; + +import net.gsantner.markor.R; +import net.gsantner.markor.activity.MainActivity; import net.gsantner.markor.format.TextFormat; +import net.gsantner.markor.format.markdown.MarkdownTextConverter; +import net.gsantner.markor.util.AppSettings; +import net.gsantner.markor.util.ShareUtil; +import net.gsantner.opoc.util.FileUtils; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.Serializable; -import java.util.ArrayList; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Locale; + +import other.de.stanetz.jpencconverter.JavaPasswordbasedCryption; @SuppressWarnings({"WeakerAccess", "UnusedReturnValue", "unused"}) public class Document implements Serializable { - private final static int MIN_HISTORY_DELAY = 1500; // [ms] - private final static int MAX_HISTORY_SIZE = 5; + private static final int MAX_TITLE_EXTRACTION_LENGTH = 25; + + public static final String EXTRA_DOCUMENT = "EXTRA_DOCUMENT"; // Document + public static final String EXTRA_PATH = "EXTRA_PATH"; // java.io.File + public static final String EXTRA_PATH_IS_FOLDER = "EXTRA_PATH_IS_FOLDER"; // boolean + public static final String EXTRA_FILE_LINE_NUMBER = "EXTRA_FILE_LINE_NUMBER"; // int + private final File _file; + private final String _fileExtension; private int _format = TextFormat.FORMAT_UNKNOWN; - private ArrayList _history = new ArrayList<>(); - private File _file = null; // Full filepath (path + filename + extension) - private String _title = ""; // The title of the document. May lead to a rename at save - private String _fileExtension = ""; // Not versioned. folder(path) / title + ext - private String _content = ""; - private boolean _doHistory = true; - private int _historyPosition = 0; - private long _lastChanged = 0; - private long _modTime = 0; - private boolean _forceNoHistory = false; + private String _title = ""; + private long _modTime = 0; // Modtime as of when the file was last loaded / written private int _initialLineNumber = -1; - - public Document() { - } + private String _lastHash = null; public Document(File file) { _file = file; + final String name = _file.getName(); + final int doti = name.lastIndexOf("."); + if (doti < 0) { + _fileExtension = ""; + _title = name; + } else { + _fileExtension = name.substring(doti).toLowerCase(); + _title = name.substring(0, doti); + } + + // Set initial format + final String fnlower = getFile().getName().toLowerCase(); + if (TextFormat.CONVERTER_TODOTXT.isFileOutOfThisFormat(fnlower)) { + setFormat(TextFormat.FORMAT_TODOTXT); + } else if (TextFormat.CONVERTER_KEYVALUE.isFileOutOfThisFormat(fnlower)) { + setFormat(TextFormat.FORMAT_KEYVALUE); + } else if (TextFormat.CONVERTER_MARKDOWN.isFileOutOfThisFormat(fnlower)) { + setFormat(TextFormat.FORMAT_MARKDOWN); + } else if (TextFormat.CONVERTER_ZIMWIKI.isFileOutOfThisFormat(getPath())) { + setFormat(TextFormat.FORMAT_ZIMWIKI); + } else { + setFormat(TextFormat.FORMAT_PLAIN); + } + } + + public String getPath() { + return getPath(this); } public static String getPath(final Document document) { if (document != null) { final File file = document.getFile(); if (file != null) { - return file.getPath(); + return file.getAbsolutePath(); } } return null; } - public synchronized Document cloneDocument() { - return fromDocumentToDocument(this, new Document()); + public File getFile() { + return _file; } - public synchronized Document loadFromDocument(Document source) { - return fromDocumentToDocument(source, this); + public String getTitle() { + return _title; } - public synchronized static Document fromDocumentToDocument(Document source, Document target) { - target.setDoHistory(false); - target.setFile(source.getFile()); - target.setTitle(source.getTitle()); - target.setContent(source.getContent()); - target.setFormat(source.getFormat()); - target.setModTime(source.getModTime()); - target.setDoHistory(true); - return target; + public void setTitle(String title) { + _title = title == null ? "" : title; } - public synchronized boolean canGoToEarlierVersion() { - // Position 5, History is 5 big, yes - // Position 3, History is 5 big, yes - // Position 0, History is 5 big, no - // Position 0, History is 0 big, no - return _historyPosition > 0 && _history.size() > 0; + public String getName() { + return getFile().getName(); } - public synchronized boolean canGoToNewerVersion() { - // Position 5, History is 5 big, no - // Position 3, History is 5 big, yes - // Position 0, History is 5 big, yes - // Position 0, History is 0 big, no - return _historyPosition < _history.size() - 1; + public void setInitialLineNumber(int num) { + _initialLineNumber = num; } - public synchronized void goToEarlierVersion() { - if (canGoToEarlierVersion()) { - // If we are at the current state, but this was not saved yet -> save current state - if (hasChangesNotInHistory()) { - forceAddNextChangeToHistory(); - addToHistory(); - _historyPosition--; - } + public int getInitialLineNumber() { + return _initialLineNumber; + } - _historyPosition--; - if (_historyPosition >= 0 && _historyPosition < _history.size()) { - loadFromDocument(_history.get(_historyPosition)); - } + @Override + public boolean equals(Object obj) { + if (obj instanceof Document) { + Document other = ((Document) obj); + return equalsc(getFile(), other.getFile()) + && equalsc(getTitle(), other.getTitle()) + && (getFormat() == other.getFormat()); } + return super.equals(obj); } - public boolean hasChangesNotInHistory() { - return _historyPosition == _history.size() && (_history.size() == 0 || !_history.get(_history.size() - 1).equals(this)); + private static boolean equalsc(Object o1, Object o2) { + return (o1 == null && o2 == null) || o1 != null && o1.equals(o2); } - public synchronized void goToNewerVersion() { - if (canGoToNewerVersion()) { - _historyPosition++; - loadFromDocument(_history.get(_historyPosition)); - } + public String getFileExtension() { + return _fileExtension; } - public synchronized void addToHistory() { - if (isDoHistory() && (((_lastChanged + MIN_HISTORY_DELAY) < System.currentTimeMillis()))) { - while (_historyPosition != _history.size() && _history.size() != 0) { - _history.remove(_history.size() - 1); - } - if (_history.size() >= MAX_HISTORY_SIZE) { - _history.remove(2); - _historyPosition--; - } - if (_history.isEmpty() || (!_history.isEmpty() && !_history.get(_history.size() - 1).equals(this))) { - _history.add(cloneDocument()); - _historyPosition++; - _lastChanged = System.currentTimeMillis(); - } - } + public int getFormat() { + return _format; } - public synchronized File getFile() { - return _file; + public void setFormat(int format) { + _format = format; } - public synchronized void setFile(File file) { - if (!equalsc(getFile(), file)) { - addToHistory(); - _file = file; - } + public void resetModTime() { + _modTime = 0; } - public synchronized String getTitle() { - return _title; + public long getModTime() { + return _modTime; } - public synchronized void setTitle(String title) { - if (!equalsc(getTitle(), title)) { - addToHistory(); - _title = title; - } + public boolean hasNewerModTime() { + return _file.lastModified() > _modTime; } - public synchronized String getContent() { - return _content; + public static boolean isEncrypted(File file) { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && file.getName().endsWith(JavaPasswordbasedCryption.DEFAULT_ENCRYPTION_EXTENSION); } - public synchronized void setContent(String content) { - if (!equalsc(getContent(), content)) { - addToHistory(); - _content = content; - } + public boolean isEncrypted() { + return isEncrypted(getFile()); } - public synchronized Document getInitialVersion() { - if (hasChangesNotInHistory()) { - boolean history = isDoHistory(); - setDoHistory(true); - addToHistory(); - setDoHistory(history); + // Try several fallbacks to get a valid file + private static File getValidFile(Context context, Bundle arguments) { + File file = (File) arguments.getSerializable(EXTRA_PATH); + + final File notebook = new AppSettings(context).getNotebookDirectory(); + + // Default to notebook if null + file = (file == null) ? notebook : file; + + // Default to notebook if IS_FOLDER conflicts + final boolean isFolder = arguments.getBoolean(EXTRA_PATH_IS_FOLDER, false); + file = (isFolder && file.exists() && !file.isDirectory()) ? notebook : file; + + // Default to notebook if could not create directory + file = ((isFolder || file.isDirectory()) && !file.exists() && !file.mkdirs()) ? notebook : file; + + // Try to + if (file.isDirectory()) { + final String content = arguments.getString(Intent.EXTRA_TEXT); + File temp = new File(file, filenameFromContent(content) + MarkdownTextConverter.EXT_MARKDOWN__TXT); + while (temp.exists()) { + temp = new File(file, getFileNameWithTimestamp(true)); + } + return temp; } - return _history.size() == 0 ? this : _history.get(0); + + return file; } - @Override - public boolean equals(Object obj) { - if (obj instanceof Document) { - Document other = ((Document) obj); - return equalsc(getFile(), other.getFile()) - && equalsc(getTitle(), other.getTitle()) - && equalsc(getContent(), other.getContent()); + public static Document fromArguments(Context context, Bundle arguments) { + + // When called directly with a document + if (arguments.containsKey(EXTRA_DOCUMENT)) { + return (Document) arguments.getSerializable(EXTRA_DOCUMENT); } - return super.equals(obj); - } - private static boolean equalsc(Object o1, Object o2) { - return (o1 == null && o2 == null) || o1 != null && o1.equals(o2); - } + Document document = new Document(getValidFile(context, arguments)); - // - // - // + if (arguments.containsKey(EXTRA_FILE_LINE_NUMBER)) { + final int lineNumber = arguments.getInt(EXTRA_FILE_LINE_NUMBER); + document.setInitialLineNumber(lineNumber); + } - public boolean isDoHistory() { - return _doHistory && !_forceNoHistory; - } + return document; + } + + public synchronized String loadContent(final Context context) { + + String content; + final char[] pw; + if (isEncrypted() && (pw = getPasswordWithWarning(context)) != null) { + try { + final byte[] encryptedContext = FileUtils.readCloseStreamWithSize(new FileInputStream(getFile()), (int) getFile().length()); + if (encryptedContext.length > JavaPasswordbasedCryption.Version.NAME_LENGTH) { + content = JavaPasswordbasedCryption.getDecryptedText(encryptedContext, pw); + } else { + content = new String(encryptedContext, StandardCharsets.UTF_8); + } + } catch (FileNotFoundException e) { + Log.e(Document.class.getName(), "loadDocument: File " + getFile() + " not found."); + content = ""; + } catch (JavaPasswordbasedCryption.EncryptionFailedException | IllegalArgumentException e) { + Toast.makeText(context, R.string.could_not_decrypt_file_content_wrong_password_or_is_the_file_maybe_not_encrypted, Toast.LENGTH_LONG).show(); + Log.e(Document.class.getName(), "loadDocument: decrypt failed for File " + getFile() + ". " + e.getMessage(), e); + content = ""; + } + } else { + content = FileUtils.readTextFileFast(getFile()); + } - public void setDoHistory(boolean doHistory) { - _doHistory = doHistory; - } + if (MainActivity.IS_DEBUG_ENABLED) { + AppSettings.appendDebugLog( + "\n\n\n--------------\nLoaded document, filepattern " + + getName().replaceAll(".*\\.", "-") + + ", chars: " + content.length() + " bytes:" + content.getBytes().length + + "(" + FileUtils.getReadableFileSize(content.getBytes().length, true) + + "). Language >" + Locale.getDefault().toString() + + "<, Language override >" + AppSettings.get().getLanguage() + "<"); + } - public ArrayList getHistory() { - return _history; - } + _modTime = _file.lastModified(); - public void setHistory(ArrayList history) { - _history = history; + return content; } - public int getHistoryPosition() { - return _historyPosition; + @RequiresApi(api = Build.VERSION_CODES.M) + private static char[] getPasswordWithWarning(final Context context) { + final char[] pw = new AppSettings(context).getDefaultPassword(); + if (pw == null || pw.length == 0) { + final String warningText = context.getString(R.string.no_password_set_cannot_encrypt_decrypt); + Toast.makeText(context, warningText, Toast.LENGTH_LONG).show(); + Log.w(Document.class.getName(), warningText); + return null; + } + return pw; } - public void setHistoryPosition(int historyPosition) { - _historyPosition = historyPosition; + public boolean testCreateParent() { + return testCreateParent(_file); } - public void forceAddNextChangeToHistory() { - _lastChanged = 0; + public boolean saveContent(final Context context, final String content) { + return saveContent(context, content, null); } - public String getFileExtension() { - if (_fileExtension == null && _file != null) { - _fileExtension = (_file.getName().contains(".") ? _file.getName().substring(_file.getName().lastIndexOf(".")) : "").toLowerCase(); + public static boolean testCreateParent(final File file) { + try { + final File parent = file.getParentFile(); + return parent != null && (parent.exists() || parent.mkdirs()); + } catch (NullPointerException e) { + return false; } - return _fileExtension; } - public long getLastChanged() { - return _lastChanged; - } + public synchronized boolean saveContent(final Context context, final String content, ShareUtil shareUtil) { + if (!testCreateParent()) { + return false; + } + shareUtil = shareUtil != null ? shareUtil : new ShareUtil(context); - public int getFormat() { - return _format; - } + final String newHash = FileUtils.sha512sum(content.getBytes()); - public void setFormat(int format) { - _format = format; - } + // Don't write if content same and file hasn't changed + if (newHash != null && newHash.equals(_lastHash) && !hasNewerModTime()) { + return true; + } - public long getModTime() { - return _modTime; + boolean success; + try { + final char[] pw; + final byte[] contentAsBytes; + if (isEncrypted() && (pw = getPasswordWithWarning(context)) != null) { + contentAsBytes = new JavaPasswordbasedCryption(Build.VERSION.SDK_INT, new SecureRandom()).encrypt(content, pw); + } else { + contentAsBytes = content.getBytes(); + } + + if (shareUtil.isUnderStorageAccessFolder(_file)) { + shareUtil.writeFile(_file, false, (fileOpened, fos) -> { + try { + fos.write(contentAsBytes); + fos.flush(); + } catch (Exception ignored) { + } + }); + success = true; + } else { + success = FileUtils.writeFile(getFile(), contentAsBytes); + } + } catch (JavaPasswordbasedCryption.EncryptionFailedException e) { + Log.e(Document.class.getName(), "writeContent: encrypt failed for File " + getPath() + ". " + e.getMessage(), e); + Toast.makeText(context, R.string.could_not_encrypt_file_content_the_file_was_not_saved, Toast.LENGTH_LONG).show(); + success = false; + } + + if (success) { + _lastHash = newHash; + _modTime = _file.lastModified(); // Should be == now + } + + return success; } - public void setModTime(long modTime) { - _modTime = modTime; + public static String getMaskedContent(final String text) { + final String httpToken = "§$§$§$§$"; + return text + .replace("http://", httpToken) + .replace("https://", httpToken) + .replaceAll("\\w", "a") + .replace(httpToken, "https://"); } - public void setInitialLineNumber(final int lineNumber) { - _initialLineNumber = lineNumber; + public static String normalizeFilename(final String name) { + if (TextUtils.isEmpty(name.trim())) { + return getFileNameWithTimestamp(false); + } else { + return name.replaceAll("[\\\\/:\"´`'*$?<>\n\r@|#]+", "").trim(); + } } - public int getInitialLineNumber() { - return _initialLineNumber; + public static String filenameFromContent(final String content) { + if (!TextUtils.isEmpty(content)) { + final String contentL1 = content.split("\n")[0]; + if (contentL1.length() < MAX_TITLE_EXTRACTION_LENGTH) { + return contentL1; + } else { + return contentL1.substring(0, MAX_TITLE_EXTRACTION_LENGTH); + } + } else { + return getFileNameWithTimestamp(false); + } } + // Convenient wrapper + private static String getFileNameWithTimestamp(boolean includeExt) { + final String prefix = Resources.getSystem().getString(R.string.document); + final String ext = includeExt ? MarkdownTextConverter.EXT_MARKDOWN__TXT : ""; + return ShareUtil.getFilenameWithTimestamp(prefix, null, ext); + } } diff --git a/app/src/main/java/net/gsantner/markor/ui/NewFileDialog.java b/app/src/main/java/net/gsantner/markor/ui/NewFileDialog.java index 97f6473e5..35394789d 100644 --- a/app/src/main/java/net/gsantner/markor/ui/NewFileDialog.java +++ b/app/src/main/java/net/gsantner/markor/ui/NewFileDialog.java @@ -31,6 +31,7 @@ import net.gsantner.markor.R; import net.gsantner.markor.format.todotxt.TodoTxtTask; import net.gsantner.markor.format.zimwiki.ZimWikiTextActions; +import net.gsantner.markor.model.Document; import net.gsantner.markor.util.AppSettings; import net.gsantner.markor.util.ShareUtil; import net.gsantner.opoc.ui.AndroidSpinnerOnItemSelectedAdapter; @@ -166,7 +167,7 @@ private AlertDialog.Builder makeDialog(final File basedir, final boolean allowCr appSettings.setNewFileDialogLastUsedExtension(fileExtEdit.getText().toString().trim()); final String usedFilename = getFileNameWithoutExtension(fileNameEdit.getText().toString(), templateSpinner.getSelectedItemPosition()); - final File f = new File(basedir, usedFilename.trim() + fileExtEdit.getText().toString().trim()); + final File f = new File(basedir, Document.normalizeFilename(usedFilename.trim()) + fileExtEdit.getText().toString().trim()); final byte[] templateContents = getTemplateContent(templateSpinner, basedir, f.getName(), encryptCheckbox.isChecked()); shareUtil.writeFile(f, false, (arg_ok, arg_fos) -> { try { diff --git a/app/src/main/java/net/gsantner/markor/util/DocumentIO.java b/app/src/main/java/net/gsantner/markor/util/DocumentIO.java deleted file mode 100644 index a7694669d..000000000 --- a/app/src/main/java/net/gsantner/markor/util/DocumentIO.java +++ /dev/null @@ -1,280 +0,0 @@ -/*####################################################### - * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ - * - * License of this file: Apache 2.0 (Commercial upon request) - * https://www.apache.org/licenses/LICENSE-2.0 - * -#########################################################*/ -package net.gsantner.markor.util; - -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; -import android.text.InputFilter; -import android.text.Spanned; -import android.text.TextUtils; -import android.util.Log; -import android.widget.Toast; - -import net.gsantner.markor.R; -import net.gsantner.markor.activity.MainActivity; -import net.gsantner.markor.format.TextFormat; -import net.gsantner.markor.format.markdown.MarkdownTextConverter; -import net.gsantner.markor.model.Document; -import net.gsantner.opoc.util.FileUtils; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.util.Locale; -import java.util.UUID; - -import other.de.stanetz.jpencconverter.JavaPasswordbasedCryption; - -public class DocumentIO { - public static final String EXTRA_DOCUMENT = "EXTRA_DOCUMENT"; // Document - public static final String EXTRA_PATH = "EXTRA_PATH"; // java.io.File - public static final String EXTRA_PATH_IS_FOLDER = "EXTRA_PATH_IS_FOLDER"; // boolean - public static final String EXTRA_FILE_LINE_NUMBER = "EXTRA_FILE_LINE_NUMBER"; // int - - public static final int MAX_TITLE_EXTRACTION_LENGTH = 25; - public static boolean SAVE_IGNORE_EMTPY_NEXT_TIME = false; - - public static Document loadDocument(Context context, Intent arguments, @Nullable Document existingDocument) { - if (existingDocument != null) { - return existingDocument; - } - - Bundle bundle = new Bundle(); - if (arguments.hasExtra(EXTRA_DOCUMENT)) { - bundle.putSerializable(EXTRA_DOCUMENT, arguments.getSerializableExtra(EXTRA_DOCUMENT)); - } else { - bundle.putSerializable(EXTRA_PATH, arguments.getSerializableExtra(EXTRA_PATH)); - bundle.putBoolean(EXTRA_PATH_IS_FOLDER, arguments.getBooleanExtra(EXTRA_PATH_IS_FOLDER, false)); - } - return loadDocument(context, bundle, existingDocument); - } - - @SuppressWarnings({"ResultOfMethodCallIgnored"}) - public static synchronized Document loadDocument(Context context, Bundle arguments, @Nullable Document existingDocument) { - if (existingDocument != null) { - return existingDocument; - } - - // When called directly from a filepath - if (arguments.containsKey(EXTRA_DOCUMENT)) { - return (Document) arguments.getSerializable(EXTRA_DOCUMENT); - } - - Document document = new Document(); - document.setDoHistory(false); - File extraPath = (File) arguments.getSerializable(EXTRA_PATH); - File filePath = extraPath; - - // Generate random not existing filepath if filename not specified - boolean extraPathIsFolder = arguments.getBoolean(EXTRA_PATH_IS_FOLDER); - if (extraPathIsFolder) { - extraPath.mkdirs(); - while (filePath.exists()) { - filePath = new File(extraPath, String.format("%s-%s%s", context.getString(R.string.document), UUID.randomUUID().toString(), MarkdownTextConverter.EXT_MARKDOWN__MD)); - } - } else if (filePath.isFile() && filePath.canRead()) { - // Extract content and title - document.setTitle(filePath.getName()); - String content; - final char[] pw; - if (isEncryptedFile(filePath) && (pw = getPasswordWithWarning(context)) != null) { - try { - final byte[] encryptedContext = FileUtils.readCloseStreamWithSize(new FileInputStream(filePath), (int) filePath.length()); - if (encryptedContext.length > JavaPasswordbasedCryption.Version.NAME_LENGTH) { - content = JavaPasswordbasedCryption.getDecryptedText(encryptedContext, pw); - } else { - content = new String(encryptedContext, StandardCharsets.UTF_8); - } - } catch (FileNotFoundException e) { - Log.e(DocumentIO.class.getName(), "loadDocument: File " + filePath + " not found."); - content = ""; - } catch (JavaPasswordbasedCryption.EncryptionFailedException | IllegalArgumentException e) { - Toast.makeText(context, R.string.could_not_decrypt_file_content_wrong_password_or_is_the_file_maybe_not_encrypted, Toast.LENGTH_LONG).show(); - Log.e(DocumentIO.class.getName(), "loadDocument: decrypt failed for File " + filePath + ". " + e.getMessage(), e); - content = ""; - } - } else { - content = FileUtils.readTextFileFast(filePath); - } - document.setContent(content); - document.setModTime(filePath.lastModified()); - } - - document.setFile(filePath); - - if (document.getFormat() == TextFormat.FORMAT_UNKNOWN) { - String fnlower = document.getFile().getName().toLowerCase(); - document.setFormat(TextFormat.FORMAT_PLAIN); - - if (TextFormat.CONVERTER_TODOTXT.isFileOutOfThisFormat(fnlower)) { - document.setFormat(TextFormat.FORMAT_TODOTXT); - if (!TextUtils.isEmpty(document.getContent())) { - document.setContent(document.getContent().trim()); - } - } else if (TextFormat.CONVERTER_KEYVALUE.isFileOutOfThisFormat(fnlower)) { - document.setFormat(TextFormat.FORMAT_KEYVALUE); - } else if (TextFormat.CONVERTER_MARKDOWN.isFileOutOfThisFormat(fnlower)) { - document.setFormat(TextFormat.FORMAT_MARKDOWN); - } else if (TextFormat.CONVERTER_ZIMWIKI.isFileOutOfThisFormat(filePath.getAbsolutePath())) { - document.setFormat(TextFormat.FORMAT_ZIMWIKI); - } else { - document.setFormat(TextFormat.FORMAT_PLAIN); - } - } - - String title; - if ((title = document.getTitle()).contains(".")) { - int lastIndexOfDot = title.lastIndexOf("."); - document.setTitle(title.substring(0, lastIndexOfDot)); - } - - document.setDoHistory(true); - if (MainActivity.IS_DEBUG_ENABLED) { - String c = document.getContent(); - AppSettings.appendDebugLog("\n\n\n--------------\nLoaded document, filepattern " + document.getFile().getName().replaceAll(".*\\.", "-") + ", chars: " + c.length() + " bytes:" + c.getBytes().length + "(" + FileUtils.getReadableFileSize(c.getBytes().length, true) + "). Language >" + Locale.getDefault().toString() + "<, Language override >" + AppSettings.get().getLanguage() + "<"); - } - - if (arguments.containsKey(EXTRA_FILE_LINE_NUMBER)) { - final int lineNumber = arguments.getInt(EXTRA_FILE_LINE_NUMBER); - document.setInitialLineNumber(lineNumber); - } - - return document; - } - - public static synchronized boolean saveDocument(final Document document, final String text, final ShareUtil shareUtil, Context context) { - if (text == null || (!SAVE_IGNORE_EMTPY_NEXT_TIME && text.trim().isEmpty() && text.length() < ShareUtil.MIN_OVERWRITE_LENGTH)) { - return false; - } - boolean ret; - String filename = DocumentIO.normalizeTitleForFilename(document, text) + document.getFileExtension(); - document.setDoHistory(true); - document.setFile(new File(document.getFile().getParentFile(), filename)); - - Document documentInitial = document.getInitialVersion(); - - document.setFile(documentInitial.getFile()); - - if (!text.equals(documentInitial.getContent())) { - ret = writeContent(document, text, shareUtil, context); - } else { - ret = true; - } - return ret; - } - - private static boolean writeContent(Document document, String text, ShareUtil shareUtil, Context context) { - boolean ret; - document.forceAddNextChangeToHistory(); - document.setContent(text + (!TextUtils.isEmpty(text) && !text.endsWith("\n") ? "\n" : "")); - - // Create parent (=folder of file) if not exists - if (!document.getFile().getParentFile().exists()) { - //noinspection ResultOfMethodCallIgnored - document.getFile().getParentFile().mkdirs(); - } - try { - final char[] pw; - final byte[] contentAsBytes; - if (isEncryptedFile(document.getFile()) && (pw = getPasswordWithWarning(context)) != null) { - contentAsBytes = new JavaPasswordbasedCryption(Build.VERSION.SDK_INT, new SecureRandom()).encrypt(document.getContent(), pw); - } else { - contentAsBytes = document.getContent().getBytes(); - } - - if (shareUtil.isUnderStorageAccessFolder(document.getFile())) { - shareUtil.writeFile(document.getFile(), false, (fileOpened, fos) -> { - try { - fos.write(contentAsBytes); - } catch (Exception ignored) { - } - }); - ret = true; - } else { - ret = FileUtils.writeFile(document.getFile(), contentAsBytes); - } - } catch (JavaPasswordbasedCryption.EncryptionFailedException e) { - Log.e(DocumentIO.class.getName(), "writeContent: encrypt failed for File " + - document.getFile().getAbsolutePath() + ". " + e.getMessage(), e); - Toast.makeText(context, R.string.could_not_encrypt_file_content_the_file_was_not_saved, Toast.LENGTH_LONG).show(); - ret = false; - } - return ret; - } - - public static String getMaskedContent(Document document) { - String text = document.getContent().toLowerCase(); - String httpToken = "§$§$§$§$"; - text = text.replace("http://", httpToken).replace("https://", httpToken); - text = text.replaceAll("\\w", "a"); - text = text.replace(httpToken, "https://"); - return text; - } - - public static String normalizeTitleForFilename(Document _document, String currentContent) { - String name = _document.getTitle(); - try { - if (name.length() == 0) { - if (currentContent.length() == 0) { - return null; - } else { - String contentL1 = currentContent.split("\n")[0]; - if (contentL1.length() < MAX_TITLE_EXTRACTION_LENGTH) { - name = contentL1; - } else { - name = contentL1.substring(0, MAX_TITLE_EXTRACTION_LENGTH); - } - } - } - name = name.replaceAll("[\\\\/:\"´`'*$?<>\n\r@|#]+", "").trim(); - } catch (Exception ignored) { - } - if (name == null || name.isEmpty()) { - name = "Note " + UUID.randomUUID().toString(); - } - return name; - } - - public static final InputFilter INPUT_FILTER_FILESYSTEM_FILENAME = new InputFilter() { - private final String blockCharacterSet = "\\/:\"´`'*?<>\n\r@|"; - - public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { - for (int i = 0; !TextUtils.isEmpty(source) && i < source.length(); i++) { - if (blockCharacterSet.contains(("" + source.charAt(i)))) { - return ""; - } - } - return null; - } - }; - - @RequiresApi(api = Build.VERSION_CODES.M) - private static char[] getPasswordWithWarning(final Context context) { - final char[] pw = new AppSettings(context).getDefaultPassword(); - if (pw == null || pw.length == 0) { - final String warningText = context.getString(R.string.no_password_set_cannot_encrypt_decrypt); - Toast.makeText(context, warningText, Toast.LENGTH_LONG).show(); - Log.w(DocumentIO.class.getName(), warningText); - return null; - } - return pw; - } - - private static boolean isEncryptedFile(File file) { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && file.getName().endsWith(JavaPasswordbasedCryption.DEFAULT_ENCRYPTION_EXTENSION); - } - -} diff --git a/app/src/main/java/net/gsantner/markor/util/MarkorWebViewClient.java b/app/src/main/java/net/gsantner/markor/util/MarkorWebViewClient.java index b64fc743c..2ad5f2ce9 100644 --- a/app/src/main/java/net/gsantner/markor/util/MarkorWebViewClient.java +++ b/app/src/main/java/net/gsantner/markor/util/MarkorWebViewClient.java @@ -20,6 +20,7 @@ import net.gsantner.markor.R; import net.gsantner.markor.activity.DocumentActivity; import net.gsantner.markor.format.TextFormat; +import net.gsantner.markor.model.Document; import java.io.File; import java.net.URLDecoder; @@ -56,7 +57,7 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { } if (TextFormat.isTextFile(file)) { Intent newPreview = new Intent(_activity, DocumentActivity.class); - newPreview.putExtra(DocumentIO.EXTRA_PATH, file); + newPreview.putExtra(Document.EXTRA_PATH, file); newPreview.putExtra(DocumentActivity.EXTRA_DO_PREVIEW, true); _activity.startActivity(newPreview); } else if (file.getName().toLowerCase().endsWith(".apk")) { diff --git a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java index 130021ea3..1b2b456f2 100644 --- a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java @@ -14,9 +14,7 @@ import android.text.TextUtils; import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.File; @@ -28,8 +26,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.net.URLConnection; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.ArrayList; @@ -45,10 +44,17 @@ public class FileUtils { private static final int BUFFER_SIZE = 4096; public static String readTextFileFast(final File file) { - try { - return new String(readCloseStreamWithSize(new FileInputStream(file), (int) file.length())); + try (final FileInputStream inputStream = new FileInputStream(file)) { + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + final byte[] buffer = new byte[1024]; + for (int length; (length = inputStream.read(buffer)) != -1; ) { + result.write(buffer, 0, length); + } + return result.toString("UTF-8"); } catch (FileNotFoundException e) { System.err.println("readTextFileFast: File " + file + " not found."); + } catch (IOException e) { + e.printStackTrace(); } return ""; } @@ -170,46 +176,18 @@ public static byte[] readCloseBinaryStream(final InputStream stream) { return baos.toByteArray(); } - public static boolean writeFile(final File file, byte[] data) { - try { - OutputStream output = null; - try { - output = new BufferedOutputStream(new FileOutputStream(file)); - output.write(data); - output.flush(); - return true; - } finally { - if (output != null) { - output.close(); - } - } + public static boolean writeFile(final File file, final byte[] data) { + try (final FileOutputStream output = new FileOutputStream(file)) { + output.write(data); + return true; } catch (Exception ex) { + ex.printStackTrace(); return false; } } - public static boolean writeFile(final File file, final String content) { - BufferedWriter writer = null; - try { - if (!file.getParentFile().isDirectory() && !file.getParentFile().mkdirs()) - return false; - - writer = new BufferedWriter(new FileWriter(file)); - writer.write(content); - writer.flush(); - return true; - } catch (IOException e) { - e.printStackTrace(); - return false; - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } + public static boolean writeFile(final File file, final String data) { + return writeFile(file, data.getBytes()); } public static boolean copyFile(final File src, final File dst) { @@ -525,4 +503,16 @@ public static File join(File file, String... childSegments) { } return file; } + + public static String sha512sum(final byte[] bytes) { + try { + final StringBuilder sb = new StringBuilder(); + for (final byte b : MessageDigest.getInstance("SHA-512").digest(bytes)) { + sb.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + return null; + } + } } diff --git a/app/src/test/java/net/gsantner/markor/model/DocumentTest.java b/app/src/test/java/net/gsantner/markor/model/DocumentTest.java deleted file mode 100644 index 80db82bf8..000000000 --- a/app/src/test/java/net/gsantner/markor/model/DocumentTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/*####################################################### - * - * Maintained by Gregor Santner, 2017- - * https://gsantner.net/ - * - * License of this file: Apache 2.0 (Commercial upon request) - * https://www.apache.org/licenses/LICENSE-2.0 - * -#########################################################*/ -package net.gsantner.markor.model; - -import net.gsantner.markor.util.DocumentIO; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class DocumentTest { - - @Test - public void documentOlderVersion() { - Document document = new Document(); - document.setTitle("Hello"); - document.forceAddNextChangeToHistory(); - document.setTitle("Hello World"); - document.forceAddNextChangeToHistory(); - document.goToEarlierVersion(); - assertThat(document.getTitle()).isEqualTo("Hello"); - } - - @Test - public void documentNewerVersion() { - Document document = new Document(); - document.setTitle("Hello"); - document.forceAddNextChangeToHistory(); - document.setTitle("Hello World"); - document.forceAddNextChangeToHistory(); - document.setTitle("Hello World Again"); - document.goToEarlierVersion(); - document.goToEarlierVersion(); - assertThat(document.getTitle()).isEqualTo("Hello"); - document.goToNewerVersion(); - assertThat(document.getTitle()).isEqualTo("Hello World"); - assertThat(document.canGoToNewerVersion()).isEqualTo(true); - document.goToNewerVersion(); - assertThat(document.getTitle()).isEqualTo("Hello World Again"); - assertThat(document.canGoToNewerVersion()).isEqualTo(false); - } - - public String normalizeTitleForFilename(Document document) { - return DocumentIO.normalizeTitleForFilename(document, document.getContent().toString()); - } - - @Test - public void filenameNormalization() { - assertThat(normalizeTitleForFilename(nd(null, "HelloWorld"))).isEqualTo("HelloWorld"); - assertThat(normalizeTitleForFilename(nd("HelloWorld", "text"))).isEqualTo("HelloWorld"); - assertThat(normalizeTitleForFilename(nd(null, "text\nnewline"))).isEqualTo("text"); - assertThat(normalizeTitleForFilename(nd(null, "sumtext/folder"))).isEqualTo("sumtextfolder"); - assertThat(normalizeTitleForFilename(nd(null, "## hello world"))).isEqualTo("hello world"); - } - - private Document nd(String title, String content) { - Document document = new Document(); - if (title != null) { - document.setTitle(title); - } - if (content != null) { - document.setContent(content); - } - return document; - } -} diff --git a/app/thirdparty/java/other/writeily/widget/WrFilesWidgetFactory.java b/app/thirdparty/java/other/writeily/widget/WrFilesWidgetFactory.java index 02321eff6..33127c58f 100644 --- a/app/thirdparty/java/other/writeily/widget/WrFilesWidgetFactory.java +++ b/app/thirdparty/java/other/writeily/widget/WrFilesWidgetFactory.java @@ -18,9 +18,9 @@ import net.gsantner.markor.R; import net.gsantner.markor.format.TextFormat; import net.gsantner.markor.format.markdown.MarkdownTextConverter; +import net.gsantner.markor.model.Document; import net.gsantner.markor.ui.FilesystemViewerCreator; import net.gsantner.markor.util.AppSettings; -import net.gsantner.markor.util.DocumentIO; import net.gsantner.opoc.ui.FilesystemViewerAdapter; import net.gsantner.opoc.ui.FilesystemViewerFragment; @@ -38,7 +38,7 @@ public class WrFilesWidgetFactory implements RemoteViewsService.RemoteViewsFacto public WrFilesWidgetFactory(Context context, Intent intent) { _context = context; _appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); - _dir = (File) intent.getSerializableExtra(DocumentIO.EXTRA_PATH); + _dir = (File) intent.getSerializableExtra(Document.EXTRA_PATH); } @Override @@ -98,7 +98,7 @@ public RemoteViews getViewAt(int position) { rowView.setTextColor(R.id.widget_note_title, _context.getResources().getColor(R.color.light__primary_text)); if (position < _widgetFilesList.length) { File file = _widgetFilesList[position]; - Intent fillInIntent = new Intent().putExtra(DocumentIO.EXTRA_PATH, file).putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, file.isDirectory()); + Intent fillInIntent = new Intent().putExtra(Document.EXTRA_PATH, file).putExtra(Document.EXTRA_PATH_IS_FOLDER, file.isDirectory()); rowView.setTextViewText(R.id.widget_note_title, MarkdownTextConverter.MD_EXTENSION_PATTERN.matcher(file.getName()).replaceAll("")); rowView.setOnClickFillInIntent(R.id.widget_note_title, fillInIntent); } diff --git a/app/thirdparty/java/other/writeily/widget/WrMarkorWidgetProvider.java b/app/thirdparty/java/other/writeily/widget/WrMarkorWidgetProvider.java index cd1efda00..f8bd17e6d 100644 --- a/app/thirdparty/java/other/writeily/widget/WrMarkorWidgetProvider.java +++ b/app/thirdparty/java/other/writeily/widget/WrMarkorWidgetProvider.java @@ -22,8 +22,8 @@ import net.gsantner.markor.R; import net.gsantner.markor.activity.DocumentRelayActivity; import net.gsantner.markor.activity.MainActivity; +import net.gsantner.markor.model.Document; import net.gsantner.markor.util.AppSettings; -import net.gsantner.markor.util.DocumentIO; import java.io.File; @@ -74,8 +74,8 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a // ~~~Create new File~~~ Share empty text into markor, easier to access from widget than new file dialog Intent openShare = new Intent(context, DocumentRelayActivity.class) .setAction(Intent.ACTION_SEND) - .putExtra(DocumentIO.EXTRA_PATH, directoryF) - .putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, true) + .putExtra(Document.EXTRA_PATH, directoryF) + .putExtra(Document.EXTRA_PATH_IS_FOLDER, true) .putExtra(Intent.EXTRA_TEXT, ""); views.setOnClickPendingIntent(R.id.widget_new_note, PendingIntent.getActivity(context, requestCode++, openShare, 0)); @@ -86,15 +86,15 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a // Open To-do Intent openTodo = new Intent(context, DocumentRelayActivity.class) .setAction(Intent.ACTION_EDIT) - .putExtra(DocumentIO.EXTRA_PATH, appSettings.getTodoFile()) - .putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, false); + .putExtra(Document.EXTRA_PATH, appSettings.getTodoFile()) + .putExtra(Document.EXTRA_PATH_IS_FOLDER, false); views.setOnClickPendingIntent(R.id.widget_todo, PendingIntent.getActivity(context, requestCode++, openTodo, 0)); // Open QuickNote Intent openQuickNote = new Intent(context, DocumentRelayActivity.class) .setAction(Intent.ACTION_EDIT) - .putExtra(DocumentIO.EXTRA_PATH, appSettings.getQuickNoteFile()) - .putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, false); + .putExtra(Document.EXTRA_PATH, appSettings.getQuickNoteFile()) + .putExtra(Document.EXTRA_PATH_IS_FOLDER, false); views.setOnClickPendingIntent(R.id.widget_quicknote, PendingIntent.getActivity(context, requestCode++, openQuickNote, 0)); // Open Favourites @@ -104,8 +104,8 @@ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] a // ListView Intent notesListIntent = new Intent(context, WrFilesWidgetService.class); notesListIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); - notesListIntent.putExtra(DocumentIO.EXTRA_PATH, directoryF); - notesListIntent.putExtra(DocumentIO.EXTRA_PATH_IS_FOLDER, true); + notesListIntent.putExtra(Document.EXTRA_PATH, directoryF); + notesListIntent.putExtra(Document.EXTRA_PATH_IS_FOLDER, true); notesListIntent.setData(Uri.parse(notesListIntent.toUri(Intent.URI_INTENT_SCHEME))); views.setEmptyView(R.id.widget_list_container, R.id.widget_empty_hint);