1 /* <lambda>null2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.virtualization.terminal 17 18 import android.content.res.Configuration 19 import android.view.KeyEvent 20 import android.view.LayoutInflater 21 import android.view.View 22 import android.view.ViewGroup 23 import android.view.WindowInsets 24 25 class ModifierKeysController(val activity: MainActivity, val parent: ViewGroup) { 26 private val window = activity.window 27 private val keysSingleLine: View 28 private val keysDoubleLine: View 29 private var activeTerminalView: TerminalView? = null 30 private var keysInSingleLine: Boolean = false 31 32 init { 33 // Prepare the two modifier keys layout, but only attach the double line one since the 34 // keysInSingleLine is set to true by default 35 val layout = LayoutInflater.from(activity) 36 keysSingleLine = layout.inflate(R.layout.modifier_keys_singleline, parent, false) 37 keysDoubleLine = layout.inflate(R.layout.modifier_keys_doubleline, parent, false) 38 39 addClickListeners(keysSingleLine) 40 addClickListeners(keysDoubleLine) 41 42 keysSingleLine.visibility = View.GONE 43 keysDoubleLine.visibility = View.GONE 44 parent.addView(keysDoubleLine) 45 46 // Setup for the update to be called when needed 47 window.decorView.rootView.setOnApplyWindowInsetsListener { _: View?, insets: WindowInsets -> 48 update() 49 insets 50 } 51 } 52 53 fun addTerminalView(terminalView: TerminalView) { 54 terminalView.setOnFocusChangeListener { _: View, onFocus: Boolean -> 55 if (onFocus) { 56 activeTerminalView = terminalView 57 } else { 58 activeTerminalView = null 59 terminalView.disableCtrlKey() 60 } 61 update() 62 } 63 } 64 65 private fun addClickListeners(keys: View) { 66 // Only ctrl key is special, it communicates with xtermjs to modify key event with ctrl key 67 keys 68 .findViewById<View>(R.id.btn_ctrl) 69 .setOnClickListener({ 70 activeTerminalView!!.mapCtrlKey() 71 activeTerminalView!!.enableCtrlKey() 72 }) 73 74 val listener = 75 View.OnClickListener { v: View -> 76 BTN_KEY_CODE_MAP[v.id]?.also { keyCode -> 77 activeTerminalView!!.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode)) 78 activeTerminalView!!.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode)) 79 } 80 } 81 82 for (btn in BTN_KEY_CODE_MAP.keys) { 83 keys.findViewById<View>(btn).setOnClickListener(listener) 84 } 85 } 86 87 fun update() { 88 // Pass if no TerminalView focused. 89 if (activeTerminalView == null) { 90 val keys = if (keysInSingleLine) keysSingleLine else keysDoubleLine 91 keys.visibility = View.GONE 92 } else { 93 // select single line or double line 94 val needSingleLine = needsKeysInSingleLine() 95 if (keysInSingleLine != needSingleLine) { 96 if (needSingleLine) { 97 parent.removeView(keysDoubleLine) 98 parent.addView(keysSingleLine) 99 } else { 100 parent.removeView(keysSingleLine) 101 parent.addView(keysDoubleLine) 102 } 103 keysInSingleLine = needSingleLine 104 } 105 // set visibility 106 val needShow = needToShowKeys() 107 val keys = if (keysInSingleLine) keysSingleLine else keysDoubleLine 108 keys.visibility = if (needShow) View.VISIBLE else View.GONE 109 } 110 } 111 112 // Modifier keys are required only when IME is shown and the HW qwerty keyboard is not present 113 private fun needToShowKeys(): Boolean = 114 activity.window.decorView.rootWindowInsets.isVisible(WindowInsets.Type.ime()) && 115 activeTerminalView!!.hasFocus() && 116 !(activity.resources.configuration.keyboard == Configuration.KEYBOARD_QWERTY) 117 118 // If terminal's height including height of modifier keys is less than 40% of the screen 119 // height, we need to show modifier keys in a single line to save the vertical space 120 private fun needsKeysInSingleLine(): Boolean { 121 val keys = if (keysInSingleLine) keysSingleLine else keysDoubleLine 122 return activeTerminalView!!.height + keys.height < 0.4f * activity.window.decorView.height 123 } 124 125 companion object { 126 private val BTN_KEY_CODE_MAP = 127 mapOf( 128 R.id.btn_tab to KeyEvent.KEYCODE_TAB, // Alt key sends ESC keycode 129 R.id.btn_alt to KeyEvent.KEYCODE_ESCAPE, 130 R.id.btn_esc to KeyEvent.KEYCODE_ESCAPE, 131 R.id.btn_left to KeyEvent.KEYCODE_DPAD_LEFT, 132 R.id.btn_right to KeyEvent.KEYCODE_DPAD_RIGHT, 133 R.id.btn_up to KeyEvent.KEYCODE_DPAD_UP, 134 R.id.btn_down to KeyEvent.KEYCODE_DPAD_DOWN, 135 R.id.btn_home to KeyEvent.KEYCODE_MOVE_HOME, 136 R.id.btn_end to KeyEvent.KEYCODE_MOVE_END, 137 R.id.btn_pgup to KeyEvent.KEYCODE_PAGE_UP, 138 R.id.btn_pgdn to KeyEvent.KEYCODE_PAGE_DOWN, 139 ) 140 } 141 } 142