From 9fc71fed6d71fac6dbf80bbfe363c0e059f1d381 Mon Sep 17 00:00:00 2001 From: pweaver Date: Sun, 3 Sep 2017 17:17:03 -0700 Subject: [PATCH] Add option to navigate by sentence. Adding a setting to toggle between navigating by sentence and navigating by paragraph. The default is paragraph, which matches the previous intended behavior. Opening any setting crashed the app because of a ClassCastException from FrameLayout to LinearLayout. I changed the cast to ViewGroup, as the code didn't actually depend on anything more specific. Navigation by paragraph was unreliable when the app was reading because the TTS callback would happen on other threads while navigation happened. I've reposted the TTS callbacks to the main thread to prevent threads interfering with each other. Issue #33 is an example of this problem. Callbacks from TTS also didn't distinguish between utterances that had been read and utterances whose reading had been canceled. I've added logic to track which utterances are currently active, and ignore callback for those that have been cancelled. I now find that both types of navigation now work reliably. --- .../main/assets/resources/application/en.xml | 4 + .../benetech/FBReaderWithNavigationBar.java | 156 +++++++++++++++--- .../preferences/PreferenceActivity.java | 1 + .../preferences/ZLPreferenceActivity.java | 4 +- .../fbreader/fbreader/FBReaderApp.java | 3 + 5 files changed, 140 insertions(+), 28 deletions(-) diff --git a/FBReader/src/main/assets/resources/application/en.xml b/FBReader/src/main/assets/resources/application/en.xml index 8b56df0c443..34a1527c17c 100644 --- a/FBReader/src/main/assets/resources/application/en.xml +++ b/FBReader/src/main/assets/resources/application/en.xml @@ -483,6 +483,10 @@ + + + + diff --git a/FBReader/src/main/java/org/geometerplus/android/fbreader/benetech/FBReaderWithNavigationBar.java b/FBReader/src/main/java/org/geometerplus/android/fbreader/benetech/FBReaderWithNavigationBar.java index a624fdcf384..c7eaef67c47 100644 --- a/FBReader/src/main/java/org/geometerplus/android/fbreader/benetech/FBReaderWithNavigationBar.java +++ b/FBReader/src/main/java/org/geometerplus/android/fbreader/benetech/FBReaderWithNavigationBar.java @@ -45,15 +45,18 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.StringTokenizer; public class FBReaderWithNavigationBar extends FBReaderWithPinchZoom implements TextToSpeech.OnInitListener, TextToSpeech.OnUtteranceCompletedListener, SimpleGestureFilter.SimpleGestureListener, AsyncResponse { private static final String LOG_TAG ="FBRsWithNavigationBar"; private static final String ACTIVITY_RESUMING_STATE ="ACTIVITY_RESUMING_STATE"; + private static final int PARAGRAPH_ID_SHIFT = 16; private ApiServerImplementation myApi; private TextToSpeech myTTS; private int myParagraphIndex = -1; @@ -87,16 +90,18 @@ public class FBReaderWithNavigationBar extends FBReaderWithPinchZoom implements private TtsSentenceExtractor.SentenceIndex mySentences[] = new TtsSentenceExtractor.SentenceIndex[0]; private static int myCurrentSentence = 0; - private static final String UTTERANCE_ID = "GoReadTTS"; - private static HashMap myCallbackMap; private volatile int myInitializationStatus; private final static int TTS_INITIALIZED = 2; private final static int FULLY_INITIALIZED = TTS_INITIALIZED; private volatile PowerManager.WakeLock myWakeLock; private static final String IS_FIRST_TIME_RUNNING_PREFERENCE_TAG = "first_time_running"; + private final Set utterancesInProgress = new HashSet<>(); + private final Handler handler = new Handler(Looper.getMainLooper()); + private boolean activityResuming = false; // this means that the activity is resuming from being in background or orientation change and not created fresh private boolean isFirstTimeRunningApp; + private static int idCounter = 0; static { initCompatibility(); } @@ -110,6 +115,27 @@ private static void initCompatibility() { } } + private static String makeStringIdForParagraphAndSentence( + int paragraphIndex, int sentenceNumber) { + idCounter++; + long longId = (((long) idCounter) << 32) + | (paragraphIndex << PARAGRAPH_ID_SHIFT) | sentenceNumber; + + return Long.toString(longId); + } + + private static int sentenceNumberFromId(String stringId) { + long longId = Long.parseLong(stringId); + int intId = (int) longId; + return intId & ((1 << PARAGRAPH_ID_SHIFT) - 1); + } + + private static int paragraphNumberFromId(String stringId) { + long longId = Long.parseLong(stringId); + int intId = (int) longId; + return intId >> PARAGRAPH_ID_SHIFT; + } + @Override public void onCreate(Bundle savedInstanceState) { accessibilityManager = (AccessibilityManager) getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE); @@ -142,7 +168,11 @@ public void onClick(View v) { setListener(R.id.navigation_bar_skip_previous, new View.OnClickListener() { public void onClick(View v) { ((ZLAndroidApplication) getApplication()).trackGoogleAnalyticsEvent(Analytics.EVENT_CATEGORY_UI, Analytics.EVENT_ACTION_BUTTON, Analytics.EVENT_LABEL_PREV); - goBackward(); + if (fbReader.NavigateBySentenceOption.getValue()) { + goBackwardOneSentence(); + } else { + goBackwardOneParagraph(); + } } }); @@ -158,7 +188,11 @@ public void onFocusChange(android.view.View view, boolean b) { setListener(R.id.navigation_bar_skip_next, new View.OnClickListener() { public void onClick(View v) { ((ZLAndroidApplication) getApplication()).trackGoogleAnalyticsEvent(Analytics.EVENT_CATEGORY_UI, Analytics.EVENT_ACTION_BUTTON, Analytics.EVENT_LABEL_NEXT); - goForward(); + if (fbReader.NavigateBySentenceOption.getValue()) { + goForwardOneSentence(); + } else { + goForwardOneParagraph(); + } } }); @@ -173,10 +207,6 @@ public void onFocusChange(android.view.View view, boolean b) { setActive(false); - if (myCallbackMap == null) { - myCallbackMap = new HashMap(); - myCallbackMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, UTTERANCE_ID); - } myApi = new ApiServerImplementation(); try { startActivityForResult(new Intent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA), CHECK_TTS_INSTALLED); @@ -285,7 +315,6 @@ public void onResume() { if (isPaused() && !screenLockEventOccurred) { playEarcon(START_READING_EARCON); - //speakParagraph(getNextParagraph()); } else { screenLockEventOccurred = false; } @@ -333,7 +362,6 @@ private boolean isAndroidVersionOlderThanLollipop() { } private void postRepaint() { - Handler handler = new Handler(Looper.getMainLooper()); handler.postDelayed(new Runnable() { @Override public void run() { @@ -496,6 +524,12 @@ private void doFinalInitialization() { private void highlightParagraph() { if (0 <= myParagraphIndex && myParagraphIndex < myParagraphsNumber) { + TextPosition paragraphStart = new TextPosition(myParagraphIndex, 0, 0); + TextPosition paragraphEnd = new TextPosition(myParagraphIndex, Integer.MAX_VALUE, 0); + if (paragraphStart.compareTo(myApi.getPageStart()) < 0 + || paragraphEnd.compareTo(myApi.getPageEnd()) > 0) { + myApi.setPageStart(paragraphStart); + } myApi.highlightArea( new TextPosition(myParagraphIndex, 0, 0), new TextPosition(myParagraphIndex, Integer.MAX_VALUE, 0) @@ -509,6 +543,8 @@ private void stopTalking() { setIsPaused(); setActive(false); enablePlayButton(); + // Prevent us reacting to callback from speech we are canceling + utterancesInProgress.clear(); if (myTTS != null) { myTTS.stop(); } @@ -538,8 +574,10 @@ private synchronized void setActive(final boolean isActiveToUse) { } private void speakString(String text, final int sentenceNumber) { - HashMap callbackMap = new HashMap(); - callbackMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, Integer.toString(sentenceNumber)); + String utteranceId = makeStringIdForParagraphAndSentence(myParagraphIndex, sentenceNumber); + HashMap callbackMap = new HashMap<>(); + callbackMap.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId); + utterancesInProgress.add(utteranceId); myTTS.speak(text, TextToSpeech.QUEUE_ADD, callbackMap); } @@ -567,7 +605,7 @@ private String getNextParagraph() { final FBReaderApp fbReader = (FBReaderApp)FBReaderApp.Instance(); ZLTextRegion region = fbReader.getTextView().getDoubleTapSelectedRegion(); - boolean shouldHighlightSentence = false; + boolean shouldHighlightSentence = fbReader.NavigateBySentenceOption.getValue(); if(region == null && fbReader.getTextView().didScroll()){ region = fbReader.getTextView().getTopOfPageRegion(); shouldHighlightSentence = true; @@ -610,7 +648,6 @@ private String getNextParagraph() { } myCurrentSentence = currentSentence; } - waitForTextPosition(); if(shouldHighlightSentence){ highlightSentence(myCurrentSentence); } @@ -651,7 +688,7 @@ private void speakParagraph(String text) { int sentenceNumber = 0; int numWordIndices = sentenceList.size(); - if (isPaused()) { + if (isPaused() || fbReader.NavigateBySentenceOption.getValue()) { enablePauseButton(); setIsPlaying(); if (myCurrentSentence > 0 && numWordIndices > myCurrentSentence) { @@ -678,16 +715,32 @@ private void speakParagraph(String text) { @Override public void onUtteranceCompleted(String uttId) { - String lastSentenceID = Integer.toString(lastSentence); - if (isActive() && uttId.equals(lastSentenceID)) { + handler.post(new Runnable() { + @Override + public void run() { + onUtteranceCompletedUiThread(uttId); + } + }); + } + + private void onUtteranceCompletedUiThread(String uttId) { + if (!utterancesInProgress.contains(uttId)) { + return; + } + utterancesInProgress.remove(uttId); + int sentenceNumber = sentenceNumberFromId(uttId); + if (isActive() && (sentenceNumber == lastSentence)) { ++myParagraphIndex; - speakParagraph(getNextParagraph()); + myCurrentSentence = 0; + String text = getNextParagraph(); + speakParagraph(text); if (myParagraphIndex >= myParagraphsNumber) { stopTalking(); } } else { - myCurrentSentence = Integer.parseInt(uttId); + myCurrentSentence = sentenceNumber; if (isActive()) { + // The next sentence is already scheduled. Highlight it. int listSize = mySentences.length; if (listSize > 1 && myCurrentSentence < listSize) { highlightSentence(myCurrentSentence); @@ -746,17 +799,24 @@ private void pause() { setIsPaused(); } - private void highlightSentence(int myCurrentSentence) { - if (myCurrentSentence >= mySentences.length) { + static boolean init = false; + static int lastpp = 0; + static int lastsentence = 0; + private void highlightSentence(int sentenceIndexInCurrentParagraph) { + init = true; + lastpp = myParagraphIndex; + lastsentence = sentenceIndexInCurrentParagraph; + if (sentenceIndexInCurrentParagraph >= mySentences.length) { return; } - int endEI = myCurrentSentence < mySentences.length-1 ? mySentences[myCurrentSentence+1].i-1: Integer.MAX_VALUE; + int endEI = (sentenceIndexInCurrentParagraph < mySentences.length - 1) + ? mySentences[sentenceIndexInCurrentParagraph+1].i - 1 : Integer.MAX_VALUE; TextPosition stPos; - if (myCurrentSentence == 0) + if (sentenceIndexInCurrentParagraph == 0) stPos = new TextPosition(myParagraphIndex, 0, 0); else - stPos = new TextPosition(myParagraphIndex, mySentences[myCurrentSentence].i, 0); + stPos = new TextPosition(myParagraphIndex, mySentences[sentenceIndexInCurrentParagraph].i, 0); TextPosition edPos = new TextPosition(myParagraphIndex, endEI, 0); if (stPos.compareTo(myApi.getPageStart()) < 0 || edPos.compareTo(myApi.getPageEnd()) > 0) @@ -798,12 +858,13 @@ private int getPlayButtonImageResource(boolean isPlayButton) { return R.drawable.ic_pause_white_24dp; } - private void goForward() { + private void goForwardOneParagraph() { boolean wasPlaying = isPlaying(); stopTalking(); playEarcon(FORWARD_EARCON); if (myParagraphIndex < myParagraphsNumber) { myParagraphIndex++; + myCurrentSentence = 0; final String nextParagraph = getNextParagraph(); if (wasPlaying) { speakParagraph(nextParagraph); @@ -811,17 +872,59 @@ private void goForward() { } } - private void goBackward() { + private void goForwardOneSentence() { + final boolean wasPlaying = isPlaying(); + stopTalking(); + playEarcon(FORWARD_EARCON); + final int nextSentence = myCurrentSentence + 1; + final String nextParagraph; + if (nextSentence < mySentences.length) { + nextParagraph = getNextParagraph(); + myCurrentSentence = nextSentence; + } else { + myParagraphIndex++; + nextParagraph = getNextParagraph(); + myCurrentSentence = 0; + } + if (wasPlaying) { + speakParagraph(nextParagraph); + } + highlightSentence(myCurrentSentence); + } + + private void goBackwardOneParagraph() { boolean wasPlaying = isPlaying(); stopTalking(); playEarcon(BACK_EARCON); gotoPreviousParagraph(); final String nextParagraph = getNextParagraph(); + myCurrentSentence = 0; if (wasPlaying) { speakParagraph(nextParagraph); + } else { + highlightSentence(myCurrentSentence); } } + private void goBackwardOneSentence() { + boolean wasPlaying = isPlaying(); + stopTalking(); + playEarcon(BACK_EARCON); + final String nextParagraph; + if (myCurrentSentence == 0) { + gotoPreviousParagraph(); + nextParagraph = getNextParagraph(); + myCurrentSentence = mySentences.length - 1; + } else { + nextParagraph = getNextParagraph(); + myCurrentSentence--; + } + if (wasPlaying) { + speakParagraph(nextParagraph); + } + highlightSentence(myCurrentSentence); + } + private void showMainMenu() { stopTalking(); playEarcon(MENU_EARCON); @@ -890,6 +993,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { } finish(); } + return super.onKeyDown(keyCode, event); } diff --git a/FBReader/src/main/java/org/geometerplus/android/fbreader/preferences/PreferenceActivity.java b/FBReader/src/main/java/org/geometerplus/android/fbreader/preferences/PreferenceActivity.java index 470cf43150c..367e41e4853 100644 --- a/FBReader/src/main/java/org/geometerplus/android/fbreader/preferences/PreferenceActivity.java +++ b/FBReader/src/main/java/org/geometerplus/android/fbreader/preferences/PreferenceActivity.java @@ -275,6 +275,7 @@ protected void onClick() { final Screen moreSettingsScreen = createPreferenceScreen("moresettings"); myScreen.removePreference(dictionaryScreen.myScreen); myScreen.removePreference(imagesScreen.myScreen); + moreSettingsScreen.addOption(fbReader.NavigateBySentenceOption, "navigateBySentence"); moreSettingsScreen.addPreference(dictionaryScreen.myScreen); moreSettingsScreen.addPreference(imagesScreen.myScreen); } diff --git a/FBReader/src/main/java/org/geometerplus/android/fbreader/preferences/ZLPreferenceActivity.java b/FBReader/src/main/java/org/geometerplus/android/fbreader/preferences/ZLPreferenceActivity.java index 48fc0ce03da..69899333afe 100644 --- a/FBReader/src/main/java/org/geometerplus/android/fbreader/preferences/ZLPreferenceActivity.java +++ b/FBReader/src/main/java/org/geometerplus/android/fbreader/preferences/ZLPreferenceActivity.java @@ -27,7 +27,7 @@ import android.support.v7.widget.Toolbar; import android.view.LayoutInflater; import android.view.View; -import android.widget.LinearLayout; +import android.view.ViewGroup; import org.benetech.android.R; import org.geometerplus.zlibrary.core.options.ZLBooleanOption; @@ -166,7 +166,7 @@ public void setUpNestedScreen(PreferenceScreen preferenceScreen) { if(dialog != null) { Toolbar bar; - LinearLayout root = (LinearLayout) dialog.findViewById(android.R.id.list).getParent(); + ViewGroup root = (ViewGroup) dialog.findViewById(android.R.id.list).getParent(); bar = (Toolbar) LayoutInflater.from(this).inflate(R.layout.sub_settings, root, false); root.addView(bar, 0); // insert at top diff --git a/FBReader/src/main/java/org/geometerplus/fbreader/fbreader/FBReaderApp.java b/FBReader/src/main/java/org/geometerplus/fbreader/fbreader/FBReaderApp.java index 1fe68ff92ae..39b3f9b57fa 100644 --- a/FBReader/src/main/java/org/geometerplus/fbreader/fbreader/FBReaderApp.java +++ b/FBReader/src/main/java/org/geometerplus/fbreader/fbreader/FBReaderApp.java @@ -113,6 +113,9 @@ public static enum ImageTappingAction { public final ZLBooleanOption ShowPositionsInCancelMenuOption = new ZLBooleanOption("CancelMenu", "positions", true); + public final ZLBooleanOption NavigateBySentenceOption = + new ZLBooleanOption("Options", "NavigateBySentence", false); + private final ZLKeyBindings myBindings = new ZLKeyBindings("Keys"); public final FBView BookTextView;