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;