Skip to content

Commit

Permalink
Clipboard suggestions (#647)
Browse files Browse the repository at this point in the history
  • Loading branch information
codokie authored Jul 5, 2024
1 parent 21124a5 commit bdab98c
Show file tree
Hide file tree
Showing 17 changed files with 213 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package helium314.keyboard.compat;

import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.os.Build;

Expand All @@ -29,4 +30,11 @@ public static Long getClipTimestamp(ClipData cd) {
}
}

public static Boolean getClipSensitivity(final ClipDescription cd) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return cd != null && cd.getExtras() != null && cd.getExtras().getBoolean("android.content.extra.IS_SENSITIVE");
}
return null; // can't determine
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@ package helium314.keyboard.latin

import android.content.ClipboardManager
import android.content.Context
import android.text.InputType
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.core.view.isGone
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import helium314.keyboard.compat.ClipboardManagerCompat
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.common.ColorType
import helium314.keyboard.latin.common.isValidNumber
import helium314.keyboard.latin.databinding.ClipboardSuggestionBinding
import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.DeviceProtectedUtils
import helium314.keyboard.latin.utils.InputTypeUtils
import helium314.keyboard.latin.utils.ToolbarKey
import kotlin.collections.ArrayList

class ClipboardHistoryManager(
Expand All @@ -18,6 +30,7 @@ class ClipboardHistoryManager(

private lateinit var clipboardManager: ClipboardManager
private var onHistoryChangeListener: OnHistoryChangeListener? = null
private var clipboardSuggestionView: View? = null

fun onCreate() {
clipboardManager = latinIME.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
Expand All @@ -36,6 +49,7 @@ class ClipboardHistoryManager(
// Make sure we read clipboard content only if history settings is set
if (latinIME.mSettings.current?.mClipboardHistoryEnabled == true) {
fetchPrimaryClip()
dontShowCurrentSuggestion = false
}
}

Expand Down Expand Up @@ -90,6 +104,7 @@ class ClipboardHistoryManager(
if (onHistoryChangeListener != null) {
onHistoryChangeListener?.onClipboardHistoryEntriesRemoved(pos, count)
}
removeClipboardSuggestion()
}

fun canRemove(index: Int) = historyEntries.getOrNull(index)?.isPinned != true
Expand Down Expand Up @@ -131,6 +146,11 @@ class ClipboardHistoryManager(
return clipData.getItemAt(0)?.coerceToText(latinIME) ?: ""
}

private fun isClipSensitive(inputType: Int): Boolean {
ClipboardManagerCompat.getClipSensitivity(clipboardManager.primaryClip?.description)?.let { return it }
return InputTypeUtils.isPasswordInputType(inputType)
}

// pinned clips are stored in default shared preferences, not in device protected preferences!
private fun loadPinnedClips() {
val pinnedClipString = Settings.readPinnedClipString(latinIME)
Expand All @@ -156,8 +176,66 @@ class ClipboardHistoryManager(
fun onClipboardHistoryEntryMoved(from: Int, to: Int)
}

fun getClipboardSuggestionView(editorInfo: EditorInfo?, parent: ViewGroup?): View? {
// maybe no need to create a new view
// but a cache has to consider a few possible changes, so better don't implement without need
clipboardSuggestionView = null

// get the content, or return null
if (!latinIME.mSettings.current.mSuggestClipboardContent) return null
if (dontShowCurrentSuggestion) return null
if (parent == null) return null
val clipData = clipboardManager.primaryClip ?: return null
if (clipData.itemCount == 0 || clipData.description?.hasMimeType("text/*") == false) return null
val clipItem = clipData.getItemAt(0) ?: return null
val timeStamp = ClipboardManagerCompat.getClipTimestamp(clipData) ?: System.currentTimeMillis()
if (System.currentTimeMillis() - timeStamp > RECENT_TIME_MILLIS) return null
val content = clipItem.coerceToText(latinIME)
if (TextUtils.isEmpty(content)) return null
val inputType = editorInfo?.inputType ?: InputType.TYPE_NULL
if (InputTypeUtils.isNumberInputType(inputType) && !content.isValidNumber()) return null

// create the view
val binding = ClipboardSuggestionBinding.inflate(LayoutInflater.from(latinIME), parent, false)
val textView = binding.clipboardSuggestionText
textView.text = if (isClipSensitive(inputType)) "*".repeat(content.length) else content
val clipIcon = latinIME.mKeyboardSwitcher.keyboard.mIconsSet.getIconDrawable(ToolbarKey.CLIPBOARD.name.lowercase())
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(clipIcon, null, null, null)
textView.setOnClickListener {
dontShowCurrentSuggestion = true
latinIME.onTextInput(content.toString())
AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(KeyCode.NOT_SPECIFIED, it);
binding.root.isGone = true
}
val closeButton = binding.clipboardSuggestionClose
closeButton.setImageDrawable(latinIME.mKeyboardSwitcher.keyboard.mIconsSet.getIconDrawable(ToolbarKey.CLOSE_HISTORY.name.lowercase()))
closeButton.setOnClickListener { removeClipboardSuggestion() }

val colors = latinIME.mSettings.current.mColors
textView.setTextColor(colors.get(ColorType.KEY_TEXT))
clipIcon?.let { colors.setColor(it, ColorType.KEY_ICON) }
colors.setColor(closeButton, ColorType.REMOVE_SUGGESTION_ICON)
colors.setBackground(binding.root, ColorType.CLIPBOARD_SUGGESTION_BACKGROUND)

clipboardSuggestionView = binding.root
return clipboardSuggestionView
}

private fun removeClipboardSuggestion() {
dontShowCurrentSuggestion = true
val csv = clipboardSuggestionView ?: return
if (csv.parent != null && !csv.isGone) {
// clipboard view is shown ->
latinIME.setNeutralSuggestionStrip()
latinIME.mHandler.postResumeSuggestions(false)
}
csv.isGone = true
}

companion object {
// store pinned clips in companion object so they survive a keyboard switch (which destroys the current instance)
private val historyEntries: MutableList<ClipboardHistoryEntry> = ArrayList()
private var dontShowCurrentSuggestion: Boolean = false
const val RECENT_TIME_MILLIS = 3 * 60 * 1000L // 3 minutes (for clipboard suggestions)
}
}
33 changes: 26 additions & 7 deletions app/src/main/java/helium314/keyboard/latin/LatinIME.java
Original file line number Diff line number Diff line change
Expand Up @@ -1029,12 +1029,11 @@ void onStartInputViewInternal(final EditorInfo editorInfo, final boolean restart
// Space state must be updated before calling updateShiftState
switcher.requestUpdatingShiftState(getCurrentAutoCapsState(), getCurrentRecapitalizeState());
}
// This will set the punctuation suggestions if next word suggestion is off;
// otherwise it will clear the suggestion strip.
// Set neutral suggestions and show the toolbar if the "Auto show toolbar" setting is enabled.
if (!mHandler.hasPendingResumeSuggestions()) {
mHandler.cancelUpdateSuggestionStrip();
setNeutralSuggestionStrip();
if (hasSuggestionStripView() && currentSettingsValues.mAutoShowToolbar) {
if (hasSuggestionStripView() && currentSettingsValues.mAutoShowToolbar && !tryShowClipboardSuggestion()) {
mSuggestionStripView.setToolbarVisibility(true);
}
}
Expand Down Expand Up @@ -1330,7 +1329,7 @@ public boolean onInlineSuggestionsResponse(InlineSuggestionsResponse response) {
// Without this function the inline autofill suggestions will not be visible
mHandler.cancelResumeSuggestions();

mSuggestionStripView.setInlineSuggestionsView(inlineSuggestionView);
mSuggestionStripView.setExternalSuggestionView(inlineSuggestionView);

return true;
}
Expand Down Expand Up @@ -1652,13 +1651,33 @@ public void pickSuggestionManually(final SuggestedWordInfo suggestionInfo) {
updateStateAfterInputTransaction(completeInputTransaction);
}

// This will show either an empty suggestion strip (if prediction is enabled) or
// punctuation suggestions (if it's disabled).
// The toolbar will be shown automatically if the relevant setting is enabled
/**
* Checks if a recent clipboard suggestion is available. If available, it is set in suggestion strip.
* returns whether a clipboard suggestion has been set.
*/
public boolean tryShowClipboardSuggestion() {
final View clipboardView = mClipboardHistoryManager.getClipboardSuggestionView(getCurrentInputEditorInfo(), mSuggestionStripView);
if (clipboardView != null && hasSuggestionStripView()) {
mSuggestionStripView.setExternalSuggestionView(clipboardView);
return true;
}
return false;
}

// This will first try showing a clipboard suggestion. On success, the toolbar will be hidden
// if the "Auto hide toolbar" is enabled. Otherwise, an empty suggestion strip (if prediction
// is enabled) or punctuation suggestions (if it's disabled) will be set.
// Then, the toolbar will be shown automatically if the relevant setting is enabled
// and there is a selection of text or it's the start of a line.
@Override
public void setNeutralSuggestionStrip() {
final SettingsValues currentSettings = mSettings.getCurrent();
if (tryShowClipboardSuggestion()) {
// clipboard suggestion has been set
if (hasSuggestionStripView() && currentSettings.mAutoHideToolbar)
mSuggestionStripView.setToolbarVisibility(false);
return;
}
final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled
? SuggestedWords.getEmptyInstance()
: currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/helium314/keyboard/latin/common/Colors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ class DynamicColors(context: Context, override val themeStyle: String, override
KEY_BACKGROUND -> keyBackground
ACTION_KEY_POPUP_KEYS_BACKGROUND -> if (themeStyle == STYLE_HOLO) adjustedBackground else accent
STRIP_BACKGROUND -> if (!hasKeyBorders && themeStyle == STYLE_MATERIAL) adjustedBackground else background
CLIPBOARD_SUGGESTION_BACKGROUND -> doubleAdjustedBackground
NAVIGATION_BAR -> navBar
MORE_SUGGESTIONS_HINT, SUGGESTED_WORD, SUGGESTION_TYPED_WORD, SUGGESTION_VALID_WORD -> adjustedKeyText
ACTION_KEY_ICON, TOOL_BAR_EXPAND_KEY -> Color.WHITE
Expand Down Expand Up @@ -467,7 +468,7 @@ class DefaultColors (
CLIPBOARD_PIN, SHIFT_KEY_ICON -> accent
AUTOFILL_BACKGROUND_CHIP -> if (themeStyle == STYLE_MATERIAL && !hasKeyBorders) background else adjustedBackground
GESTURE_PREVIEW, POPUP_KEYS_BACKGROUND, MORE_SUGGESTIONS_BACKGROUND, KEY_PREVIEW -> adjustedBackground
TOOL_BAR_EXPAND_KEY_BACKGROUND -> doubleAdjustedBackground
TOOL_BAR_EXPAND_KEY_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> doubleAdjustedBackground
GESTURE_TRAIL -> gesture
KEY_TEXT, REMOVE_SUGGESTION_ICON, FUNCTIONAL_KEY_TEXT, KEY_ICON -> keyText
KEY_HINT_TEXT -> keyHintText
Expand Down Expand Up @@ -519,7 +520,7 @@ class DefaultColors (
view.setBackgroundColor(Color.WHITE) // set white to make the color filters work
when (color) {
KEY_PREVIEW, POPUP_KEYS_BACKGROUND -> view.background.colorFilter = adjustedBackgroundFilter
FUNCTIONAL_KEY_BACKGROUND, KEY_BACKGROUND, MORE_SUGGESTIONS_WORD_BACKGROUND, SPACE_BAR_BACKGROUND, STRIP_BACKGROUND -> setColor(view.background, color)
FUNCTIONAL_KEY_BACKGROUND, KEY_BACKGROUND, MORE_SUGGESTIONS_WORD_BACKGROUND, SPACE_BAR_BACKGROUND, STRIP_BACKGROUND, CLIPBOARD_SUGGESTION_BACKGROUND -> setColor(view.background, color)
ONE_HANDED_MODE_BUTTON -> setColor(view.background, if (keyboardBackground == null) MAIN_BACKGROUND else STRIP_BACKGROUND)
MORE_SUGGESTIONS_BACKGROUND -> view.background.colorFilter = backgroundFilter
MAIN_BACKGROUND -> {
Expand Down Expand Up @@ -658,6 +659,7 @@ enum class ColorType {
ONE_HANDED_MODE_BUTTON,
REMOVE_SUGGESTION_ICON,
STRIP_BACKGROUND,
CLIPBOARD_SUGGESTION_BACKGROUND,
SUGGESTED_WORD,
SUGGESTION_AUTO_CORRECT,
SUGGESTION_TYPED_WORD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ fun String.splitOnFirstSpacesOnly(): List<String> {
return out
}

fun CharSequence.isValidNumber(): Boolean {
return this.toString().toDoubleOrNull() != null
}

fun String.decapitalize(locale: Locale): String {
if (isEmpty() || !this[0].isUpperCase())
return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1648,7 +1648,11 @@ public void performUpdateSuggestionStripSync(final SettingsValues settingsValues
final SuggestedWords suggestedWords = holder.get(null,
Constants.GET_SUGGESTED_WORDS_TIMEOUT);
if (suggestedWords != null) {
mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords);
// Prefer clipboard suggestions (if available and setting is enabled) over beginning of sentence predictions.
if (!(suggestedWords.mInputStyle == SuggestedWords.INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION
&& mLatinIME.tryShowClipboardSuggestion())) {
mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords);
}
}
if (DebugFlags.DEBUG_ENABLED) {
long runTimeMillis = System.currentTimeMillis() - startTimeMillis;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_AUTOSPACE_AFTER_PUNCTUATION = "autospace_after_punctuation";
public static final String PREF_ALWAYS_INCOGNITO_MODE = "always_incognito_mode";
public static final String PREF_BIGRAM_PREDICTIONS = "next_word_prediction";
public static final String PREF_SUGGEST_CLIPBOARD_CONTENT = "suggest_clipboard_content";
public static final String PREF_GESTURE_INPUT = "gesture_input";
public static final String PREF_VIBRATION_DURATION_SETTINGS = "vibration_duration_settings";
public static final String PREF_KEYPRESS_SOUND_VOLUME = "keypress_sound_volume";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public class SettingsValues {
public final int mScoreLimitForAutocorrect;
private final boolean mSuggestionsEnabledPerUserSettings;
private final boolean mOverrideShowingSuggestions;
public final boolean mSuggestClipboardContent;
public final SettingsValuesForSuggestion mSettingsValuesForSuggestion;
public final boolean mIncognitoModeEnabled;
public final boolean mLongPressSymbolsForNumpad;
Expand Down Expand Up @@ -179,6 +180,7 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina
mScoreLimitForAutocorrect = (mAutoCorrectionThreshold < 0) ? 600000 // very aggressive
: (mAutoCorrectionThreshold < 0.07 ? 800000 : 950000); // aggressive or modest
mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res);
mSuggestClipboardContent = readSuggestClipboardContent(prefs, res);
mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout);
mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration());
final float displayWidthDp = TypedValueCompat.pxToDp(res.getDisplayMetrics().widthPixels, res.getDisplayMetrics());
Expand Down Expand Up @@ -327,6 +329,12 @@ private static boolean readBigramPredictionEnabled(final SharedPreferences prefs
R.bool.config_default_next_word_prediction));
}

private static boolean readSuggestClipboardContent (SharedPreferences prefs,
final Resources res) {
return prefs.getBoolean(Settings.PREF_SUGGEST_CLIPBOARD_CONTENT, res.getBoolean(
R.bool.config_default_suggest_clipboard_content));
}

private static float readAutoCorrectionThreshold(final Resources res,
final SharedPreferences prefs) {
final String currentAutoCorrectionSetting = Settings.readAutoCorrectConfidence(prefs, res);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
Expand Down Expand Up @@ -59,7 +58,6 @@
import helium314.keyboard.latin.settings.SettingsValues;
import helium314.keyboard.latin.suggestions.PopupSuggestionsView.MoreSuggestionsListener;
import helium314.keyboard.latin.utils.DeviceProtectedUtils;
import helium314.keyboard.latin.utils.DialogUtilsKt;
import helium314.keyboard.latin.utils.Log;
import helium314.keyboard.latin.utils.ToolbarKey;
import helium314.keyboard.latin.utils.ToolbarUtilsKt;
Expand All @@ -69,7 +67,6 @@

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;

public final class SuggestionStripView extends RelativeLayout implements OnClickListener,
OnLongClickListener {
Expand Down Expand Up @@ -110,7 +107,7 @@ public interface Listener {

private final SuggestionStripLayoutHelper mLayoutHelper;
private final StripVisibilityGroup mStripVisibilityGroup;
private boolean isInlineAutofillSuggestionsVisible = false; // Required to disable the more suggestions if inline autofill suggestions are visible
private boolean isExternalSuggestionVisible = false; // Required to disable the more suggestions if other suggestions are visible

private static class StripVisibilityGroup {
private final View mSuggestionStripView;
Expand Down Expand Up @@ -258,7 +255,7 @@ private void updateKeys() {
: km.isKeyguardLocked();
mToolbarExpandKey.setOnClickListener(hideToolbarKeys ? null : this);
mPinnedKeys.setVisibility(hideToolbarKeys ? GONE : mSuggestionsStrip.getVisibility());
isInlineAutofillSuggestionsVisible = false;
isExternalSuggestionVisible = false;
}

public void setRtl(final boolean isRtlLanguage) {
Expand All @@ -281,9 +278,9 @@ public void setSuggestions(final SuggestedWords suggestedWords, final boolean is
getContext(), mSuggestedWords, mSuggestionsStrip, this);
}

public void setInlineSuggestionsView(final View view) {
public void setExternalSuggestionView(final View view) {
clear();
isInlineAutofillSuggestionsVisible = true;
isExternalSuggestionVisible = true;
mSuggestionsStrip.addView(view);
if (Settings.getInstance().getCurrent().mAutoHideToolbar)
setToolbarVisibility(false);
Expand Down Expand Up @@ -548,7 +545,7 @@ public boolean onScroll(@Nullable MotionEvent down, @NonNull MotionEvent me, flo
public boolean onInterceptTouchEvent(final MotionEvent me) {

// Disable More Suggestions if inline autofill suggestions is visible
if(isInlineAutofillSuggestionsVisible) {
if(isExternalSuggestionVisible) {
return false;
}

Expand Down
Loading

0 comments on commit bdab98c

Please sign in to comment.