diff --git a/README.md b/README.md index 5a1bdefd0..7c07a7bfa 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Compatibility has been verified only on the following devices: ## 🤔 How to Use Traditional T9? Before using Traditional T9 for the first time you need to configure it and load a dictionary. After that, you can start typing right away in one of the three modes: Predictive, ABC, or Numeric (123). And even if you have mastered the keypad back in the day, you will still find the Predictive mode now offers more powerful and smart new ways of typing with even fewer key presses. -So make sure to read the initial setup and the hotkey tips in the [user manual](docs/user-manual.md). Also, don't miss the convenient [compatibility options](docs/user-manual.md#compatibility-options--troubleshooting) aimed to improve the experience in some applications. +So make sure to read the initial setup and the hotkey tips in the [user manual](docs/help/help.en.md). Also, don't miss the convenient [compatibility options](docs/user-manual.md#compatibility-options--troubleshooting) aimed to improve the experience in some applications. ## ⌨ Contributing As with many other open-source projects, this one is also maintained by its author in his free time. Any help in making Traditional T9 better will be highly appreciated. Here is how: diff --git a/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java b/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java index e9e1bb2ef..3666d881a 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/DataStore.java @@ -1,22 +1,35 @@ package io.github.sspanak.tt9.db; import android.content.Context; +import android.os.CancellationSignal; import android.os.Handler; import androidx.annotation.NonNull; import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import io.github.sspanak.tt9.db.entities.AddWordResult; import io.github.sspanak.tt9.db.wordPairs.WordPairStore; import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.db.words.WordStore; import io.github.sspanak.tt9.languages.Language; +import io.github.sspanak.tt9.preferences.settings.SettingsStore; import io.github.sspanak.tt9.util.ConsumerCompat; import io.github.sspanak.tt9.util.Logger; public class DataStore { - private static final Handler asyncHandler = new Handler(); + private final static String LOG_TAG = DataStore.class.getSimpleName(); + + private static final Handler asyncReturn = new Handler(); + private static final ExecutorService executor = Executors.newCachedThreadPool(); + + private static Future getWordsTask; + private static CancellationSignal getWordsCancellationSignal = new CancellationSignal(); + private static WordPairStore pairs; private static WordStore words; @@ -28,7 +41,7 @@ public static void init(Context context) { private static void runInThread(@NonNull Runnable action) { - new Thread(action).start(); + executor.submit(action); } @@ -40,7 +53,7 @@ private static void runInTransaction(@NonNull Runnable action, @NonNull Runnable words.finishTransaction(); } catch (Exception e) { words.failTransaction(); - Logger.e(DataStore.class.getSimpleName(), errorMessagePrefix + " " + e.getMessage()); + Logger.e(LOG_TAG, errorMessagePrefix + " " + e.getMessage()); } onFinish.run(); }); @@ -85,17 +98,42 @@ public static void makeTopWord(@NonNull Language language, @NonNull String word, public static void getWords(ConsumerCompat> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) { - runInThread(() -> { - ArrayList data = words.getSimilar(language, sequence, filter, minWords, maxWords); - asyncHandler.post(() -> dataHandler.accept(data)); - }); + if (getWordsTask != null && !getWordsTask.isDone()) { + dataHandler.accept(new ArrayList<>()); + getWordsCancellationSignal.cancel(); + return; + } + + getWordsCancellationSignal = new CancellationSignal(); + getWordsTask = executor.submit(() -> getWordsSync(dataHandler, language, sequence, filter, minWords, maxWords)); + executor.submit(DataStore::setGetWordsTimeout); + } + + + private static void getWordsSync(ConsumerCompat> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) { + try { + ArrayList data = words.getSimilar(getWordsCancellationSignal, language, sequence, filter, minWords, maxWords); + asyncReturn.post(() -> dataHandler.accept(data)); + } catch (Exception e) { + Logger.e(LOG_TAG, "Error fetching words: " + e.getMessage()); + } + } + + + private static void setGetWordsTimeout() { + try { + getWordsTask.get(SettingsStore.SLOW_QUERY_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (Exception e) { + getWordsCancellationSignal.cancel(); + Logger.e(LOG_TAG, "Word loading timed out after " + SettingsStore.SLOW_QUERY_TIMEOUT + " ms."); + } } public static void getCustomWords(ConsumerCompat> dataHandler, String wordFilter, int maxWords) { runInThread(() -> { ArrayList data = words.getSimilarCustom(wordFilter, maxWords); - asyncHandler.post(() -> dataHandler.accept(data)); + asyncReturn.post(() -> dataHandler.accept(data)); }); } @@ -103,7 +141,7 @@ public static void getCustomWords(ConsumerCompat> dataHandler, public static void countCustomWords(ConsumerCompat dataHandler) { runInThread(() -> { long data = words.countCustom(); - asyncHandler.post(() -> dataHandler.accept(data)); + asyncReturn.post(() -> dataHandler.accept(data)); }); } @@ -111,7 +149,7 @@ public static void countCustomWords(ConsumerCompat dataHandler) { public static void exists(ConsumerCompat> dataHandler, ArrayList languages) { runInThread(() -> { ArrayList data = words.exists(languages); - asyncHandler.post(() -> dataHandler.accept(data)); + asyncReturn.post(() -> dataHandler.accept(data)); }); } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java index 5aca4991d..464004afd 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/sqlite/ReadOps.java @@ -4,8 +4,11 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteStatement; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; @@ -118,19 +121,19 @@ public String getWords(@NonNull SQLiteDatabase db, Language language, boolean cu @NonNull - public WordList getWords(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) { + public WordList getWords(@NonNull SQLiteDatabase db, @Nullable CancellationSignal cancel, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) { if (positions.isEmpty()) { Logger.d(LOG_TAG, "No word positions. Not searching words."); return new WordList(); } String wordsQuery = getWordsQuery(language, positions, filter, maximumWords, fullOutput); - if (wordsQuery.isEmpty()) { + if (wordsQuery.isEmpty() || (cancel != null && cancel.isCanceled())) { return new WordList(); } WordList words = new WordList(); - try (Cursor cursor = db.rawQuery(wordsQuery, null)) { + try (Cursor cursor = db.rawQuery(wordsQuery, null, cancel)) { while (cursor.moveToNext()) { words.add( cursor.getString(0), @@ -138,26 +141,29 @@ public WordList getWords(@NonNull SQLiteDatabase db, @NonNull Language language, fullOutput ? cursor.getInt(2) : 0 ); } + } catch (OperationCanceledException e) { + Logger.d(LOG_TAG, "Words query cancelled!"); + return words; } return words; } - public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) { + public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull CancellationSignal cancel, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) { int generations = switch (sequence.length()) { case 2 -> wordFilter.isEmpty() ? 1 : 10; case 3, 4 -> wordFilter.isEmpty() ? 2 : 10; default -> 10; }; - return getWordPositions(db, language, sequence, generations, minPositions, wordFilter); + return getWordPositions(db, cancel, language, sequence, generations, minPositions, wordFilter); } @NonNull - public String getWordPositions(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, int generations, int minPositions, String wordFilter) { - if (sequence.length() == 1) { + public String getWordPositions(@NonNull SQLiteDatabase db, @Nullable CancellationSignal cancel, @NonNull Language language, @NonNull String sequence, int generations, int minPositions, String wordFilter) { + if (sequence.length() == 1 || (cancel != null && cancel.isCanceled())) { return sequence; } @@ -165,18 +171,24 @@ public String getWordPositions(@NonNull SQLiteDatabase db, @NonNull Language lan String cachedFactoryPositions = SlowQueryStats.getCachedIfSlow(SlowQueryStats.generateKey(language, sequence, wordFilter, minPositions)); if (cachedFactoryPositions != null) { - String customWordPositions = getCustomWordPositions(db, language, sequence, generations); + String customWordPositions = getCustomWordPositions(db, cancel, language, sequence, generations); return customWordPositions.isEmpty() ? cachedFactoryPositions : customWordPositions + "," + cachedFactoryPositions; } - try (Cursor cursor = db.rawQuery(getPositionsQuery(language, sequence, generations), null)) { + try (Cursor cursor = db.rawQuery(getPositionsQuery(language, sequence, generations), null, cancel)) { positions.appendFromDbRanges(cursor); + } catch (OperationCanceledException ignored) { + Logger.d(LOG_TAG, "Word positions query cancelled!"); + return sequence; } if (positions.size < minPositions && generations < Integer.MAX_VALUE) { Logger.d(LOG_TAG, "Not enough positions: " + positions.size + " < " + minPositions + ". Searching for more."); - try (Cursor cursor = db.rawQuery(getFactoryWordPositionsQuery(language, sequence, Integer.MAX_VALUE), null)) { + try (Cursor cursor = db.rawQuery(getFactoryWordPositionsQuery(language, sequence, Integer.MAX_VALUE), null, cancel)) { positions.appendFromDbRanges(cursor); + } catch (OperationCanceledException ignored) { + Logger.d(LOG_TAG, "Word positions query cancelled!"); + return sequence; } } @@ -184,9 +196,12 @@ public String getWordPositions(@NonNull SQLiteDatabase db, @NonNull Language lan } - @NonNull private String getCustomWordPositions(@NonNull SQLiteDatabase db, Language language, String sequence, int generations) { - try (Cursor cursor = db.rawQuery(getCustomWordPositionsQuery(language, sequence, generations), null)) { + @NonNull private String getCustomWordPositions(@NonNull SQLiteDatabase db, CancellationSignal cancel, Language language, String sequence, int generations) { + try (Cursor cursor = db.rawQuery(getCustomWordPositionsQuery(language, sequence, generations), null, cancel)) { return new WordPositionsStringBuilder().appendFromDbRanges(cursor).toString(); + } catch (OperationCanceledException e) { + Logger.d(LOG_TAG, "Custom word positions query cancelled."); + return ""; } } diff --git a/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java b/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java index 8d8ff1518..6bbff4fd2 100644 --- a/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/db/words/WordStore.java @@ -1,6 +1,7 @@ package io.github.sspanak.tt9.db.words; import android.content.Context; +import android.os.CancellationSignal; import androidx.annotation.NonNull; @@ -60,7 +61,7 @@ public ArrayList exists(ArrayList languages) { * For example: "7655" -> "roll" (exact match), but also: "rolled", "roller", "rolling", ... * and other similar. */ - public ArrayList getSimilar(Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) { + public ArrayList getSimilar(@NonNull CancellationSignal cancel, Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) { if (!checkOrNotify()) { return new ArrayList<>(); } @@ -80,15 +81,17 @@ public ArrayList getSimilar(Language language, String sequence, String w final String filter = wordFilter == null ? "" : wordFilter; Timer.start("get_positions"); - String positions = readOps.getSimilarWordPositions(sqlite.getDb(), language, sequence, filter, minWords); + String positions = readOps.getSimilarWordPositions(sqlite.getDb(), cancel, language, sequence, filter, minWords); long positionsTime = Timer.stop("get_positions"); Timer.start("get_words"); - ArrayList words = readOps.getWords(sqlite.getDb(), language, positions, filter, maxWords, false).toStringList(); + ArrayList words = readOps.getWords(sqlite.getDb(), cancel, language, positions, filter, maxWords, false).toStringList(); long wordsTime = Timer.stop("get_words"); printLoadingSummary(sequence, words, positionsTime, wordsTime); - SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions); + if (!cancel.isCanceled()) { // do not store empty results from aborted queries in the cache + SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions); + } return words; } @@ -188,8 +191,8 @@ public void makeTopWord(@NonNull Language language, @NonNull String word, @NonNu try { Timer.start(LOG_TAG); - String topWordPositions = readOps.getWordPositions(sqlite.getDb(), language, sequence, 0, 0, ""); - WordList topWords = readOps.getWords(sqlite.getDb(), language, topWordPositions, "", 9999, true); + String topWordPositions = readOps.getWordPositions(sqlite.getDb(), null, language, sequence, 0, 0, ""); + WordList topWords = readOps.getWords(sqlite.getDb(), null, language, topWordPositions, "", 9999, true); if (topWords.isEmpty()) { throw new Exception("No such word"); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java index 6ecc9cf9b..7a5a5d1c3 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java +++ b/app/src/main/java/io/github/sspanak/tt9/ime/helpers/TextField.java @@ -161,7 +161,7 @@ public int getPaddedWordBeforeCursorLength() { return 0; } - int whitespaceShift = Math.max(before.lastWhitespaceBlockIndex(), 0); + int whitespaceShift = Math.max(before.lastBoundaryIndex(), 0); return Math.min(before.length() - whitespaceShift, (int) (SettingsStore.BACKSPACE_ACCELERATION_MAX_CHARS * 1.5)); } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/hotkeys/SectionKeymap.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/hotkeys/SectionKeymap.java index ad76f329a..e5b78141f 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/hotkeys/SectionKeymap.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/hotkeys/SectionKeymap.java @@ -114,10 +114,15 @@ private void onItemClick(DropDownPreference item) { return false; } - ((DropDownPreference) preference).setValue(newKey.toString()); - previewCurrentKey((DropDownPreference) preference, newKey.toString()); - populateOtherItems((DropDownPreference) preference); - return true; + try { + ((DropDownPreference) preference).setValue(newKey.toString()); + previewCurrentKey((DropDownPreference) preference, newKey.toString()); + populateOtherItems((DropDownPreference) preference); + return true; + } catch (Exception e) { + Logger.e("SectionKeymap.onItemClick", "Failed setting new hotkey. " + e.getMessage()); + return false; + } }); } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/ItemDonate.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/ItemDonate.java new file mode 100644 index 000000000..9714a2fb3 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/ItemDonate.java @@ -0,0 +1,41 @@ +package io.github.sspanak.tt9.preferences.screens.main; + +import android.content.Intent; +import android.net.Uri; + +import androidx.preference.Preference; + +import io.github.sspanak.tt9.R; +import io.github.sspanak.tt9.preferences.PreferencesActivity; +import io.github.sspanak.tt9.preferences.items.ItemClickable; +import io.github.sspanak.tt9.util.Logger; + +class ItemDonate extends ItemClickable { + static final String NAME = "donate_link"; + private final PreferencesActivity activity; + + ItemDonate(Preference preference, PreferencesActivity activity) { + super(preference); + this.activity = activity; + } + + public ItemDonate populate() { + if (item != null) { + String appName = activity.getString(R.string.app_name_short); + String url = activity.getString(R.string.donate_url_short); + item.setSummary(activity.getString(R.string.donate_summary, appName, url)); + } + return this; + } + + @Override + protected boolean onClick(Preference p) { + try { + activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(activity.getString(R.string.donate_url)))); + return true; + } catch (Exception e) { + Logger.w(getClass().getSimpleName(), "Cannot navigate to the donation page. " + e.getMessage() + " (do you have a browser?)"); + return false; + } + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/MainSettingsScreen.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/MainSettingsScreen.java index 12be3d340..f19331730 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/MainSettingsScreen.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/MainSettingsScreen.java @@ -40,15 +40,8 @@ public void onResume() { private void createAboutSection() { - Preference donate = findPreference("donate_link"); - if (donate != null) { - String appName = getString(R.string.app_name_short); - String url = getString(R.string.donate_url_short); - donate.setSummary(getString(R.string.donate_summary, appName, url)); - } - - ItemVersionInfo debugOptions = new ItemVersionInfo(findPreference(ItemVersionInfo.NAME), activity); - debugOptions.populate().enableClickHandler(); + (new ItemDonate(findPreference(ItemDonate.NAME), activity)).populate().enableClickHandler(); + (new ItemVersionInfo(findPreference(ItemVersionInfo.NAME), activity)).populate().enableClickHandler(); } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java index d04ebbd20..dcecfe422 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/settings/SettingsStore.java @@ -20,6 +20,7 @@ public class SettingsStore extends SettingsUI { public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms public final static int RESIZE_THROTTLING_TIME = 60; // ms public final static byte SLOW_QUERY_TIME = 50; // ms + public final static int SLOW_QUERY_TIMEOUT = 3000; // ms public final static int SOFT_KEY_DOUBLE_CLICK_DELAY = 500; // ms public final static int SOFT_KEY_REPEAT_DELAY = 40; // ms public final static int SOFT_KEY_TITLE_MAX_CHARS = 5; diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKey.java b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKey.java index df1fe09f0..331d71775 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKey.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKey.java @@ -89,6 +89,7 @@ public boolean onTouch(View view, MotionEvent event) { } else if (action == MotionEvent.ACTION_UP) { if (!repeat || hold) { hold = false; + repeat = false; boolean result = handleRelease(); lastPressedKey = ignoreLastPressedKey ? -1 : getId(); return result; diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyBackspace.java b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyBackspace.java index 4663c9719..67e8e0b29 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyBackspace.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyBackspace.java @@ -1,6 +1,7 @@ package io.github.sspanak.tt9.ui.main.keys; import android.content.Context; +import android.os.Handler; import android.util.AttributeSet; import android.view.KeyEvent; @@ -12,6 +13,10 @@ public class SoftKeyBackspace extends SwipeableKey { private int repeat = 0; + private boolean isActionPerformed = false; + private final Handler waitForSwipe = new Handler(); + + public SoftKeyBackspace(Context context) { super(context); } @@ -61,12 +66,36 @@ protected float getSwipeYThreshold(Context context) { @Override final protected boolean handlePress() { super.handlePress(); - return deleteText(); + isActionPerformed = false; + waitForSwipe.postDelayed(this::handlePressDebounced, 1 + getAverageSwipeProcessingTime()); + return true; + } + + + /** + * Avoids deleting text twice when swiping - first, when the user touches the screen, and then, + * when they finish the swipe gesture. + */ + private void handlePressDebounced() { + if (!isActionPerformed) { + isActionPerformed = true; + deleteText(); + } + } + + + @Override + protected void handleStartSwipeX(float position, float delta) { + if (!isActionPerformed && validateTT9Handler()) { + isActionPerformed = true; + tt9.onBackspace(SettingsStore.BACKSPACE_ACCELERATION_REPEAT_DEBOUNCE); + } } @Override final protected void handleHold() { + isActionPerformed = true; repeat++; deleteText(); } @@ -76,28 +105,18 @@ final protected void handleHold() { final protected boolean handleRelease() { vibrate(repeat > 0 ? Vibration.getReleaseVibration() : Vibration.getNoVibration()); repeat = 0; - return true; - } - - @Override - protected void handleEndSwipeX(float position, float delta) { - if (validateTT9Handler()) { - tt9.onBackspace(SettingsStore.BACKSPACE_ACCELERATION_REPEAT_DEBOUNCE); - } + return true; } - private boolean deleteText() { + private void deleteText() { if (validateTT9Handler() && !tt9.onBackspace(repeat)) { // Limited or special numeric field (e.g. formatted money or dates) cannot always return // the text length, therefore onBackspace() seems them as empty and does nothing. This results // in fallback to the default hardware key action. Here we simulate the hardware BACKSPACE. tt9.sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); - return true; } - - return false; } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyLF4.java b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyLF4.java index fc527b61c..fdf750984 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyLF4.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SoftKeyLF4.java @@ -18,6 +18,11 @@ private boolean areThereManyLanguages() { return tt9 != null && tt9.getSettings().getEnabledLanguageIds().size() > 1; } + @Override + protected float getSwipeXThreshold(Context context) { + return super.getSwipeXThreshold(context) * 3; + } + @Override protected void handleHold() { preventRepeat(); diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SwipeableKey.java b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SwipeableKey.java index 30c16dd8c..5a5819fc2 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SwipeableKey.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/main/keys/SwipeableKey.java @@ -7,8 +7,11 @@ import io.github.sspanak.tt9.R; import io.github.sspanak.tt9.preferences.settings.SettingsStore; +import io.github.sspanak.tt9.util.Timer; abstract public class SwipeableKey extends SoftKey { + private static final String LOG_TAG = SwipeableKey.class.getSimpleName(); + private float HOLD_DURATION_THRESHOLD; protected float SWIPE_X_THRESHOLD; protected float SWIPE_Y_THRESHOLD; @@ -22,6 +25,10 @@ abstract public class SwipeableKey extends SoftKey { private float startY; private long startTime; + private int swipeCount = 0; + private long swipeProcessingTime = 0; + private long swipeProcessingTimeAverage = 40; + public SwipeableKey(Context context) { super(context); @@ -53,10 +60,31 @@ protected final void resetTimeThresholds(Context context) { protected float getSwipeYThreshold(Context context) { return context.getResources().getDimensionPixelSize(R.dimen.numpad_key_height) / 10.0f; } + private void updateSwipeTimingStats() { + long time = Timer.get(LOG_TAG); + + long deltaT = time - swipeProcessingTimeAverage; + if (deltaT < -swipeProcessingTimeAverage || deltaT > 5) { + swipeCount = 0; + swipeProcessingTime = 0; + } + + swipeCount++; + swipeProcessingTime += time; + swipeProcessingTimeAverage = swipeProcessingTime / swipeCount; + } + + + protected long getAverageSwipeProcessingTime() { + return swipeProcessingTimeAverage; + } + + @Override public boolean onTouch(View v, MotionEvent event) { switch(event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: + Timer.start(LOG_TAG); onPress(event); break; case MotionEvent.ACTION_MOVE: @@ -107,9 +135,11 @@ private void onMove(View v, MotionEvent event) { handleSwipeX(event.getRawX(), deltaX); } else if (Math.abs(deltaY) >= SWIPE_Y_THRESHOLD) { isSwipingY = true; + updateSwipeTimingStats(); handleStartSwipeY(event.getRawY(), deltaY); } else if (Math.abs(deltaX) >= SWIPE_X_THRESHOLD) { isSwipingX = true; + updateSwipeTimingStats(); handleStartSwipeX(event.getRawX(), deltaX); } else if (!isHolding && Math.abs(deltaX) < SWIPE_X_THRESHOLD && Math.abs(deltaY) < SWIPE_Y_THRESHOLD) { onLongClick(v); diff --git a/app/src/main/java/io/github/sspanak/tt9/util/Text.java b/app/src/main/java/io/github/sspanak/tt9/util/Text.java index ee09bf1d4..35175550c 100644 --- a/app/src/main/java/io/github/sspanak/tt9/util/Text.java +++ b/app/src/main/java/io/github/sspanak/tt9/util/Text.java @@ -164,21 +164,30 @@ public int length() { } - public int lastWhitespaceBlockIndex() { - if (text == null) { + public int lastBoundaryIndex() { + if (text == null || text.length() < 2) { return -1; } - for (int i = text.length() - 1; i >= 0; i--) { + char lastChar = text.charAt(text.length() - 1); + char penultimateChar = text.charAt(text.length() - 2); + + boolean endsWithWhitespaceBlock = Character.isWhitespace(lastChar) && Character.isWhitespace(penultimateChar); + boolean endsWithPunctuationBlock = (lastChar == '.' || lastChar == ',') && (penultimateChar == '.' || penultimateChar == ','); + + for (int i = text.length() - 1, firstChar = 1; i >= 0; i--, firstChar = 0) { + char currentChar = text.charAt(i); + if ( - Character.isWhitespace(text.charAt(i)) - && (i == 0 || !Character.isWhitespace(text.charAt(i - 1))) + (endsWithPunctuationBlock && currentChar != '.' && currentChar != ',') + || (endsWithWhitespaceBlock && !Character.isWhitespace(currentChar)) + || (!endsWithWhitespaceBlock && !endsWithPunctuationBlock && firstChar == 0 && Character.isWhitespace(currentChar)) ) { - return i; + return i + 1; } } - return -1; + return 0; } diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index fc185ac5c..e73cf4dd1 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -26,14 +26,14 @@ 10000dp - 2dp + 6dp 0dp 56dp - 36dp - 0.66 + 38dp + 0.675 17sp - 38dp + 40dp 36dp diff --git a/app/src/main/res/xml/prefs.xml b/app/src/main/res/xml/prefs.xml index 59b2af25c..09fa3af5e 100644 --- a/app/src/main/res/xml/prefs.xml +++ b/app/src/main/res/xml/prefs.xml @@ -38,12 +38,7 @@ - - + app:title="@string/donate_title" />