From da03e3ddd45ea0d41e9a3fc2817e43363c5333e8 Mon Sep 17 00:00:00 2001 From: "pdroidandroid@gmail.com" Date: Wed, 1 Jun 2022 00:18:01 +0200 Subject: [PATCH 1/2] Added emoji usage frequency support --- .../keyboard/KeyboardSwitcher.java | 2 +- .../keyboard/emoji/DynamicGridKeyboard.java | 162 ++++---------- .../keyboard/emoji/EmojiCategory.java | 42 ++-- .../keyboard/emoji/EmojiPalettesAdapter.java | 67 +++--- .../keyboard/emoji/EmojiPalettesView.java | 52 +++-- .../inputmethod/keyboard/emoji/RecentEmoji.kt | 78 +++++++ .../keyboard/emoji/RecentEmojiDbHelper.kt | 168 ++++++++++++++ .../keyboard/emoji/RecentEmojiKeyboard.kt | 207 ++++++++++++++++++ .../openboard/inputmethod/latin/LatinIME.java | 22 +- .../inputmethod/latin/settings/Settings.java | 30 ++- .../latin/settings/SettingsValues.java | 9 +- .../latin/utils/ExecutorUtils.java | 7 +- .../latin/utils/SparseArrayUtils.kt | 33 +++ app/src/main/res/values/strings.xml | 4 + .../main/res/xml/prefs_screen_advanced.xml | 6 + 15 files changed, 676 insertions(+), 213 deletions(-) create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmoji.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmojiDbHelper.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmojiKeyboard.kt create mode 100644 app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/SparseArrayUtils.kt diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java index e46d85d2e..fdea056b5 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java @@ -24,7 +24,6 @@ import android.view.LayoutInflater; import android.view.View; import android.view.inputmethod.EditorInfo; - import org.dslul.openboard.inputmethod.event.Event; import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException; import org.dslul.openboard.inputmethod.keyboard.clipboard.ClipboardHistoryView; @@ -545,6 +544,7 @@ public View onCreateInputView(final boolean isHardwareAcceleratedDrawingEnabled) mKeyboardView.setKeyboardActionListener(mLatinIME); mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled( isHardwareAcceleratedDrawingEnabled); + mEmojiPalettesView.setUiHandler(mLatinIME.mHandler); mEmojiPalettesView.setKeyboardActionListener(mLatinIME); mClipboardHistoryView.setHardwareAcceleratedDrawingEnabled( isHardwareAcceleratedDrawingEnabled); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/DynamicGridKeyboard.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/DynamicGridKeyboard.java index 35b217637..9c7e31398 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/DynamicGridKeyboard.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/DynamicGridKeyboard.java @@ -16,47 +16,39 @@ package org.dslul.openboard.inputmethod.keyboard.emoji; -import android.content.SharedPreferences; import android.text.TextUtils; -import android.util.Log; - import org.dslul.openboard.inputmethod.keyboard.Key; import org.dslul.openboard.inputmethod.keyboard.Keyboard; import org.dslul.openboard.inputmethod.keyboard.internal.MoreKeySpec; -import org.dslul.openboard.inputmethod.latin.settings.Settings; -import org.dslul.openboard.inputmethod.latin.utils.JsonUtils; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; import java.util.List; +import java.util.Objects; /** * This is a Keyboard class where you can add keys dynamically shown in a grid layout */ -final class DynamicGridKeyboard extends Keyboard { - private static final String TAG = DynamicGridKeyboard.class.getSimpleName(); +public class DynamicGridKeyboard extends Keyboard { + + private static final String TAG = "DynamicGridKeyboard"; private static final int TEMPLATE_KEY_CODE_0 = 0x30; private static final int TEMPLATE_KEY_CODE_1 = 0x31; private final Object mLock = new Object(); - private final SharedPreferences mPrefs; private final int mHorizontalStep; private final int mHorizontalGap; private final int mVerticalStep; private final int mColumnsNum; - private final int mMaxKeyCount; - private final boolean mIsRecents; - private final ArrayDeque mGridKeys = new ArrayDeque<>(); - private final ArrayDeque mPendingKeys = new ArrayDeque<>(); + private final LinkedList mGridKeys = new LinkedList<>(); private List mCachedGridKeys; - public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templateKeyboard, - final int maxKeyCount, final int categoryId) { + public DynamicGridKeyboard(final Keyboard templateKeyboard) { super(templateKeyboard); final Key key0 = getTemplateKey(TEMPLATE_KEY_CODE_0); final Key key1 = getTemplateKey(TEMPLATE_KEY_CODE_1); @@ -64,9 +56,6 @@ public DynamicGridKeyboard(final SharedPreferences prefs, final Keyboard templat mHorizontalStep = key0.getWidth() + mHorizontalGap; mVerticalStep = key0.getHeight() + mVerticalGap; mColumnsNum = mBaseWidth / mHorizontalStep; - mMaxKeyCount = maxKeyCount; - mIsRecents = categoryId == EmojiCategory.ID_RECENTS; - mPrefs = prefs; } private Key getTemplateKey(final int code) { @@ -87,124 +76,56 @@ public int getColumnsCount() { return mColumnsNum; } - public void addPendingKey(final Key usedKey) { - synchronized (mLock) { - mPendingKeys.addLast(usedKey); - } + public int getKeyCount() { + return mGridKeys.size(); } - public void flushPendingRecentKeys() { - synchronized (mLock) { - while (!mPendingKeys.isEmpty()) { - addKey(mPendingKeys.pollFirst(), true); - } - saveRecentKeys(); - } - } - - public void addKeyFirst(final Key usedKey) { - addKey(usedKey, true); - if (mIsRecents) { - saveRecentKeys(); + public void addKeyFrom(final Key baseKey) { + if (baseKey == null) { + return; } + addKey(makeGridKey(baseKey)); } - public void addKeyLast(final Key usedKey) { - addKey(usedKey, false); - } - - private void addKey(final Key usedKey, final boolean addFirst) { - if (usedKey == null) { + public void addKey(final GridKey key) { + if (key == null) { return; } synchronized (mLock) { mCachedGridKeys = null; - // When a key is added to recents keyboard, we don't want to keep its more keys - // neither its hint label. Also, we make sure its background type is matching our keyboard - // if key comes from another keyboard (ie. a {@link MoreKeysKeyboard}). - final boolean dropMoreKeys = mIsRecents; - // Check if hint was a more emoji indicator and prevent its copy if more keys aren't copied - final boolean dropHintLabel = dropMoreKeys && "\u25E5".equals(usedKey.getHintLabel()); - final GridKey key = new GridKey(usedKey, - dropMoreKeys ? null : usedKey.getMoreKeys(), - dropHintLabel ? null : usedKey.getHintLabel(), - mIsRecents ? Key.BACKGROUND_TYPE_EMPTY : usedKey.getBackgroundType()); while (mGridKeys.remove(key)) { // Remove duplicate keys. } - if (addFirst) { - mGridKeys.addFirst(key); - } else { - mGridKeys.addLast(key); - } - while (mGridKeys.size() > mMaxKeyCount) { - mGridKeys.removeLast(); - } - int index = 0; - for (final GridKey gridKey : mGridKeys) { - final int keyX0 = getKeyX0(index); - final int keyY0 = getKeyY0(index); - final int keyX1 = getKeyX1(index); - final int keyY1 = getKeyY1(index); - gridKey.updateCoordinates(keyX0, keyY0, keyX1, keyY1); - index++; - } + mGridKeys.add(key); } + updateKeysCoordinates(); } - private void saveRecentKeys() { - final ArrayList keys = new ArrayList<>(); - for (final Key key : mGridKeys) { - if (key.getOutputText() != null) { - keys.add(key.getOutputText()); - } else { - keys.add(key.getCode()); - } - } - final String jsonStr = JsonUtils.listToJsonStr(keys); - Settings.writeEmojiRecentKeys(mPrefs, jsonStr); + public void removeLastKey() { + mGridKeys.removeLast(); } - private static Key getKeyByCode(final Collection keyboards, - final int code) { - for (final DynamicGridKeyboard keyboard : keyboards) { - for (final Key key : keyboard.getSortedKeys()) { - if (key.getCode() == code) { - return key; - } - } - } - return null; + public void sortKeys(Comparator comparator) { + Collections.sort(mGridKeys, comparator); + updateKeysCoordinates(); } - private static Key getKeyByOutputText(final Collection keyboards, - final String outputText) { - for (final DynamicGridKeyboard keyboard : keyboards) { - for (final Key key : keyboard.getSortedKeys()) { - if (outputText.equals(key.getOutputText())) { - return key; - } - } - } - return null; + protected GridKey makeGridKey(Key baseKey) { + return new GridKey(baseKey, + baseKey.getMoreKeys(), + baseKey.getHintLabel(), + baseKey.getBackgroundType()); } - public void loadRecentKeys(final Collection keyboards) { - final String str = Settings.readEmojiRecentKeys(mPrefs); - final List keys = JsonUtils.jsonStrToList(str); - for (final Object o : keys) { - final Key key; - if (o instanceof Integer) { - final int code = (Integer)o; - key = getKeyByCode(keyboards, code); - } else if (o instanceof String) { - final String outputText = (String)o; - key = getKeyByOutputText(keyboards, outputText); - } else { - Log.w(TAG, "Invalid object: " + o); - continue; - } - addKeyLast(key); + private void updateKeysCoordinates() { + int index = 0; + for (final GridKey gridKey : mGridKeys) { + final int keyX0 = getKeyX0(index); + final int keyY0 = getKeyY0(index); + final int keyX1 = getKeyX1(index); + final int keyY1 = getKeyY1(index); + gridKey.updateCoordinates(keyX0, keyY0, keyX1, keyY1); + index++; } } @@ -234,7 +155,7 @@ public List getSortedKeys() { if (mCachedGridKeys != null) { return mCachedGridKeys; } - final ArrayList cachedKeys = new ArrayList(mGridKeys); + final ArrayList cachedKeys = new ArrayList<>(mGridKeys); mCachedGridKeys = Collections.unmodifiableList(cachedKeys); return mCachedGridKeys; } @@ -246,7 +167,7 @@ public List getNearestKeys(final int x, final int y) { return getSortedKeys(); } - static final class GridKey extends Key { + protected static class GridKey extends Key { private int mCurrentX; private int mCurrentY; @@ -280,6 +201,11 @@ public boolean equals(final Object o) { return TextUtils.equals(getOutputText(), key.getOutputText()); } + @Override + public int hashCode() { + return Objects.hash(getCode(), getLabel(), getOutputText()); + } + @Override public String toString() { return "GridKey: " + super.toString(); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiCategory.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiCategory.java index 1a61ed984..85561a001 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiCategory.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiCategory.java @@ -16,13 +16,13 @@ package org.dslul.openboard.inputmethod.keyboard.emoji; +import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Paint; import android.graphics.Rect; import android.util.Log; - import androidx.core.graphics.PaintCompat; import org.dslul.openboard.inputmethod.keyboard.Key; import org.dslul.openboard.inputmethod.keyboard.Keyboard; @@ -30,6 +30,7 @@ import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet; import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.settings.Settings; +import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils; import java.util.ArrayList; import java.util.Collections; @@ -119,6 +120,7 @@ public CategoryProperties(final int categoryId, final int pageCount) { private final SharedPreferences mPrefs; private final Resources mRes; + private final RecentEmojiDbHelper mRecentEmojiDbHelper; private final int mMaxRecentsKeyCount; private final KeyboardLayoutSet mLayoutSet; private final HashMap mCategoryNameToIdMap = new HashMap<>(); @@ -130,11 +132,12 @@ public CategoryProperties(final int categoryId, final int pageCount) { private int mCurrentCategoryId = EmojiCategory.ID_UNSPECIFIED; private int mCurrentCategoryPageId = 0; - public EmojiCategory(final SharedPreferences prefs, final Resources res, - final KeyboardLayoutSet layoutSet, final TypedArray emojiPaletteViewAttr) { - mPrefs = prefs; - mRes = res; - mMaxRecentsKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_recents_key_count); + public EmojiCategory(final Context context, final KeyboardLayoutSet layoutSet, + final TypedArray emojiPaletteViewAttr) { + mPrefs = DeviceProtectedUtils.getSharedPreferences(context); + mRes = context.getResources(); + mRecentEmojiDbHelper = new RecentEmojiDbHelper(context); + mMaxRecentsKeyCount = mRes.getInteger(R.integer.config_emoji_keyboard_max_recents_key_count); mLayoutSet = layoutSet; for (int i = 0; i < sCategoryName.length; ++i) { mCategoryNameToIdMap.put(sCategoryName[i], i); @@ -157,16 +160,12 @@ public EmojiCategory(final SharedPreferences prefs, final Resources res, } addShownCategoryId(EmojiCategory.ID_EMOTICONS); - DynamicGridKeyboard recentsKbd = - getKeyboard(EmojiCategory.ID_RECENTS, 0 /* categoryPageId */); - recentsKbd.loadRecentKeys(mCategoryKeyboardMap.values()); - mCurrentCategoryId = Settings.readLastShownEmojiCategoryId(mPrefs, defaultCategoryId); mCurrentCategoryPageId = Settings.readLastShownEmojiCategoryPageId(mPrefs, 0); if (!isShownCategoryId(mCurrentCategoryId)) { mCurrentCategoryId = defaultCategoryId; } else if (mCurrentCategoryId == EmojiCategory.ID_RECENTS && - recentsKbd.getSortedKeys().isEmpty()) { + Settings.readEmojiRecentCount(mPrefs) == 0) { mCurrentCategoryId = defaultCategoryId; } @@ -309,9 +308,10 @@ public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) { } if (categoryId == EmojiCategory.ID_RECENTS) { - final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs, + final DynamicGridKeyboard kbd = new RecentEmojiKeyboard(mRecentEmojiDbHelper, mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), - mMaxRecentsKeyCount, categoryId); + mCategoryKeyboardMap.values(), + mMaxRecentsKeyCount); mCategoryKeyboardMap.put(categoryKeyboardMapKey, kbd); return kbd; } @@ -321,14 +321,13 @@ public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) { final Key[][] sortedKeysPages = sortKeysGrouped( keyboard.getSortedKeys(), keyCountPerPage); for (int pageId = 0; pageId < sortedKeysPages.length; ++pageId) { - final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs, - mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), - keyCountPerPage, categoryId); + final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard( + mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS)); for (final Key emojiKey : sortedKeysPages[pageId]) { if (emojiKey == null) { break; } - tempKeyboard.addKeyLast(emojiKey); + tempKeyboard.addKeyFrom(emojiKey); } mCategoryKeyboardMap.put( getCategoryKeyboardMapKey(categoryId, pageId), tempKeyboard); @@ -337,10 +336,13 @@ public DynamicGridKeyboard getKeyboard(final int categoryId, final int id) { } } + public RecentEmojiKeyboard getRecentEmojiKeyboard() { + return (RecentEmojiKeyboard) getKeyboard(EmojiCategory.ID_RECENTS, 0 /* categoryPageId */); + } + private int computeMaxKeyCountPerPage() { - final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs, - mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), - 0, 0); + final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard( + mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS)); return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getColumnsCount(); } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java index 6b0b546e0..35b74c6c7 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesAdapter.java @@ -21,57 +21,26 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - -import org.dslul.openboard.inputmethod.keyboard.Key; -import org.dslul.openboard.inputmethod.keyboard.Keyboard; -import org.dslul.openboard.inputmethod.keyboard.KeyboardView; -import org.dslul.openboard.inputmethod.latin.R; - import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import org.dslul.openboard.inputmethod.latin.settings.Settings; +import org.dslul.openboard.inputmethod.keyboard.Keyboard; +import org.dslul.openboard.inputmethod.latin.R; final class EmojiPalettesAdapter extends RecyclerView.Adapter{ private static final String TAG = EmojiPalettesAdapter.class.getSimpleName(); private static final boolean DEBUG_PAGER = false; private final OnKeyEventListener mListener; - private final DynamicGridKeyboard mRecentsKeyboard; private final SparseArray mActiveKeyboardViews = new SparseArray<>(); private final EmojiCategory mEmojiCategory; - private int mActivePosition = 0; + private final LinearLayoutManager mLayoutManager; - public EmojiPalettesAdapter(final EmojiCategory emojiCategory, + public EmojiPalettesAdapter(final EmojiCategory emojiCategory, final LinearLayoutManager layoutManager, final OnKeyEventListener listener) { mEmojiCategory = emojiCategory; + mLayoutManager = layoutManager; mListener = listener; - mRecentsKeyboard = mEmojiCategory.getKeyboard(EmojiCategory.ID_RECENTS, 0); - } - - public void flushPendingRecentKeys() { - mRecentsKeyboard.flushPendingRecentKeys(); - final KeyboardView recentKeyboardView = - mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId()); - if (recentKeyboardView != null) { - recentKeyboardView.invalidateAllKeys(); - } - } - - public void addRecentKey(final Key key) { - if (Settings.getInstance().getCurrent().mIncognitoModeEnabled) { - // We do not want to log recent keys while being in incognito - return; - } - if (mEmojiCategory.isInRecentTab()) { - mRecentsKeyboard.addPendingKey(key); - return; - } - mRecentsKeyboard.addKeyFirst(key); - final KeyboardView recentKeyboardView = - mActiveKeyboardViews.get(mEmojiCategory.getRecentTabId()); - if (recentKeyboardView != null) { - recentKeyboardView.invalidateAllKeys(); - } } public void onPageScrolled() { @@ -81,12 +50,28 @@ public void onPageScrolled() { public void releaseCurrentKey(final boolean withKeyRegistering) { // Make sure the delayed key-down event (highlight effect and haptic feedback) will be // canceled. - final EmojiPageKeyboardView currentKeyboardView = - mActiveKeyboardViews.get(mActivePosition); - if (currentKeyboardView == null) { + final int first = mLayoutManager.findFirstVisibleItemPosition(); + final int last = mLayoutManager.findLastVisibleItemPosition(); + if (first == RecyclerView.NO_POSITION || last == RecyclerView.NO_POSITION || last < first) { return; } - currentKeyboardView.releaseCurrentKey(withKeyRegistering); + for (int i = first; i <= last; i++) { + final EmojiPageKeyboardView keyboardView = mActiveKeyboardViews.get(i); + keyboardView.releaseCurrentKey(withKeyRegistering); + } + } + + public void invalidateVisibleKeyboardViews() { + final int first = mLayoutManager.findFirstVisibleItemPosition(); + final int last = mLayoutManager.findLastVisibleItemPosition(); + if (first == RecyclerView.NO_POSITION || last == RecyclerView.NO_POSITION || last < first) { + return; + } + for (int i = first; i <= last; i++) { + final EmojiPageKeyboardView keyboardView = mActiveKeyboardViews.get(i); + keyboardView.invalidateAllKeys(); + keyboardView.requestLayout(); + } } /* diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesView.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesView.java index 4d904d3a9..f830ef38c 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/EmojiPalettesView.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.util.AttributeSet; +import android.util.SparseArray; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; @@ -32,7 +33,6 @@ import android.widget.TabHost.OnTabChangeListener; import android.widget.TabWidget; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -45,14 +45,11 @@ import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes; import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet; import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager; +import org.dslul.openboard.inputmethod.latin.LatinIME; import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype; import org.dslul.openboard.inputmethod.latin.common.Constants; -import org.dslul.openboard.inputmethod.latin.settings.Settings; -import org.dslul.openboard.inputmethod.latin.settings.SettingsValues; -import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils; import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils; - import org.jetbrains.annotations.NotNull; import static org.dslul.openboard.inputmethod.latin.common.Constants.NOT_A_COORDINATE; @@ -122,8 +119,7 @@ public EmojiPalettesView(final Context context, final AttributeSet attrs, final final KeyboardLayoutSet layoutSet = builder.build(); final TypedArray emojiPalettesViewAttr = context.obtainStyledAttributes(attrs, R.styleable.EmojiPalettesView, defStyle, R.style.EmojiPalettesView); - mEmojiCategory = new EmojiCategory(DeviceProtectedUtils.getSharedPreferences(context), - res, layoutSet, emojiPalettesViewAttr); + mEmojiCategory = new EmojiCategory(context, layoutSet, emojiPalettesViewAttr); mCategoryIndicatorEnabled = emojiPalettesViewAttr.getBoolean( R.styleable.EmojiPalettesView_categoryIndicatorEnabled, false); mCategoryIndicatorDrawableResId = emojiPalettesViewAttr.getResourceId( @@ -187,8 +183,7 @@ protected void onFinishInflate() { tabWidget.setRightStripDrawable(mCategoryIndicatorBackgroundResId); } - mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this); - + mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, mEmojiLayoutManager, this); mEmojiRecyclerView = findViewById(R.id.emoji_keyboard_list); mEmojiRecyclerView.setLayoutManager(mEmojiLayoutManager); mEmojiRecyclerView.setAdapter(mEmojiPalettesAdapter); @@ -345,7 +340,14 @@ public void onPressKey(final Key key) { */ @Override public void onReleaseKey(final Key key) { - mEmojiPalettesAdapter.addRecentKey(key); + if (mEmojiCategory.isInRecentTab()) { + // Needs to save pending updates for recent keys when we get out of the recents + // category because we don't want to move the recent emojis around while the user + // is in the recents category. + mEmojiCategory.getRecentEmojiKeyboard().notifyEmojiUsedPending(key); + } else { + mEmojiCategory.getRecentEmojiKeyboard().notifyEmojiUsed(key); + } mEmojiCategory.saveLastTypedCategoryPage(); final int code = key.getCode(); if (code == Constants.CODE_OUTPUT_TEXT) { @@ -364,7 +366,7 @@ public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { } private static void setupAlphabetKey(final TextView alphabetKey, final String label, - final KeyDrawParams params) { + final KeyDrawParams params) { alphabetKey.setText(label); alphabetKey.setTextColor(params.mFunctionalTextColor); alphabetKey.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mLabelSize); @@ -391,14 +393,22 @@ public void startEmojiPalettes(final String switchToAlphaLabel, setCurrentCategoryAndPageId(mEmojiCategory.getCurrentCategoryId(), mEmojiCategory.getCurrentCategoryPageId(), true /* force */); } + mEmojiCategory.getRecentEmojiKeyboard().setListener(mOnRecentEmojiChangedListener); } public void stopEmojiPalettes() { + if (mEmojiCategory.isInRecentTab()) { + mEmojiCategory.getRecentEmojiKeyboard().applyPendingChanges(); + } + mEmojiCategory.getRecentEmojiKeyboard().setListener(null); mEmojiPalettesAdapter.releaseCurrentKey(true /* withKeyRegistering */); - mEmojiPalettesAdapter.flushPendingRecentKeys(); mEmojiRecyclerView.setAdapter(null); } + public void setUiHandler(final LatinIME.UIHandler handler) { + mEmojiCategory.getRecentEmojiKeyboard().loadRecentEmojis(handler); + } + public void setKeyboardActionListener(final KeyboardActionListener listener) { mKeyboardActionListener = listener; mDeleteKeyOnTouchListener.setKeyboardActionListener(listener); @@ -419,10 +429,7 @@ private void setCurrentCategoryAndPageId(final int categoryId, final int categor final int oldCategoryPageId = mEmojiCategory.getCurrentCategoryPageId(); if (oldCategoryId == EmojiCategory.ID_RECENTS && categoryId != EmojiCategory.ID_RECENTS) { - // Needs to save pending updates for recent keys when we get out of the recents - // category because we don't want to move the recent emojis around while the user - // is in the recents category. - mEmojiPalettesAdapter.flushPendingRecentKeys(); + mEmojiCategory.getRecentEmojiKeyboard().applyPendingChanges(); } if (force || oldCategoryId != categoryId || oldCategoryPageId != categoryPageId) { @@ -438,6 +445,19 @@ private void setCurrentCategoryAndPageId(final int categoryId, final int categor } } + public void onRecentEmojisAvailable(SparseArray recentEmojis) { + mEmojiCategory.getRecentEmojiKeyboard().onRecentEmojisAvailable(recentEmojis); + } + + private final RecentEmojiKeyboard.OnRecentEmojiChangedListener mOnRecentEmojiChangedListener = new RecentEmojiKeyboard.OnRecentEmojiChangedListener() { + @Override + public void onRecentEmojiChanged() { + if (mEmojiCategory.isInRecentTab()) { + mEmojiPalettesAdapter.invalidateVisibleKeyboardViews(); + } + } + }; + private static class DeleteKeyOnTouchListener implements OnTouchListener { private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmoji.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmoji.kt new file mode 100644 index 000000000..d38f0f235 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmoji.kt @@ -0,0 +1,78 @@ +package org.dslul.openboard.inputmethod.keyboard.emoji + +import org.dslul.openboard.inputmethod.keyboard.Key +import org.dslul.openboard.inputmethod.latin.common.Constants +import kotlin.math.log10 + +class RecentEmoji( + val code: Int = Constants.NOT_A_CODE, + val text: String? = null, + private var uses: LongArray = LongArray(0) + ) : Comparable { + + constructor(key: Key) : this(key.code, key.outputText) + + private var frequency = BASE_FREQUENCY + + val lastUse: Long + get() = uses.lastOrNull() ?: 0 + + val usesToStore: List + get() { + return if (uses.size > MIN_COUNT_FOR_NEGLIGIBLE_USES_REMOVAL) { + val now = now() + val usesWithMagnitudes = uses.associateWith { orderOfMagnitude(now, it) } + val magnitudeThreshold = usesWithMagnitudes.maxOf { it.value } + MAGNITUDE_RELATIVE_THRESHOLD_FOR_REMOVAL + usesWithMagnitudes.filterValues { it > magnitudeThreshold }.keys.toList() + } else { + uses.toList() + } + } + + fun addUseNow() { + uses += now() + } + + fun setSingleUseNow() { + uses = longArrayOf(now()) + } + + fun setSingleUseFromLastUse() { + if (uses.size > 1) { + uses = longArrayOf(lastUse) + } + } + + fun updateFrequency() { + val now = now() + frequency = uses.fold(BASE_FREQUENCY) { acc, it -> acc * computeUnitFrequency(now, it) } + } + + override fun compareTo(other: RecentEmoji): Int { + return other.frequency.compareTo(frequency) + } + + fun copy() = RecentEmoji(code, text).also { it.uses = uses.copyOf(uses.size) } + + companion object { + + private const val BASE_FREQUENCY = 1f + private const val BASE_USE_TIME = 24 * 3600 * 1000L // 12 hours in ms + private const val MIN_COUNT_FOR_NEGLIGIBLE_USES_REMOVAL = 32 + private const val MAGNITUDE_RELATIVE_THRESHOLD_FOR_REMOVAL = -3 + + fun now() = System.currentTimeMillis() + + // Ranges in [2.0; 1.0[ with 1.125 for BASE_USE_TIME + private fun computeUnitFrequency(now: Long, use: Long): Float { + val x = ((now - use) / BASE_USE_TIME.toFloat()).coerceAtLeast(0f) + // f(x)=1+(1/(x+1))**2 + val inv = 1f / (x + 1) + return 1f + inv * inv + } + + private fun orderOfMagnitude(now: Long, use: Long): Int { + return log10(computeUnitFrequency(now, use) - 1).toInt() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmojiDbHelper.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmojiDbHelper.kt new file mode 100644 index 000000000..78930e536 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmojiDbHelper.kt @@ -0,0 +1,168 @@ +package org.dslul.openboard.inputmethod.keyboard.emoji + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import android.util.Log +import android.util.SparseArray +import androidx.annotation.WorkerThread + +/** + * Class to deal with recent emojis database + */ +class RecentEmojiDbHelper(context: Context) : SQLiteOpenHelper( + context, + RECENT_EMOJI_DATABASE_NAME, + null, + RECENT_EMOJI_DATABASE_VERSION +) { + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL(LOG_TABLE_CREATE) + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + // Remove all data. + db.execSQL("DROP TABLE IF EXISTS $RECENT_EMOJI_TABLE_NAME") + onCreate(db) + } + + @WorkerThread + fun getRecentEmojis(): SparseArray { + return SparseArray().apply { + readAll(readableDatabase).use { cursor -> + while (cursor.moveToNext()) { + val hash = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_HASH)) + val code = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_CODE)) + val text = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_TEXT)) + val rawUses = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_USES)) + put(hash, RecentEmoji(code, text, decodeUses(rawUses))) + } + } + } + } + + @WorkerThread + fun addRecentEmoji(hash: Int, recentEmoji: RecentEmoji) { + val result = insert(writableDatabase, hash, recentEmoji.code, recentEmoji.text, recentEmoji.usesToStore) + if (result == INVALID_ID) { + throw IllegalStateException("Trying to add a recent emoji that " + + "is already in database for hash=$hash") + } + } + + @WorkerThread + fun updateRecentEmojiUses(hash: Int, uses: List) { + val affectedRows = updateUses(writableDatabase, hash, uses) + if (affectedRows == 0) { + throw IllegalStateException("Trying to update a recent emoji that " + + "is not yet in database for hash=$hash") + } else if (affectedRows > 1) { + Log.w(TAG, "Updating recent emoji for hash=$hash led in " + + "$affectedRows rows modified.") + } + } + + @WorkerThread + fun removeRecentEmoji(hash: Int) { + val affectedRows = delete(writableDatabase, hash) + if (affectedRows == 0) { + throw IllegalStateException("Trying to remove a recent emoji that " + + "is not yet in database for hash=$hash") + } else if (affectedRows > 1) { + Log.w(TAG, "Removing recent emoji for hash=$hash led in " + + "$affectedRows rows deleted.") + } + } + + companion object { + + private const val TAG = "RecentEmojiDbHelper" + + private const val RECENT_EMOJI_DATABASE_NAME = "recentEmoji" + private const val RECENT_EMOJI_DATABASE_VERSION = 1 + private const val RECENT_EMOJI_TABLE_NAME = "recentEmoji" + private const val COLUMN_COUNT = 4 + private const val COLUMN_HASH = "hash" + private const val COLUMN_CODE = "code" + private const val COLUMN_TEXT = "text" + private const val COLUMN_USES = "uses" + + private const val USES_SEPARATOR = ';' + private const val EMPTY_STRING = "" + private const val INVALID_ID = -1L + + private const val LOG_TABLE_CREATE = ("CREATE TABLE " + RECENT_EMOJI_TABLE_NAME + " (" + + COLUMN_HASH + " INTEGER," + + COLUMN_CODE + " INTEGER," + + COLUMN_TEXT + " TEXT," + + COLUMN_USES + " TEXT," + + "PRIMARY KEY (" + COLUMN_HASH + "));") + + fun readAll(db: SQLiteDatabase): Cursor { + return db.query(RECENT_EMOJI_TABLE_NAME, + arrayOf(COLUMN_HASH, COLUMN_CODE, COLUMN_TEXT, COLUMN_USES), + null, + null, + null, + null, + null) + } + + fun insert(db: SQLiteDatabase, hash: Int, code: Int, text: String?, uses: List): Long { + val contentValues = ContentValues(COLUMN_COUNT).apply { + put(COLUMN_HASH, hash) + put(COLUMN_CODE, code) + if (text != null) put(COLUMN_TEXT, text) else putNull(COLUMN_TEXT) + put(COLUMN_USES, encodeUses(uses)) + } + return db.insert(RECENT_EMOJI_TABLE_NAME, null, contentValues) + } + + fun delete(db: SQLiteDatabase, hash: Int): Int { + return db.delete(RECENT_EMOJI_TABLE_NAME, + "$COLUMN_HASH=?", + arrayOf(hash.toString())) + } + + fun updateUses(db: SQLiteDatabase, hash: Int, uses: List): Int { + val contentValues = ContentValues(1).apply { + put(COLUMN_USES, encodeUses(uses)) + } + return db.update(RECENT_EMOJI_TABLE_NAME, + contentValues, + "$COLUMN_HASH=?", + arrayOf(hash.toString())) + } + + private fun encodeUses(uses: List): String { + if (uses.isEmpty()) return EMPTY_STRING + if (uses.size == 1) return uses.first().toString() + val builder = StringBuilder() + val base = uses.first() + builder.append(base) + uses.drop(1).forEach { use -> + builder.append(USES_SEPARATOR) + builder.append(use - base) + } + return builder.toString() + } + + private fun decodeUses(rawString: String): LongArray { + val count = rawString.count { it == USES_SEPARATOR } + 1 + val uses = LongArray(count) + var base = 0L + rawString.split(USES_SEPARATOR).forEachIndexed { index, part -> + if (index == 0) { + base = part.toLong() + uses[0] = base + } else { + uses[index] = base + part.toLong() + } + } + return uses + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmojiKeyboard.kt b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmojiKeyboard.kt new file mode 100644 index 000000000..9297fd222 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/emoji/RecentEmojiKeyboard.kt @@ -0,0 +1,207 @@ +package org.dslul.openboard.inputmethod.keyboard.emoji + +import android.util.Log +import android.util.SparseArray +import androidx.core.util.containsKey +import androidx.core.util.forEach +import androidx.core.util.valueIterator +import org.dslul.openboard.inputmethod.keyboard.Key +import org.dslul.openboard.inputmethod.keyboard.Keyboard +import org.dslul.openboard.inputmethod.latin.LatinIME +import org.dslul.openboard.inputmethod.latin.settings.Settings +import org.dslul.openboard.inputmethod.latin.utils.ExecutorUtils +import org.dslul.openboard.inputmethod.latin.utils.getOrPut +import java.util.* + +class RecentEmojiKeyboard( + private val recentEmojiDbHelper: RecentEmojiDbHelper, + templateKeyboard: Keyboard?, + private var emojiBaseKeyboards: Collection, + private val maxKeyCount: Int +) : DynamicGridKeyboard(templateKeyboard) { + + var listener: OnRecentEmojiChangedListener? = null + + private val recentEmojis = SparseArray() + private val pendingKeys = ArrayDeque() + private var hasLoadedRecentEmojis = false + private var emojiUsageSaveEnabled = false + + private val gridKeyComparator by lazy { + Comparator { key1, key2 -> + val e1 = recentEmojis[key1.hashCode()] + val e2 = recentEmojis[key2.hashCode()] + // Null cases should never append in theory, but we do not want to crash the + // whole IME if we have a desync between keyboard data and our recent emojis + when { + e1 == null && e2 != null -> -1 + e1 != null && e2 == null -> +1 + e1 == null && e2 == null -> 0 + else -> e1.compareTo(e2) + } + } + } + + fun loadRecentEmojis(handler: LatinIME.UIHandler) = startLoadRecentEmojis(handler) + + fun onRecentEmojisAvailable(array: SparseArray) { + for (i in array.size() - 1 downTo 0) { + val recentEmoji = array.valueAt(i) + val key = if (recentEmoji.text != null) getBaseKeyByOutputText(recentEmoji.text) + else getBaseKeyByCode(recentEmoji.code) + if (key != null) { + recentEmojis.put(array.keyAt(i), recentEmoji) + addKeyFrom(key) + recentEmoji.updateFrequency() + } else { + Log.i(TAG, "No base key found for: code=${recentEmoji.code} text=${recentEmoji.text}") + startRemoveRecentEmoji(array.keyAt(i)) + } + } + sortKeys(gridKeyComparator) + hasLoadedRecentEmojis = true + listener?.onRecentEmojiChanged() + } + + fun notifyEmojiUsed(baseKey: Key) { + if (!hasLoadedRecentEmojis || Settings.getInstance().current.mIncognitoModeEnabled) { + // We do not want to log recent keys while being in incognito or waiting for our initial setup + return + } + val exists = updateRecentEmojiFromKey(baseKey) + if (!exists) { + addKeyFrom(baseKey) + } + sortKeys(gridKeyComparator) + listener?.onRecentEmojiChanged() + } + + fun notifyEmojiUsedPending(baseKey: Key) { + if (!hasLoadedRecentEmojis || Settings.getInstance().current.mIncognitoModeEnabled) { + // We do not want to log recent keys while being in incognito or waiting for our initial setup + return + } + updateRecentEmojiFromKey(baseKey) + pendingKeys.add(baseKey) + } + + fun applyPendingChanges() { + if (pendingKeys.isEmpty()) { + return + } + while (pendingKeys.isNotEmpty()) { + addKeyFrom(pendingKeys.remove()) + } + sortKeys(gridKeyComparator) + listener?.onRecentEmojiChanged() + } + + private fun updateRecentEmojiFromKey(baseKey: Key): Boolean { + checkEmojiUsageSaveEnabled() + val hash = Objects.hash(baseKey.code, baseKey.label, baseKey.outputText) + val exists = recentEmojis.containsKey(hash) + val recentEmoji = recentEmojis.getOrPut(hash) { + RecentEmoji(baseKey).also { startAddNewEmoji(hash, it) } + } + if (emojiUsageSaveEnabled) { + recentEmoji.addUseNow() + } else { + recentEmoji.setSingleUseNow() + } + startUpdateEmojisUses(hash, recentEmoji) + + // Update frequency of every emoji to keep them comparable + recentEmojis.forEach { _, item -> item.updateFrequency() } + return exists + } + + private fun getBaseKeyByCode(code: Int) = emojiBaseKeyboards.firstNotNullOfOrNull { + it.sortedKeys.firstOrNull { key -> key.code == code } + } + + private fun getBaseKeyByOutputText(outputText: String) = emojiBaseKeyboards.firstNotNullOfOrNull { + it.sortedKeys.firstOrNull { key -> key.outputText == outputText } + } + + private fun checkEmojiUsageSaveEnabled() { + val oldValue = emojiUsageSaveEnabled + val newValue = Settings.getInstance().current.mEmojiUsageFreqEnabled + if (oldValue != newValue) { + emojiUsageSaveEnabled = newValue + if (!newValue) { + // Setting is now off, we must forget dynamically usage histories to compute the new simple order. + recentEmojis.forEach { hash, recentEmoji -> + recentEmoji.setSingleUseFromLastUse() + startUpdateEmojisUses(hash, recentEmoji) + } + } + } + } + + override fun addKeyFrom(usedKey: Key?) { + super.addKeyFrom(usedKey) + if (keyCount > maxKeyCount) { + sortKeys(gridKeyComparator) + while (keyCount > maxKeyCount) { + removeLastKey() + // Find recent emoji with the smallest frequency, corresponding to + // the last key we juste removed. + val recentEmoji = recentEmojis.valueIterator().asSequence().sorted().last() + val index = recentEmojis.indexOfValue(recentEmoji) + recentEmojis.removeAt(index) + val hash = recentEmojis.keyAt(index) + startRemoveRecentEmoji(hash) + } + } + } + + override fun makeGridKey(baseKey: Key): GridKey { + // When a key is added to recents keyboard, we don't want to keep its more keys + // neither its hint label. Also, we make sure its background type is matching our keyboard + // if key comes from another keyboard (ie. a {@link MoreKeysKeyboard}). + val dropMoreKeys = true + // Check if hint was a more emoji indicator and prevent its copy if more keys aren't copied + val dropHintLabel = dropMoreKeys && "\u25E5" == baseKey.hintLabel + return GridKey(baseKey, + if (dropMoreKeys) null else baseKey.moreKeys, + if (dropHintLabel) null else baseKey.hintLabel, + Key.BACKGROUND_TYPE_EMPTY) + } + + private fun startLoadRecentEmojis(handler: LatinIME.UIHandler) { + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.EMOJIS).execute { + val result = recentEmojiDbHelper.getRecentEmojis() + handler.postUpdateRecentEmojis(result) + } + } + + private fun startAddNewEmoji(hash: Int, recentEmoji: RecentEmoji) { + Settings.getInstance().writeEmojiRecentCount(recentEmojis.size()) + val localCopy = recentEmoji.copy() + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.EMOJIS).execute { + recentEmojiDbHelper.addRecentEmoji(hash, localCopy) + } + } + + private fun startUpdateEmojisUses(hash: Int, recentEmoji: RecentEmoji) { + val localList = recentEmoji.usesToStore + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.EMOJIS).execute { + recentEmojiDbHelper.updateRecentEmojiUses(hash, localList) + } + } + + private fun startRemoveRecentEmoji(hash: Int) { + Settings.getInstance().writeEmojiRecentCount(recentEmojis.size()) + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.EMOJIS).execute { + recentEmojiDbHelper.removeRecentEmoji(hash) + } + } + + interface OnRecentEmojiChangedListener { + fun onRecentEmojiChanged() + } + + companion object { + private const val TAG = "RecentEmojiKeyboard" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java index 12ea21299..ca8f4de1d 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java @@ -47,7 +47,7 @@ import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodSubtype; - +import androidx.annotation.NonNull; import org.dslul.openboard.inputmethod.accessibility.AccessibilityUtils; import org.dslul.openboard.inputmethod.annotations.UsedForTesting; import org.dslul.openboard.inputmethod.compat.EditorInfoCompatUtils; @@ -63,6 +63,8 @@ import org.dslul.openboard.inputmethod.keyboard.KeyboardId; import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher; import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView; +import org.dslul.openboard.inputmethod.keyboard.emoji.EmojiPalettesView; +import org.dslul.openboard.inputmethod.keyboard.emoji.RecentEmoji; import org.dslul.openboard.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import org.dslul.openboard.inputmethod.latin.common.Constants; @@ -90,6 +92,7 @@ import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils; import org.dslul.openboard.inputmethod.latin.utils.ViewLayoutUtils; +import javax.annotation.Nonnull; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -97,8 +100,6 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; - import static org.dslul.openboard.inputmethod.latin.common.Constants.ImeOption.FORCE_ASCII; import static org.dslul.openboard.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE; import static org.dslul.openboard.inputmethod.latin.common.Constants.ImeOption.NO_MICROPHONE_COMPAT; @@ -229,6 +230,7 @@ public static final class UIHandler extends LeakGuardHandlerWrapper { private static final int MSG_RESUME_SUGGESTIONS_FOR_START_INPUT = 10; private static final int MSG_SWITCH_LANGUAGE_AUTOMATICALLY = 11; private static final int MSG_UPDATE_CLIPBOARD_PINNED_CLIPS = 12; + private static final int MSG_UPDATE_RECENT_EMOJIS = 13; // Update this when adding new messages private static final int MSG_LAST = MSG_UPDATE_CLIPBOARD_PINNED_CLIPS; @@ -329,9 +331,17 @@ public void handleMessage(final Message msg) { break; case MSG_UPDATE_CLIPBOARD_PINNED_CLIPS: @SuppressWarnings("unchecked") - List entries = (List) msg.obj; + final List entries = (List) msg.obj; latinIme.mClipboardHistoryManager.onPinnedClipsAvailable(entries); break; + case MSG_UPDATE_RECENT_EMOJIS: + EmojiPalettesView emojiPalettesView = latinIme.mInputView.findViewById(R.id.emoji_palettes_view); + if (emojiPalettesView != null) { + @SuppressWarnings("unchecked") + final SparseArray array = (SparseArray) msg.obj; + emojiPalettesView.onRecentEmojisAvailable(array); + } + break; } } @@ -458,6 +468,10 @@ public void postUpdateClipboardPinnedClips(final List cli obtainMessage(MSG_UPDATE_CLIPBOARD_PINNED_CLIPS, clips).sendToTarget(); } + public void postUpdateRecentEmojis(@NonNull final SparseArray array) { + obtainMessage(MSG_UPDATE_RECENT_EMOJIS, array).sendToTarget(); + } + // Working variables for the following methods. private boolean mIsOrientationChanging; private boolean mPendingSuccessiveImsCallback; diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java index 135c6ceab..5318cb194 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/Settings.java @@ -24,7 +24,6 @@ import android.content.res.Resources; import android.os.Build; import android.util.Log; - import android.view.Gravity; import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager; import org.dslul.openboard.inputmethod.latin.InputAttributes; @@ -37,13 +36,12 @@ import org.dslul.openboard.inputmethod.latin.utils.RunInLocale; import org.dslul.openboard.inputmethod.latin.utils.StatsUtils; +import javax.annotation.Nonnull; import java.util.Collections; import java.util.Locale; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; -import javax.annotation.Nonnull; - public final class Settings implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = Settings.class.getSimpleName(); // Settings screens @@ -144,6 +142,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_EMOJI_CATEGORY_LAST_TYPED_ID = "emoji_category_last_typed_id"; public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_ID = "last_shown_emoji_category_id"; public static final String PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID = "last_shown_emoji_category_page_id"; + public static final String PREF_EMOJI_USAGE_FREQ = "pref_emoji_usage_freq"; private static final float UNDEFINED_PREFERENCE_VALUE_FLOAT = -1.0f; private static final int UNDEFINED_PREFERENCE_VALUE_INT = -1; @@ -174,6 +173,7 @@ private void onCreate(final Context context) { mPrefs = DeviceProtectedUtils.getSharedPreferences(context); mPrefs.registerOnSharedPreferenceChangeListener(this); upgradeAutocorrectionSettings(mPrefs, mRes); + checkLegacyEmojiRecentKeys(mPrefs); } public void onDestroy() { @@ -430,6 +430,10 @@ public void writeOneHandedModeGravity(final int gravity) { mPrefs.edit().putInt(PREF_ONE_HANDED_GRAVITY, gravity).apply(); } + public void writeEmojiRecentCount(final int count) { + writeEmojiRecentCount(mPrefs, count); + } + public static boolean readHasHardwareKeyboard(final Configuration conf) { // The standard way of finding out whether we have a hardware keyboard. This code is taken // from InputMethodService#onEvaluateInputShown, which canonically determines this. @@ -474,12 +478,12 @@ public Set readCorpusHandlesForPersonalization() { return mPrefs.getStringSet(PREF_CORPUS_HANDLES_FOR_PERSONALIZATION, emptySet); } - public static void writeEmojiRecentKeys(final SharedPreferences prefs, String str) { - prefs.edit().putString(PREF_EMOJI_RECENT_KEYS, str).apply(); + public static void writeEmojiRecentCount(final SharedPreferences prefs, final int c) { + prefs.edit().putInt(PREF_EMOJI_RECENT_KEYS, c).apply(); } - public static String readEmojiRecentKeys(final SharedPreferences prefs) { - return prefs.getString(PREF_EMOJI_RECENT_KEYS, ""); + public static int readEmojiRecentCount(final SharedPreferences prefs) { + return prefs.getInt(PREF_EMOJI_RECENT_KEYS, 0); } public static void writeLastTypedEmojiCategoryPageId( @@ -514,6 +518,10 @@ public static int readLastShownEmojiCategoryPageId( return prefs.getInt(PREF_LAST_SHOWN_EMOJI_CATEGORY_PAGE_ID, defValue); } + public static boolean readEmojiUsageFrequencyEnabled(final SharedPreferences prefs) { + return prefs.getBoolean(PREF_EMOJI_USAGE_FREQ, true); + } + private void upgradeAutocorrectionSettings(final SharedPreferences prefs, final Resources res) { final String thresholdSetting = prefs.getString(PREF_AUTO_CORRECTION_THRESHOLD_OBSOLETE, null); @@ -530,4 +538,12 @@ private void upgradeAutocorrectionSettings(final SharedPreferences prefs, final editor.commit(); } } + + private void checkLegacyEmojiRecentKeys(final SharedPreferences prefs) { + try { + prefs.getInt(PREF_EMOJI_RECENT_KEYS, 0); + } catch (ClassCastException e) { + prefs.edit().remove(PREF_EMOJI_RECENT_KEYS).apply(); + } + } } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/SettingsValues.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/SettingsValues.java index e639a6e3d..f20794254 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/SettingsValues.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/settings/SettingsValues.java @@ -23,22 +23,19 @@ import android.content.res.Resources; import android.util.Log; import android.view.inputmethod.EditorInfo; - import org.dslul.openboard.inputmethod.compat.AppWorkaroundsUtils; import org.dslul.openboard.inputmethod.latin.InputAttributes; import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.RichInputMethodManager; -import org.dslul.openboard.inputmethod.latin.common.StringUtils; import org.dslul.openboard.inputmethod.latin.utils.AsyncResultHolder; import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils; import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils; import org.dslul.openboard.inputmethod.latin.utils.TargetPackageInfoGetterTask; -import java.util.Arrays; -import java.util.Locale; - import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Locale; /** * When you call the constructor of this class, you may want to change the current system locale by @@ -93,6 +90,7 @@ public class SettingsValues { public final boolean mSlidingKeyInputPreviewEnabled; public final int mKeyLongpressTimeout; public final boolean mEnableEmojiAltPhysicalKey; + public final boolean mEmojiUsageFreqEnabled; public final boolean mShowAppIcon; public final boolean mIsShowAppIconSettingInPreferences; public final boolean mCloudSyncEnabled; @@ -181,6 +179,7 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina mKeypressSoundVolume = Settings.readKeypressSoundVolume(prefs, res); mEnableEmojiAltPhysicalKey = prefs.getBoolean( Settings.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY, true); + mEmojiUsageFreqEnabled = Settings.readEmojiUsageFrequencyEnabled(prefs); mShowAppIcon = Settings.readShowSetupWizardIcon(prefs, context); mIsShowAppIconSettingInPreferences = prefs.contains(Settings.PREF_SHOW_SETUP_WIZARD_ICON); mAutoCorrectionThreshold = readAutoCorrectionThreshold(res, diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ExecutorUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ExecutorUtils.java index 352418d69..c15477283 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ExecutorUtils.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/ExecutorUtils.java @@ -17,7 +17,6 @@ package org.dslul.openboard.inputmethod.latin.utils; import android.util.Log; - import org.dslul.openboard.inputmethod.annotations.UsedForTesting; import java.lang.Thread.UncaughtExceptionHandler; @@ -35,9 +34,11 @@ public class ExecutorUtils { public static final String KEYBOARD = "Keyboard"; public static final String SPELLING = "Spelling"; + public static final String EMOJIS = "RecentEmojis"; private static ScheduledExecutorService sKeyboardExecutorService = newExecutorService(KEYBOARD); private static ScheduledExecutorService sSpellingExecutorService = newExecutorService(SPELLING); + private static ScheduledExecutorService sEmojisExecutorService = newExecutorService(EMOJIS); private static ScheduledExecutorService newExecutorService(final String name) { return Executors.newSingleThreadScheduledExecutor(new ExecutorFactory(name)); @@ -89,6 +90,8 @@ public static ScheduledExecutorService getBackgroundExecutor(final String name) return sKeyboardExecutorService; case SPELLING: return sSpellingExecutorService; + case EMOJIS: + return sEmojisExecutorService; default: throw new IllegalArgumentException("Invalid executor: " + name); } @@ -113,6 +116,8 @@ public static void killTasks(final String name) { case SPELLING: sSpellingExecutorService = newExecutorService(SPELLING); break; + case EMOJIS: + sEmojisExecutorService = newExecutorService(EMOJIS); default: throw new IllegalArgumentException("Invalid executor: " + name); } diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/SparseArrayUtils.kt b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/SparseArrayUtils.kt new file mode 100644 index 000000000..91736a294 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/utils/SparseArrayUtils.kt @@ -0,0 +1,33 @@ +package org.dslul.openboard.inputmethod.latin.utils + +import android.util.SparseArray +import androidx.core.util.forEach + +public inline fun SparseArray.filter(predicate: (Int, V) -> Boolean): SparseArray { + return filterTo(SparseArray(), predicate) +} + +public inline fun > SparseArray.filterTo(destination: C, predicate: (Int, V) -> Boolean): C { + forEach { key, value -> if (predicate(key, value)) destination.append(key, value) } + return destination +} + +public inline fun SparseArray.map(transform: (Int, V) -> R): SparseArray { + return mapTo(SparseArray(), transform) +} + +public inline fun > SparseArray.mapTo(destination: C, transform: (Int, V) -> R): C { + forEach { key, value -> destination.put(key, transform(key, value)) } + return destination +} + +public inline fun SparseArray.getOrPut(key: Int, defaultValue: () -> V): V { + val value = get(key) + return if (value == null) { + val answer = defaultValue() + put(key, answer) + answer + } else { + value + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6fd5fa0d..9a607e589 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -178,6 +178,10 @@ If disabled, clipboard key will paste clipboard content if any History retention time + + Save emojis usage frequency + + Allows recent emojis to be sorted accordingly to their frequency of use Delete swipe diff --git a/app/src/main/res/xml/prefs_screen_advanced.xml b/app/src/main/res/xml/prefs_screen_advanced.xml index 6d13600b3..d126511ed 100644 --- a/app/src/main/res/xml/prefs_screen_advanced.xml +++ b/app/src/main/res/xml/prefs_screen_advanced.xml @@ -76,6 +76,12 @@ android:summary="@string/delete_swipe_summary" android:defaultValue="true" /> + + Date: Wed, 1 Jun 2022 17:20:39 +0200 Subject: [PATCH 2/2] Fixed access to emoji palettes view through KeyboardSwitcher --- .../openboard/inputmethod/keyboard/KeyboardSwitcher.java | 4 ++++ .../java/org/dslul/openboard/inputmethod/latin/LatinIME.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java index fdea056b5..21f518cd0 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java @@ -511,6 +511,10 @@ public MainKeyboardView getMainKeyboardView() { return mKeyboardView; } + public EmojiPalettesView getEmojiPalettesView() { + return mEmojiPalettesView; + } + public void deallocateMemory() { if (mKeyboardView != null) { mKeyboardView.cancelAllOngoingEvents(); diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java index ca8f4de1d..1b4ca4b16 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java @@ -335,7 +335,7 @@ public void handleMessage(final Message msg) { latinIme.mClipboardHistoryManager.onPinnedClipsAvailable(entries); break; case MSG_UPDATE_RECENT_EMOJIS: - EmojiPalettesView emojiPalettesView = latinIme.mInputView.findViewById(R.id.emoji_palettes_view); + final EmojiPalettesView emojiPalettesView = switcher.getEmojiPalettesView(); if (emojiPalettesView != null) { @SuppressWarnings("unchecked") final SparseArray array = (SparseArray) msg.obj;