Files
Unexpected-Keyboard/srcs/juloo.keyboard2/Keyboard2.java
T
Jules Aguillon f192dfcb32 Add landscape variant for the number and pin layouts (#1341)
The number pane and the pin entry layouts are split in landscape
orientation. Instead of inserting empty space in the middle of the
layout, the numpad is duplicated. The other keys are not duplicated.
2026-07-01 01:09:36 +02:00

553 lines
17 KiB
Java

package juloo.keyboard2;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.InputMethodService;
import android.os.Build.VERSION;
import android.os.Handler;
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.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.cdict.Cdict;
import juloo.keyboard2.dict.Dictionaries;
import juloo.keyboard2.dict.DictionariesActivity;
import juloo.keyboard2.prefs.LayoutsPreference;
import juloo.keyboard2.suggestions.CandidatesView;
import juloo.keyboard2.suggestions.Suggestions;
public class Keyboard2 extends InputMethodService
implements SharedPreferences.OnSharedPreferenceChangeListener
{
/** The view containing the keyboard and candidates view. */
private ViewGroup _keyboard_container_view;
private Keyboard2View _keyboard_layout_view;
private CandidatesView _candidates_view;
private Suggestions _suggestions;
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;
/** Installed and current locales. */
private Dictionaries _dictionaries;
private ViewGroup _emojiPane = null;
private ViewGroup _clipboard_pane = null;
private Handler _handler;
private Config _config;
private FoldStateTracker _foldStateTracker;
/** 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 LayoutModifier.modify_layout(current_layout_unmodified());
}
void setTextLayout(int l)
{
_config.set_current_layout(l);
_currentSpecialLayout = null;
_keyboard_layout_view.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;
_keyboard_layout_view.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 LayoutModifier.modify_numpad(KeyboardData.load(getResources(), layout_id),
current_layout_unmodified());
}
KeyboardData loadNumericLayout()
{
return loadNumpad(_config.orientation_landscape ?
R.xml.numeric_landscape : R.xml.numeric);
}
KeyboardData loadPinentry(int layout_id)
{
return LayoutModifier.modify_pinentry(KeyboardData.load(getResources(), layout_id),
current_layout_unmodified());
}
@Override
public void onCreate()
{
super.onCreate();
SharedPreferences prefs = DirectBootAwarePreferences.get_shared_preferences(this);
_handler = new Handler(getMainLooper());
_foldStateTracker = new FoldStateTracker(this);
_dictionaries = Dictionaries.instance(this);
Config.initGlobalConfig(prefs, getResources(),
_foldStateTracker.isUnfolded(), _dictionaries);
_config = Config.globalConfig();
Receiver recvr = this.new Receiver();
_suggestions = new Suggestions(recvr, _config);
_keyeventhandler = new KeyEventHandler(recvr, _suggestions);
KeyValue.Stateful._handler = recvr;
_config.handler = _keyeventhandler;
prefs.registerOnSharedPreferenceChangeListener(this);
Logs.set_debug_logs(getResources().getBoolean(R.bool.debug_logs));
refreshSubtypeImm();
create_keyboard_view();
ClipboardHistoryService.on_startup(this, _keyeventhandler);
_foldStateTracker.setChangedCallback(() -> { refresh_config(); });
}
@Override
public void onDestroy() {
super.onDestroy();
_foldStateTracker.close();
}
private void create_keyboard_view()
{
_keyboard_container_view = (ViewGroup)inflate_view(R.layout.keyboard);
_keyboard_layout_view = (Keyboard2View)_keyboard_container_view.findViewById(R.id.keyboard_view);
_candidates_view = (CandidatesView)_keyboard_container_view.findViewById(R.id.candidates_view);
}
InputMethodManager get_imm()
{
return (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
}
private void refreshSubtypeImm()
{
_config.shouldOfferVoiceTyping = true;
KeyboardData default_layout = null;
_config.device_locales = DeviceLocales.load(this);
if (_config.device_locales.default_ != null)
{
String layout_name = _config.device_locales.default_.default_layout;
if (layout_name != null)
default_layout = LayoutsPreference.layout_of_string(getResources(), layout_name);
}
_config.extra_keys_subtype = _config.device_locales.extra_keys();
if (default_layout == null)
default_layout = loadLayout(R.xml.latn_qwerty_us);
_localeTextLayout = default_layout;
}
private void refresh_current_dictionary()
{
_config.current_dictionary = null;
_config.emoji_dictionary = null;
if (_config.device_locales.default_ == null)
return;
String current = _config.device_locales.default_.dictionary;
if (current == null)
return;
Cdict[] dicts = _dictionaries.load(current);
if (dicts == null)
return;
_config.current_dictionary = Dictionaries.find_by_name(dicts, "main");
_config.emoji_dictionary = Dictionaries.find_by_name(dicts, "emoji");
}
private void refresh_candidates_view()
{
boolean should_show =
_config.suggestions_enabled
&& _config.editor_config.should_show_candidates_view;
if (should_show)
_candidates_view.refresh_config(_config);
_candidates_view.setVisibility(should_show ? View.VISIBLE : View.GONE);
}
/** Might re-create the keyboard view. [_keyboard_layout_view.setKeyboard()] and
[setInputView()] must be called soon after. */
private void refresh_config()
{
int prev_theme = _config.theme;
_config.refresh(getResources(), _foldStateTracker.isUnfolded(), _dictionaries);
refresh_current_dictionary();
// Refreshing the theme config requires re-creating the views
if (prev_theme != _config.theme)
{
create_keyboard_view();
_emojiPane = null;
_clipboard_pane = null;
setInputView(_keyboard_container_view);
}
// Set keyboard background opacity
Drawable bg = _keyboard_container_view.getBackground().mutate();
bg.setAlpha(_config.keyboardOpacity);
_keyboard_container_view.setBackground(bg);
_keyboard_layout_view.reset();
refresh_candidates_view();
}
private KeyboardData refresh_special_layout()
{
if (_config.editor_config.numeric_layout)
{
switch (_config.selected_number_layout)
{
case PIN:
return loadPinentry(_config.orientation_landscape ?
R.xml.pin_landscape : R.xml.pin);
case NUMBER:
return loadNumericLayout();
}
}
return null;
}
@Override
public void onStartInputView(EditorInfo info, boolean restarting)
{
_config.editor_config.refresh(info, getResources());
refresh_config();
_currentSpecialLayout = refresh_special_layout();
_keyboard_layout_view.setKeyboard(current_layout());
_keyeventhandler.started(_config);
setInputView(_keyboard_container_view);
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();
v.requestApplyInsets();
}
@Override
public void updateFullscreenMode() {
super.updateFullscreenMode();
updateSoftInputWindowLayoutParams();
}
private void updateSoftInputWindowLayoutParams() {
final Window window = getWindow().getWindow();
// On API >= 35, Keyboard2View behaves as edge-to-edge
// APIs 30 to 34 have visual artifact when edge-to-edge is enabled
if (VERSION.SDK_INT >= 35)
{
WindowManager.LayoutParams wattrs = window.getAttributes();
wattrs.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
// Allow to draw behind system bars
wattrs.setFitInsetsTypes(0);
window.setDecorFitsSystemWindows(false);
}
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();
refresh_current_dictionary();
refresh_candidates_view();
_keyboard_layout_view.setKeyboard(current_layout());
_keyeventhandler.ime_subtype_changed();
}
@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, newSelEnd);
if ((oldSelStart == oldSelEnd) != (newSelStart == newSelEnd))
_keyboard_layout_view.set_selection_state(newSelStart != newSelEnd);
}
@Override
public void onFinishInputView(boolean finishingInput)
{
super.onFinishInputView(finishingInput);
_keyboard_layout_view.reset();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences _prefs, String _key)
{
refresh_config();
_keyboard_layout_view.setKeyboard(current_layout());
}
@Override
public boolean onEvaluateFullscreenMode()
{
/* Entirely disable fullscreen mode. */
return false;
}
@Override
public boolean onEvaluateInputViewShown()
{
// Since Android 16, this method returns [false] for unknown reasons.
if (super.onEvaluateInputViewShown())
return true;
if (getResources().getConfiguration().hardKeyboardHidden
== Configuration.HARDKEYBOARDHIDDEN_NO
&& _config.physical_keyboard_hide)
{
Logs.debug("Physical keyboard is present");
return false;
}
return true;
}
/** Called from [onClick] attributes. */
public void launch_dictionaries_activity(View v)
{
start_activity(DictionariesActivity.class);
}
void start_activity(Class cls)
{
Intent intent = new Intent(this, cls);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
/** Not static */
public class Receiver implements KeyEventHandler.IReceiver,
KeyValue.Stateful.Symbol_provider
{
public void handle_event_key(KeyValue.Event ev)
{
switch (ev)
{
case CONFIG:
start_activity(SettingsActivity.class);
break;
case SWITCH_TEXT:
_currentSpecialLayout = null;
_keyboard_layout_view.setKeyboard(current_layout());
break;
case SWITCH_NUMERIC:
setSpecialLayout(loadNumericLayout());
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(_keyboard_container_view);
break;
case CHANGE_METHOD_PICKER:
get_imm().showInputMethodPicker();
break;
case CHANGE_METHOD_PREV:
if (VERSION.SDK_INT < 28)
get_imm().switchToLastInputMethod(getConnectionToken());
else
switchToPreviousInputMethod();
break;
case CHANGE_METHOD_NEXT:
if (VERSION.SDK_INT < 28)
get_imm().switchToNextInputMethod(getConnectionToken(), false);
else
switchToNextInputMethod(false);
break;
case ACTION:
InputConnection conn = getCurrentInputConnection();
if (conn != null)
conn.performEditorAction(_config.editor_config.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;
case HIDE_SELF:
Keyboard2.this.requestHideSelf(0);
break;
}
}
public void set_shift_state(boolean state, boolean lock)
{
_keyboard_layout_view.set_shift_state(state, lock);
}
public void set_compose_pending(boolean pending)
{
_keyboard_layout_view.set_compose_pending(pending);
}
public void selection_state_changed(boolean selection_is_ongoing)
{
_keyboard_layout_view.set_selection_state(selection_is_ongoing);
}
public InputConnection getCurrentInputConnection()
{
return Keyboard2.this.getCurrentInputConnection();
}
public Handler getHandler()
{
return _handler;
}
public void set_suggestions(Suggestions suggestions)
{
_candidates_view.set_candidates(suggestions);
}
public String provide_stateful_key_symbol(KeyValue.Stateful q)
{
switch (q)
{
case Complete_first: return _suggestions.suggestions[0];
case Complete_second: return _suggestions.suggestions[1];
case Complete_third: return _suggestions.suggestions[2];
case Complete_emoji: return _suggestions.emoji_suggestion;
}
return "";
}
}
private IBinder getConnectionToken()
{
return getWindow().getWindow().getAttributes().token;
}
private View inflate_view(int layout)
{
return View.inflate(new ContextThemeWrapper(this, _config.theme), layout, null);
}
}