diff --git a/app/build.gradle b/app/build.gradle index 3d4e30e6a..acd7bf335 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -140,7 +140,6 @@ dependencies { // UI libs implementation 'com.github.Pixplicity:gene-rate:v1.1.8' implementation 'com.github.AppIntro:AppIntro:6.2.0' - implementation 'com.github.kailash09dabhi:OmRecorder:1.1.5' implementation 'com.github.mertakdut:EpubParser:1.0.95' implementation 'com.github.martin-stone:hsv-alpha-color-picker-android:3.1.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d8ee6a8d6..28b5d48bc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,6 @@ - _insertItem.callback(InsertType.LINK_SEARCH)); buttonPictureCamera.setOnClickListener(b -> _insertItem.callback(InsertType.IMAGE_CAMERA)); buttonPictureGallery.setOnClickListener(v -> _insertItem.callback(InsertType.IMAGE_GALLERY)); - buttonAudioRecord.setOnClickListener(v -> _insertItem.callback(InsertType.AUDIO_RECORDING)); buttonPictureEdit.setOnClickListener(v -> _insertItem.callback(InsertType.IMAGE_EDIT)); dialog.show(); @@ -368,7 +360,7 @@ private static void insertItem( if (GsTextUtils.isNullOrEmpty(nameEdit.getText())) { nameEdit.setText(pathEdit.getText()); } - } else { + } else { if (pathEdit != null) { pathEdit.setText(GsFileUtils.relativePath(currentFile, file)); } @@ -408,7 +400,7 @@ private static void insertItem( } case AUDIO_RECORDING: { if (!cu.requestAudioRecording(activity, insertFileLink)) { - GsAudioRecordOmDialog.showAudioRecordDialog(activity, R.string.record_audio, insertFileLink); + // noop, OM library is outdated and so voice recording feature removed } break; } diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java b/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java index d5ad5ab68..42dec9e89 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java +++ b/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java @@ -169,7 +169,7 @@ private boolean runHighlight(final boolean recompute) { private void updateHighlighting() { if (runHighlight(false)) { // Do not batch as we do not want to reflow - _hl.clearDynamic().applyDynamic(hlRegion()); + _hl.clearDynamic().applyDynamic(hlRegion()); _oldHlRect.set(_hlRect); } } @@ -704,7 +704,12 @@ public void draw(final Canvas canvas) { final int count = layout.getLineCount(); final int offsetY = _editor.getPaddingTop(); for (; i < count; i++) { - final int start = layout.getLineStart(i); + int start; + try { + start = layout.getLineStart(i); + } catch (IndexOutOfBoundsException ex) { + break; // Even though the drawing is against count, might throw IndexOutOfBounds during drawing + } if (start == 0 || text.charAt(start - 1) == '\n') { final int y = layout.getLineBaseline(i); if (y > _lineNumbersArea.bottom) { diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/SyntaxHighlighterBase.java b/app/src/main/java/net/gsantner/markor/frontend/textview/SyntaxHighlighterBase.java index 25de2206b..fdd5a36ba 100644 --- a/app/src/main/java/net/gsantner/markor/frontend/textview/SyntaxHighlighterBase.java +++ b/app/src/main/java/net/gsantner/markor/frontend/textview/SyntaxHighlighterBase.java @@ -375,7 +375,7 @@ public SyntaxHighlighterBase applyStatic() { boolean hasStatic = false; for (final SpanGroup group : _groups) { - if (group.isStatic) { + if (group != null && group.isStatic) { hasStatic = true; _spannable.setSpan(group.span, group.start, group.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } diff --git a/app/src/main/java/net/gsantner/opoc/frontend/GsAudioRecordOmDialog.java b/app/src/main/java/net/gsantner/opoc/frontend/GsAudioRecordOmDialog.java deleted file mode 100644 index 8f38b9bef..000000000 --- a/app/src/main/java/net/gsantner/opoc/frontend/GsAudioRecordOmDialog.java +++ /dev/null @@ -1,226 +0,0 @@ -/*####################################################### - * - * SPDX-FileCopyrightText: 2020-2024 Gregor Santner - * SPDX-License-Identifier: Unlicense OR CC0-1.0 - * - * Written 2020-2024 by Gregor Santner - * To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. - * You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . -#########################################################*/ -package net.gsantner.opoc.frontend; - -import android.Manifest; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.content.pm.PackageManager; -import android.graphics.Color; -import android.media.AudioFormat; -import android.media.MediaPlayer; -import android.media.MediaRecorder; -import android.os.Build; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.StringRes; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; - -import net.gsantner.opoc.util.GsFileUtils; -import net.gsantner.opoc.wrapper.GsCallback; - -import java.io.File; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -import omrecorder.AudioRecordConfig; -import omrecorder.OmRecorder; -import omrecorder.PullTransport; -import omrecorder.PullableSource; -import omrecorder.Recorder; - - -// -// Callback: Called when successfully recorded -// will contain path to file in cache directory. Must be copied to custom location in callback handler -// -// Add to build.gradle: implementation 'com.kailashdabhi:om-recorder:1.1.5' -// Add to manifest: -// -@SuppressWarnings({"ResultOfMethodCallIgnored", "unused"}) -public class GsAudioRecordOmDialog { - public static void showAudioRecordDialog(final Activity activity, @StringRes final int titleResId, final GsCallback.a1 recordFinishedCallbackWithPathToTemporaryFile) { - //////////////////////////////////// - // Request permission in case not granted. Do not show dialog UI in this case - if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.RECORD_AUDIO}, 200); - return; - } - - //////////////////////////////////// - // Init - final String EMOJI_MICROPHONE = "\uD83D\uDD34"; - final String EMOJI_STOP = "⭕";//"\uD83D\uDED1"; - final String EMOJI_RESTART = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? "\uD83D\uDD04" : EMOJI_MICROPHONE; - final String EMOJI_SPEAKER = "\uD83D\uDD0A"; //"\uD83C\uDFA7"; - - final AtomicBoolean isRecording = new AtomicBoolean(); - final AtomicBoolean isRecordSavedOnce = new AtomicBoolean(); - final AtomicReference recorder = new AtomicReference<>(); - final AtomicReference mediaPlayer = new AtomicReference<>(); - final AtomicReference dialog = new AtomicReference<>(); - final AtomicReference startTime = new AtomicReference<>(); - final File TMP_FILE_RECORDING = generateFilename(activity.getCacheDir()); - if (TMP_FILE_RECORDING.exists()) { - TMP_FILE_RECORDING.delete(); - } - - // Record management callbacks - final GsCallback.a2 recorderManager = (cbArgRestart, cbArgStop) -> { - if (cbArgRestart) { - final PullableSource SRC_MICROPHONE = new PullableSource.Default(new AudioRecordConfig.Default(MediaRecorder.AudioSource.MIC, AudioFormat.ENCODING_PCM_16BIT, AudioFormat.CHANNEL_IN_STEREO, 44100)); - recorder.set(OmRecorder.wav(new PullTransport.Default(SRC_MICROPHONE), TMP_FILE_RECORDING)); - recorder.get().startRecording(); - startTime.set(System.currentTimeMillis()); - } else if (cbArgStop) { - try { - recorder.get().stopRecording(); - isRecordSavedOnce.set(true); - - int[] diff = GsFileUtils.getTimeDiffHMS(System.currentTimeMillis(), startTime.get()); - dialog.get().setMessage(String.format(Locale.getDefault(), "%02d:%02d:%02d / %s [.wav]", diff[0], diff[1], diff[2], GsFileUtils.getReadableFileSize(TMP_FILE_RECORDING.length(), true))); - } catch (Exception ignored) { - } - } - }; - - //////////////////////////////////// - // Create UI - final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity); - final LinearLayout layout = new LinearLayout(activity); - layout.setOrientation(LinearLayout.HORIZONTAL); - layout.setGravity(Gravity.CENTER_HORIZONTAL); - final TextView playbackButton = new TextView(activity); - final TextView recordButton = new TextView(activity); - final View sep1 = new View(activity); - sep1.setLayoutParams(new LinearLayout.LayoutParams(100, 1)); - - // Record button - recordButton.setTextColor(Color.BLACK); - recordButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 64); - recordButton.setGravity(Gravity.CENTER_HORIZONTAL); - recordButton.setText(EMOJI_MICROPHONE); - recordButton.setOnClickListener(v -> { - if (isRecording.get()) { - recorderManager.callback(false, true); - } else { - recorderManager.callback(true, false); - } - - // Update state - isRecording.set(!isRecording.get()); - recordButton.setText(isRecording.get() ? EMOJI_STOP : EMOJI_RESTART); - playbackButton.setEnabled(!isRecording.get()); - }); - - final GsCallback.a0 playbackStoppedCallback = () -> { - recordButton.setEnabled(true); - if (mediaPlayer.get() != null) { - mediaPlayer.getAndSet(null).release(); - } - playbackButton.setText(EMOJI_SPEAKER); - }; - - // Play button - playbackButton.setTextColor(Color.BLACK); - playbackButton.setEnabled(false); - playbackButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 64); - playbackButton.setGravity(Gravity.CENTER_HORIZONTAL); - playbackButton.setText(EMOJI_SPEAKER); - playbackButton.setOnClickListener(v -> { - final boolean startPlaybackNow = mediaPlayer.get() == null; - recordButton.setEnabled(false); - playbackButton.setText(startPlaybackNow ? EMOJI_STOP : EMOJI_SPEAKER); - if (startPlaybackNow) { - try { - MediaPlayer player = new MediaPlayer(); - mediaPlayer.set(player); - player.setDataSource(TMP_FILE_RECORDING.getAbsolutePath()); - player.prepare(); - player.start(); - player.setOnCompletionListener(mp -> playbackStoppedCallback.callback()); - player.setLooping(false); - } catch (IOException ignored) { - } - } else { - mediaPlayer.get().stop(); - playbackStoppedCallback.callback(); - } - }); - - //////////////////////////////////// - // Callback for OK & Cancel dialog button - final DialogInterface.OnClickListener dialogOkAndCancelListener = (dialogInterface, dialogButtonCase) -> { - final boolean isSavePressed = (dialogButtonCase == DialogInterface.BUTTON_POSITIVE); - if (isRecording.get() || isRecordSavedOnce.get()) { - try { - recorder.get().stopRecording(); - } catch (Exception ignored) { - } - if (!isSavePressed) { - if (TMP_FILE_RECORDING.exists()) { - TMP_FILE_RECORDING.delete(); - } - } else if (recordFinishedCallbackWithPathToTemporaryFile != null) { - recordFinishedCallbackWithPathToTemporaryFile.callback(TMP_FILE_RECORDING.getAbsolutePath()); - } - } - dialogInterface.dismiss(); - }; - - //////////////////////////////////// - // Tooltip - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - playbackButton.setTooltipText("Play recording / Stop playback"); - recordButton.setTooltipText("Record Audio (Voice Note)"); - } - - //////////////////////////////////// - // Add to layout - layout.addView(playbackButton); - layout.addView(sep1); - layout.addView(recordButton); - - //////////////////////////////////// - // Create & show dialog - dialogBuilder - .setTitle(titleResId) - .setPositiveButton(android.R.string.ok, dialogOkAndCancelListener) - .setNegativeButton(android.R.string.cancel, dialogOkAndCancelListener) - .setMessage("00:00:00 / 0kB [.wav]") - .setView(layout); - dialog.set(dialogBuilder.create()); - Window w; - dialog.get().show(); - if ((w = dialog.get().getWindow()) != null) { - w.setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); - WindowManager.LayoutParams wlp = w.getAttributes(); - wlp.gravity = Gravity.BOTTOM; - w.setAttributes(wlp); - } - } - - public static File generateFilename(final File recordDirectory) { - final String datestr = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.ENGLISH).format(new Date()); - return new File(recordDirectory, datestr + "-record.wav"); - } -} diff --git a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java index 8e6e00409..e665d0547 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/filebrowser/GsFileBrowserListAdapter.java @@ -152,7 +152,9 @@ public Map getVirtualFolders() { } for (final File file : ContextCompat.getExternalFilesDirs(_context, null)) { - //noinspection DataFlowIssue + if (file == null || file.getParentFile() == null) { + continue; + } final File remap = new File(VIRTUAL_STORAGE_ROOT, "AppData (" + file.getParentFile().toString().replace("/", "-").substring(1) + ")"); map.put(remap, file); } @@ -615,6 +617,12 @@ public void onLayoutChange(View v, int l, int t, int r, int b, int ol, int ot, i }); } + private void postScrollToAndFlash(final File file) { + if (_recyclerView != null && file != null) { + _recyclerView.post(() -> scrollToAndFlash(file)); + } + } + /** * Scroll to a file in current folder and flash * @@ -675,6 +683,11 @@ private void loadFolder(final File folder, final File show) { // This function is not called on the main thread, so post to the UI thread private synchronized void _loadFolder(final @NonNull File folder, final @Nullable File toShow) { + + if (_recyclerView == null) { + return; + } + final boolean folderChanged = !folder.equals(_currentFolder); final List newData = new ArrayList<>(); @@ -696,7 +709,6 @@ private synchronized void _loadFolder(final @NonNull File folder, final @Nullabl newData.add(new File(folder, "0")); } - if (folder.equals(VIRTUAL_STORAGE_RECENTS)) { newData.addAll(_dopt.recentFiles); } else if (folder.equals(VIRTUAL_STORAGE_POPULAR)) { @@ -762,10 +774,10 @@ private synchronized void _loadFolder(final @NonNull File folder, final @Nullabl _layoutManager.onRestoreInstanceState(_folderScrollMap.remove(_currentFolder)); } - _recyclerView.post(() -> scrollToAndFlash(toShow)); + postScrollToAndFlash(toShow); }); } else { - _recyclerView.post(() -> scrollToAndFlash(toShow)); + postScrollToAndFlash(toShow); } if (_dopt.listener != null) { @@ -773,7 +785,7 @@ private synchronized void _loadFolder(final @NonNull File folder, final @Nullabl } }); } else { - _recyclerView.post(() -> scrollToAndFlash(toShow)); + postScrollToAndFlash(toShow); } } diff --git a/app/src/main/java/net/gsantner/opoc/frontend/textview/TextViewUndoRedo.java b/app/src/main/java/net/gsantner/opoc/frontend/textview/TextViewUndoRedo.java index f2c9cfc86..9a4f6c254 100644 --- a/app/src/main/java/net/gsantner/opoc/frontend/textview/TextViewUndoRedo.java +++ b/app/src/main/java/net/gsantner/opoc/frontend/textview/TextViewUndoRedo.java @@ -148,7 +148,12 @@ public void undo() { final int end = start + (edit.after != null ? edit.after.length() : 0); mIsUndoOrRedo = true; - text.replace(start, end, edit.before); + try { + text.replace(start, end, edit.before); + } catch (Exception ex){ + // In case a undo would crash the app, don't do it instead + return; + } mIsUndoOrRedo = false; // This will get rid of underlines inserted when editor tries to come diff --git a/app/src/main/res/layout/select_path_dialog.xml b/app/src/main/res/layout/select_path_dialog.xml index 70830d0b9..ff66f295b 100644 --- a/app/src/main/res/layout/select_path_dialog.xml +++ b/app/src/main/res/layout/select_path_dialog.xml @@ -93,15 +93,6 @@ android:drawableLeft="@drawable/ic_crop_black_24dp" android:visibility="gone" android:text="@string/edit_picture" /> - -