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;