Unexpected-Keyboard/srcs/juloo.keyboard2/Keyboard2.java
Jules Aguillon 58cb6ca232 Clipboard history pane
Work in progress: It's not yet possible to paste from the pane.

The pane can be switched to and from and displays the strings recently
added to the clipboard.

ClipboardHistoryService listens for change to the system clipboard and
keep the history in memory.
This data is not persisted to the storage.

The maximum size limits the amount of user data stored in memory but
also gives a sense to the user that the history is not persisted and can
be forgotten as soon as the app stops.
2024-06-29 22:53:08 +02:00

473 lines
14 KiB
Java

package juloo.keyboard2;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.inputmethodservice.InputMethodService;
import android.os.Build.VERSION;
import android.os.IBinder;
import android.text.InputType;
import android.util.Log;
import android.util.LogPrinter;
import android.view.*;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import juloo.keyboard2.prefs.LayoutsPreference;
public class Keyboard2 extends InputMethodService
implements SharedPreferences.OnSharedPreferenceChangeListener
{
private Keyboard2View _keyboardView;
private KeyEventHandler _keyeventhandler;
/** If not 'null', the layout to use instead of [_config.current_layout]. */
private KeyboardData _currentSpecialLayout;
/** Layout associated with the currently selected locale. Not 'null'. */
private KeyboardData _localeTextLayout;
private ViewGroup _emojiPane = null;
private ViewGroup _clipboard_pane = null;
public int actionId; // Action performed by the Action key.
private Config _config;
/** Layout currently visible before it has been modified. */
KeyboardData current_layout_unmodified()
{
if (_currentSpecialLayout != null)
return _currentSpecialLayout;
KeyboardData layout = null;
int layout_i = _config.get_current_layout();
if (layout_i >= _config.layouts.size())
layout_i = 0;
if (layout_i < _config.layouts.size())
layout = _config.layouts.get(layout_i);
if (layout == null)
layout = _localeTextLayout;
return layout;
}
/** Layout currently visible. */
KeyboardData current_layout()
{
if (_currentSpecialLayout != null)
return _currentSpecialLayout;
return _config.modify_layout(current_layout_unmodified());
}
void setTextLayout(int l)
{
_config.set_current_layout(l);
_currentSpecialLayout = null;
_keyboardView.setKeyboard(current_layout());
}
void incrTextLayout(int delta)
{
int s = _config.layouts.size();
setTextLayout((_config.get_current_layout() + delta + s) % s);
}
void setSpecialLayout(KeyboardData l)
{
_currentSpecialLayout = l;
_keyboardView.setKeyboard(l);
}
KeyboardData loadLayout(int layout_id)
{
return KeyboardData.load(getResources(), layout_id);
}
/** Load a layout that contains a numpad. */
KeyboardData loadNumpad(int layout_id)
{
return _config.modify_numpad(KeyboardData.load(getResources(), layout_id),
current_layout_unmodified());
}
KeyboardData loadPinentry(int layout_id)
{
return _config.modify_pinentry(KeyboardData.load(getResources(), layout_id),
current_layout_unmodified());
}
@Override
public void onCreate()
{
super.onCreate();
SharedPreferences prefs = DirectBootAwarePreferences.get_shared_preferences(this);
_keyeventhandler = new KeyEventHandler(getMainLooper(), this.new Receiver());
Config.initGlobalConfig(prefs, getResources(), _keyeventhandler);
prefs.registerOnSharedPreferenceChangeListener(this);
_config = Config.globalConfig();
_keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard);
_keyboardView.reset();
Logs.set_debug_logs(getResources().getBoolean(R.bool.debug_logs));
ClipboardHistoryService.on_startup(this);
}
private List<InputMethodSubtype> getEnabledSubtypes(InputMethodManager imm)
{
String pkg = getPackageName();
for (InputMethodInfo imi : imm.getEnabledInputMethodList())
if (imi.getPackageName().equals(pkg))
return imm.getEnabledInputMethodSubtypeList(imi, true);
return Arrays.asList();
}
@TargetApi(12)
private ExtraKeys extra_keys_of_subtype(InputMethodSubtype subtype)
{
String extra_keys = subtype.getExtraValueOf("extra_keys");
String script = subtype.getExtraValueOf("script");
if (extra_keys != null)
return ExtraKeys.parse(script, extra_keys);
return ExtraKeys.EMPTY;
}
@TargetApi(12)
private void refreshAccentsOption(InputMethodManager imm, InputMethodSubtype subtype)
{
List<InputMethodSubtype> enabled_subtypes = getEnabledSubtypes(imm);
List<ExtraKeys> extra_keys = new ArrayList<ExtraKeys>();
// Gather extra keys from all enabled subtypes
extra_keys.add(extra_keys_of_subtype(subtype));
for (InputMethodSubtype s : enabled_subtypes)
extra_keys.add(extra_keys_of_subtype(s));
_config.extra_keys_subtype = ExtraKeys.merge(extra_keys);
}
InputMethodManager get_imm()
{
return (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
}
private void refreshSubtypeImm()
{
InputMethodManager imm = get_imm();
_config.shouldOfferVoiceTyping = true;
KeyboardData default_layout = null;
_config.extra_keys_subtype = null;
if (VERSION.SDK_INT >= 12)
{
InputMethodSubtype subtype = imm.getCurrentInputMethodSubtype();
if (subtype != null)
{
String s = subtype.getExtraValueOf("default_layout");
if (s != null)
default_layout = LayoutsPreference.layout_of_string(getResources(), s);
refreshAccentsOption(imm, subtype);
}
}
if (default_layout == null)
default_layout = loadLayout(R.xml.latn_qwerty_us);
_localeTextLayout = default_layout;
}
private String actionLabel_of_imeAction(int action)
{
int res;
switch (action)
{
case EditorInfo.IME_ACTION_NEXT: res = R.string.key_action_next; break;
case EditorInfo.IME_ACTION_DONE: res = R.string.key_action_done; break;
case EditorInfo.IME_ACTION_GO: res = R.string.key_action_go; break;
case EditorInfo.IME_ACTION_PREVIOUS: res = R.string.key_action_prev; break;
case EditorInfo.IME_ACTION_SEARCH: res = R.string.key_action_search; break;
case EditorInfo.IME_ACTION_SEND: res = R.string.key_action_send; break;
case EditorInfo.IME_ACTION_UNSPECIFIED:
case EditorInfo.IME_ACTION_NONE:
default: return null;
}
return getResources().getString(res);
}
private void refresh_action_label(EditorInfo info)
{
// First try to look at 'info.actionLabel', if it isn't set, look at
// 'imeOptions'.
if (info.actionLabel != null)
{
_config.actionLabel = info.actionLabel.toString();
actionId = info.actionId;
_config.swapEnterActionKey = false;
}
else
{
int action = info.imeOptions & EditorInfo.IME_MASK_ACTION;
_config.actionLabel = actionLabel_of_imeAction(action); // Might be null
actionId = action;
_config.swapEnterActionKey =
(info.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0;
}
}
/** Might re-create the keyboard view. [_keyboardView.setKeyboard()] and
[setInputView()] must be called soon after. */
private void refresh_config()
{
int prev_theme = _config.theme;
_config.refresh(getResources());
refreshSubtypeImm();
// Refreshing the theme config requires re-creating the views
if (prev_theme != _config.theme)
{
_keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard);
_emojiPane = null;
_clipboard_pane = null;
setInputView(_keyboardView);
}
_keyboardView.reset();
}
private KeyboardData refresh_special_layout(EditorInfo info)
{
switch (info.inputType & InputType.TYPE_MASK_CLASS)
{
case InputType.TYPE_CLASS_NUMBER:
case InputType.TYPE_CLASS_PHONE:
case InputType.TYPE_CLASS_DATETIME:
if (_config.pin_entry_enabled)
return loadPinentry(R.xml.pin);
else
return loadNumpad(R.xml.numeric);
default:
break;
}
return null;
}
@Override
public void onStartInputView(EditorInfo info, boolean restarting)
{
refresh_config();
refresh_action_label(info);
_currentSpecialLayout = refresh_special_layout(info);
_keyboardView.setKeyboard(current_layout());
_keyeventhandler.started(info);
setInputView(_keyboardView);
Logs.debug_startup_input_view(info, _config);
}
@Override
public void setInputView(View v)
{
ViewParent parent = v.getParent();
if (parent != null && parent instanceof ViewGroup)
((ViewGroup)parent).removeView(v);
super.setInputView(v);
updateSoftInputWindowLayoutParams();
}
@Override
public void updateFullscreenMode() {
super.updateFullscreenMode();
updateSoftInputWindowLayoutParams();
}
private void updateSoftInputWindowLayoutParams() {
final Window window = getWindow().getWindow();
updateLayoutHeightOf(window, ViewGroup.LayoutParams.MATCH_PARENT);
final View inputArea = window.findViewById(android.R.id.inputArea);
updateLayoutHeightOf(
(View) inputArea.getParent(),
isFullscreenMode()
? ViewGroup.LayoutParams.MATCH_PARENT
: ViewGroup.LayoutParams.WRAP_CONTENT);
updateLayoutGravityOf((View) inputArea.getParent(), Gravity.BOTTOM);
}
private static void updateLayoutHeightOf(final Window window, final int layoutHeight) {
final WindowManager.LayoutParams params = window.getAttributes();
if (params != null && params.height != layoutHeight) {
params.height = layoutHeight;
window.setAttributes(params);
}
}
private static void updateLayoutHeightOf(final View view, final int layoutHeight) {
final ViewGroup.LayoutParams params = view.getLayoutParams();
if (params != null && params.height != layoutHeight) {
params.height = layoutHeight;
view.setLayoutParams(params);
}
}
private static void updateLayoutGravityOf(final View view, final int layoutGravity) {
final ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp instanceof LinearLayout.LayoutParams) {
final LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) lp;
if (params.gravity != layoutGravity) {
params.gravity = layoutGravity;
view.setLayoutParams(params);
}
} else if (lp instanceof FrameLayout.LayoutParams) {
final FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) lp;
if (params.gravity != layoutGravity) {
params.gravity = layoutGravity;
view.setLayoutParams(params);
}
}
}
@Override
public void onCurrentInputMethodSubtypeChanged(InputMethodSubtype subtype)
{
refreshSubtypeImm();
_keyboardView.setKeyboard(current_layout());
}
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd)
{
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd);
_keyeventhandler.selection_updated(oldSelStart, newSelStart);
}
@Override
public void onFinishInputView(boolean finishingInput)
{
super.onFinishInputView(finishingInput);
_keyboardView.reset();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences _prefs, String _key)
{
refresh_config();
_keyboardView.setKeyboard(current_layout());
}
@Override
public boolean onEvaluateFullscreenMode()
{
/* Entirely disable fullscreen mode. */
return false;
}
/** Not static */
public class Receiver implements KeyEventHandler.IReceiver
{
public void handle_event_key(KeyValue.Event ev)
{
switch (ev)
{
case CONFIG:
Intent intent = new Intent(Keyboard2.this, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
break;
case SWITCH_TEXT:
_currentSpecialLayout = null;
_keyboardView.setKeyboard(current_layout());
break;
case SWITCH_NUMERIC:
setSpecialLayout(loadNumpad(R.xml.numeric));
break;
case SWITCH_EMOJI:
if (_emojiPane == null)
_emojiPane = (ViewGroup)inflate_view(R.layout.emoji_pane);
setInputView(_emojiPane);
break;
case SWITCH_CLIPBOARD:
if (_clipboard_pane == null)
_clipboard_pane = (ViewGroup)inflate_view(R.layout.clipboard_pane);
setInputView(_clipboard_pane);
break;
case SWITCH_BACK_EMOJI:
case SWITCH_BACK_CLIPBOARD:
setInputView(_keyboardView);
break;
case CHANGE_METHOD_PICKER:
get_imm().showInputMethodPicker();
break;
case CHANGE_METHOD_AUTO:
if (VERSION.SDK_INT < 28)
get_imm().switchToLastInputMethod(getConnectionToken());
else
switchToNextInputMethod(false);
break;
case ACTION:
InputConnection conn = getCurrentInputConnection();
if (conn != null)
conn.performEditorAction(actionId);
break;
case SWITCH_FORWARD:
incrTextLayout(1);
break;
case SWITCH_BACKWARD:
incrTextLayout(-1);
break;
case SWITCH_GREEKMATH:
setSpecialLayout(loadNumpad(R.xml.greekmath));
break;
case CAPS_LOCK:
set_shift_state(true, true);
break;
case SWITCH_VOICE_TYPING:
if (!VoiceImeSwitcher.switch_to_voice_ime(Keyboard2.this, get_imm(),
Config.globalPrefs()))
_config.shouldOfferVoiceTyping = false;
break;
case SWITCH_VOICE_TYPING_CHOOSER:
VoiceImeSwitcher.choose_voice_ime(Keyboard2.this, get_imm(),
Config.globalPrefs());
break;
}
}
public void set_shift_state(boolean state, boolean lock)
{
_keyboardView.set_shift_state(state, lock);
}
public void set_compose_pending(boolean pending)
{
_keyboardView.set_compose_pending(pending);
}
public InputConnection getCurrentInputConnection()
{
return Keyboard2.this.getCurrentInputConnection();
}
}
private IBinder getConnectionToken()
{
return getWindow().getWindow().getAttributes().token;
}
private View inflate_view(int layout)
{
return View.inflate(new ContextThemeWrapper(this, _config.theme), layout, null);
}
}