package juloo.keyboard2; 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 java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import android.widget.FrameLayout; import android.widget.LinearLayout; public class Keyboard2 extends InputMethodService implements SharedPreferences.OnSharedPreferenceChangeListener { static private final String TAG = "Keyboard2"; private Keyboard2View _keyboardView; private KeyEventHandler _keyeventhandler; // If not 'null', the layout to use instead of [_currentTextLayout]. private KeyboardData _currentSpecialLayout; private Current_text_layout _currentTextLayout; // Layout associated with the currently selected locale. private KeyboardData _localeTextLayout; private ViewGroup _emojiPane = null; private Config _config; private boolean _debug_logs = false; /** Layout currently visible. */ KeyboardData current_layout() { if (_currentSpecialLayout != null) return _currentSpecialLayout; KeyboardData layout; if (_currentTextLayout == Current_text_layout.SECONDARY) layout = _config.second_layout; else if (_config.layout == null) layout = _localeTextLayout; else layout = _config.layout; return _config.modify_layout(layout); } void setTextLayout(Current_text_layout layout) { _currentTextLayout = layout; _currentSpecialLayout = null; _keyboardView.setKeyboard(current_layout()); } void setSpecialLayout(KeyboardData l) { _currentSpecialLayout = l; _keyboardView.setKeyboard(l); } KeyboardData loadLayout(int layout_id) { return KeyboardData.load(getResources(), layout_id); } @Override public void onCreate() { super.onCreate(); KeyboardData.init(getResources()); SharedPreferences prefs = DirectBootAwarePreferences.get_shared_preferences(this); prefs.registerOnSharedPreferenceChangeListener(this); _keyeventhandler = new KeyEventHandler(getMainLooper(), this.new Receiver()); Config.initGlobalConfig(prefs, getResources(), _keyeventhandler); _config = Config.globalConfig(); _keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard); _keyboardView.reset(); _debug_logs = getResources().getBoolean(R.bool.debug_logs); } private List getEnabledSubtypes(InputMethodManager imm) { String pkg = getPackageName(); for (InputMethodInfo imi : imm.getEnabledInputMethodList()) if (imi.getPackageName().equals(pkg)) return imm.getEnabledInputMethodSubtypeList(imi, true); return Arrays.asList(); } private void refreshSubtypeLayout(InputMethodSubtype subtype) { String s = subtype.getExtraValueOf("default_layout"); if (s != null) _localeTextLayout = _config.layout_of_string(getResources(), s); else _localeTextLayout = KeyboardData.load(getResources(), R.xml.qwerty); } private void extra_keys_of_subtype(Set dst, InputMethodSubtype subtype) { String extra_keys = subtype.getExtraValueOf("extra_keys"); if (extra_keys == null) return; String[] ks = extra_keys.split("\\|"); for (int i = 0; i < ks.length; i++) dst.add(KeyValue.getKeyByName(ks[i])); } private void refreshAccentsOption(InputMethodManager imm, InputMethodSubtype subtype) { HashSet extra_keys = new HashSet(); List enabled_subtypes = getEnabledSubtypes(imm); switch (_config.accents) { // '3' was "all accents", now unused case 1: case 3: extra_keys_of_subtype(extra_keys, subtype); for (InputMethodSubtype s : enabled_subtypes) extra_keys_of_subtype(extra_keys, s); break; case 2: extra_keys_of_subtype(extra_keys, subtype); break; case 4: break; default: throw new IllegalArgumentException(); } _config.extra_keys_subtype = extra_keys; if (enabled_subtypes.size() > 1) _config.shouldOfferSwitchingToNextInputMethod = true; } private void refreshSubtypeLegacyFallback() { // Fallback for the accents option: Only respect the "None" case switch (_config.accents) { case 1: case 2: case 3: _config.extra_keys_subtype = null; break; case 4: _config.extra_keys_subtype = new HashSet(); break; } } private void refreshSubtypeImm() { InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); if (VERSION.SDK_INT < 28) _config.shouldOfferSwitchingToNextInputMethod = true; else _config.shouldOfferSwitchingToNextInputMethod = shouldOfferSwitchingToNextInputMethod(); if (VERSION.SDK_INT < 12) { // Subtypes won't work well under API level 12 (getExtraValueOf) refreshSubtypeLegacyFallback(); } else { InputMethodSubtype subtype = imm.getCurrentInputMethodSubtype(); if (subtype == null) { // On some rare cases, [subtype] is null. refreshSubtypeLegacyFallback(); } else { refreshSubtypeLayout(subtype); refreshAccentsOption(imm, subtype); } } if (_config.second_layout == null) { _config.shouldOfferSwitchingToSecond = false; _currentTextLayout = Current_text_layout.PRIMARY; } else { _config.shouldOfferSwitchingToSecond = true; } } 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(); _keyeventhandler.actionId = info.actionId; _config.swapEnterActionKey = false; } else { int action = info.imeOptions & EditorInfo.IME_MASK_ACTION; _config.actionLabel = actionLabel_of_imeAction(action); // Might be null _keyeventhandler.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; } _keyboardView.reset(); } private void log_editor_info(EditorInfo info) { LogPrinter p = new LogPrinter(Log.DEBUG, TAG); info.dump(p, ""); if (info.extras != null) Log.d(TAG, "extras: "+info.extras.toString()); Log.d(TAG, "swapEnterActionKey: "+_config.swapEnterActionKey); Log.d(TAG, "actionLabel: "+_config.actionLabel); } private void 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: _currentSpecialLayout = _config.modify_numpad(KeyboardData.load(getResources(), R.xml.pin)); break; default: _currentSpecialLayout = null; break; } } @Override public void onStartInputView(EditorInfo info, boolean restarting) { refresh_config(); refresh_action_label(info); refresh_special_layout(info); _keyboardView.setKeyboard(current_layout()); _keyeventhandler.started(info); setInputView(_keyboardView); if (_debug_logs) log_editor_info(info); } @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(); setInputView(_keyboardView); _keyboardView.setKeyboard(current_layout()); } @Override public boolean onEvaluateFullscreenMode() { /* Entirely disable fullscreen mode. */ return false; } /** Not static */ public class Receiver implements KeyEventHandler.IReceiver { public void switchInputMethod() { InputMethodManager imm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); imm.showInputMethodPicker(); } public void switchToPrevInputMethod() { if (VERSION.SDK_INT < 28) { InputMethodManager imm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE); imm.switchToLastInputMethod(getConnectionToken()); } else Keyboard2.this.switchToPreviousInputMethod(); } public void setPane_emoji() { if (_emojiPane == null) _emojiPane = (ViewGroup)inflate_view(R.layout.emoji_pane); setInputView(_emojiPane); } public void setPane_normal() { setInputView(_keyboardView); } public void set_shift_state(boolean state, boolean lock) { _keyboardView.set_shift_state(state, lock); } public void set_layout(KeyEventHandler.Layout l) { switch (l) { case Current: _currentSpecialLayout = null; _keyboardView.setKeyboard(current_layout()); break; case Primary: setTextLayout(Current_text_layout.PRIMARY); break; case Secondary: if (_config.second_layout != null) setTextLayout(Current_text_layout.SECONDARY); break; case Numeric: setSpecialLayout(_config.modify_numpad(loadLayout(R.xml.numeric))); break; case Greekmath: setSpecialLayout(_config.modify_numpad(loadLayout(R.xml.greekmath))); break; } } public void showKeyboardConfig() { Intent intent = new Intent(Keyboard2.this, SettingsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } 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); } private static enum Current_text_layout { PRIMARY, SECONDARY } }