• 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.Intent
19 import android.graphics.Bitmap
20 import android.net.http.SslError
21 import android.os.Bundle
22 import android.util.Log
23 import android.view.LayoutInflater
24 import android.view.View
25 import android.view.ViewGroup
26 import android.webkit.ClientCertRequest
27 import android.webkit.JavascriptInterface
28 import android.webkit.SslErrorHandler
29 import android.webkit.WebChromeClient
30 import android.webkit.WebResourceError
31 import android.webkit.WebResourceRequest
32 import android.webkit.WebSettings
33 import android.webkit.WebView
34 import android.webkit.WebViewClient
35 import android.widget.TextView
36 import androidx.fragment.app.Fragment
37 import androidx.fragment.app.activityViewModels
38 import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
39 import com.android.virtualization.terminal.CertificateUtils.createOrGetKey
40 import com.android.virtualization.terminal.CertificateUtils.writeCertificateToFile
41 import java.security.PrivateKey
42 import java.security.cert.X509Certificate
43 
44 class TerminalTabFragment() : Fragment() {
45     private lateinit var terminalView: TerminalView
46     private lateinit var bootProgressView: View
47     private lateinit var id: String
48     private var certificates: Array<X509Certificate>? = null
49     private var privateKey: PrivateKey? = null
50     private val terminalViewModel: TerminalViewModel by activityViewModels()
51 
52     override fun onCreateView(
53         inflater: LayoutInflater,
54         container: ViewGroup?,
55         savedInstanceState: Bundle?,
56     ): View {
57         val view = inflater.inflate(R.layout.fragment_terminal_tab, container, false)
58         arguments?.let { id = it.getString("id")!! }
59         return view
60     }
61 
62     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63         super.onViewCreated(view, savedInstanceState)
64         terminalView = view.findViewById(R.id.webview)
65         bootProgressView = view.findViewById(R.id.boot_progress)
66         initializeWebView()
67         readClientCertificate()
68 
69         terminalView.webViewClient = TerminalWebViewClient()
70 
71         if (savedInstanceState != null) {
72             terminalView.restoreState(savedInstanceState)
73         } else {
74             (activity as MainActivity).connectToTerminalService(terminalView)
75         }
76     }
77 
78     override fun onSaveInstanceState(outState: Bundle) {
79         super.onSaveInstanceState(outState)
80         terminalView.saveState(outState)
81     }
82 
83     override fun onResume() {
84         super.onResume()
85         updateFocus()
86     }
87 
88     private fun initializeWebView() {
89         terminalView.settings.databaseEnabled = true
90         terminalView.settings.domStorageEnabled = true
91         terminalView.settings.javaScriptEnabled = true
92         terminalView.settings.cacheMode = WebSettings.LOAD_DEFAULT
93 
94         terminalView.webChromeClient = TerminalWebChromeClient()
95         terminalView.webViewClient = TerminalWebViewClient()
96         terminalView.addJavascriptInterface(TerminalViewInterface(context!!), "TerminalApp")
97 
98         (activity as MainActivity).modifierKeysController.addTerminalView(terminalView)
99         terminalViewModel.terminalViews.add(terminalView)
100     }
101 
102     private inner class TerminalWebChromeClient : WebChromeClient() {
103         override fun onReceivedTitle(view: WebView?, title: String?) {
104             super.onReceivedTitle(view, title)
105             title?.let { originalTitle ->
106                 val ttydSuffix = " | login -f droid (localhost)"
107                 val displayedTitle =
108                     if (originalTitle.endsWith(ttydSuffix)) {
109                         // When the session is created. The format of the title will be
110                         // 'droid@localhost: ~ | login -f droid (localhost)'.
111                         originalTitle.dropLast(ttydSuffix.length)
112                     } else {
113                         originalTitle
114                     }
115 
116                 terminalViewModel.terminalTabs[id]
117                     ?.customView
118                     ?.findViewById<TextView>(R.id.tab_title)
119                     ?.text = displayedTitle
120             }
121         }
122     }
123 
124     inner class TerminalViewInterface(private val mContext: android.content.Context) {
125         @JavascriptInterface
126         fun closeTab() {
127             if (terminalViewModel.terminalTabs.containsKey(id)) {
128                 if (activity != null) {
129                     activity?.runOnUiThread {
130                         val mainActivity = (activity as MainActivity)
131                         mainActivity.closeTab(terminalViewModel.terminalTabs[id]!!)
132                     }
133                 }
134             }
135         }
136     }
137 
138     private inner class TerminalWebViewClient : WebViewClient() {
139         private var loadFailed = false
140         private var requestId: Long = 0
141 
142         override fun shouldOverrideUrlLoading(
143             view: WebView?,
144             request: WebResourceRequest?,
145         ): Boolean {
146             val intent = Intent(Intent.ACTION_VIEW, request?.url)
147             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
148             startActivity(intent)
149             return true
150         }
151 
152         override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
153             loadFailed = false
154         }
155 
156         override fun onReceivedError(
157             view: WebView,
158             request: WebResourceRequest,
159             error: WebResourceError,
160         ) {
161             loadFailed = true
162             when (error.getErrorCode()) {
163                 ERROR_CONNECT,
164                 ERROR_HOST_LOOKUP,
165                 ERROR_FAILED_SSL_HANDSHAKE,
166                 ERROR_TIMEOUT -> {
167                     view.reload()
168                     return
169                 }
170 
171                 else -> {
172                     val url: String? = request.getUrl().toString()
173                     val msg = error.getDescription()
174                     Log.e(MainActivity.TAG, "Failed to load $url: $msg")
175                 }
176             }
177         }
178 
179         override fun onPageFinished(view: WebView, url: String?) {
180             if (loadFailed) {
181                 return
182             }
183 
184             requestId++
185             view.postVisualStateCallback(
186                 requestId,
187                 object : WebView.VisualStateCallback() {
188                     override fun onComplete(completedRequestId: Long) {
189                         if (completedRequestId == requestId) {
190                             bootProgressView.visibility = View.GONE
191                             terminalView.visibility = View.VISIBLE
192                             terminalView.mapTouchToMouseEvent()
193                             terminalView.applyTerminalDisconnectCallback()
194                             updateMainActivity()
195                             updateFocus()
196                         }
197                     }
198                 },
199             )
200         }
201 
202         override fun onReceivedClientCertRequest(view: WebView?, request: ClientCertRequest) {
203             if (privateKey != null && certificates != null) {
204                 request.proceed(privateKey, certificates)
205                 return
206             }
207             super.onReceivedClientCertRequest(view, request)
208         }
209 
210         override fun onReceivedSslError(
211             view: WebView?,
212             handler: SslErrorHandler,
213             error: SslError?,
214         ) {
215             // ttyd uses self-signed certificate
216             handler.proceed()
217         }
218     }
219 
220     private fun updateMainActivity() {
221         val mainActivity = activity as MainActivity ?: return
222         if (terminalGuiSupport()) {
223             mainActivity.displayMenu!!.visibility = View.VISIBLE
224             mainActivity.displayMenu!!.isEnabled = true
225         }
226         mainActivity.tabAddButton!!.isEnabled = true
227         mainActivity.bootCompleted.open()
228     }
229 
230     private fun readClientCertificate() {
231         val pke = createOrGetKey()
232         writeCertificateToFile(activity!!, pke.certificate)
233         privateKey = pke.privateKey
234         certificates = arrayOf<X509Certificate>(pke.certificate as X509Certificate)
235     }
236 
237     private fun updateFocus() {
238         if (terminalViewModel.selectedTabViewId == id) {
239             terminalView.requestFocus()
240         }
241     }
242 
243     companion object {
244         const val TAG: String = "VmTerminalApp"
245     }
246 
247     override fun onDestroy() {
248         terminalView.terminalClose()
249         terminalViewModel.terminalViews.remove(terminalView)
250         super.onDestroy()
251     }
252 }
253