• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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