• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2024 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.annotation.MainThread
19 import android.content.ComponentName
20 import android.content.Intent
21 import android.content.ServiceConnection
22 import android.os.Build
23 import android.os.Bundle
24 import android.os.ConditionVariable
25 import android.os.FileUtils
26 import android.os.IBinder
27 import android.os.RemoteException
28 import android.text.format.Formatter
29 import android.util.Log
30 import android.view.KeyEvent
31 import android.view.View
32 import android.widget.CheckBox
33 import android.widget.TextView
34 import com.android.internal.annotations.VisibleForTesting
35 import com.android.virtualization.terminal.ImageArchive.Companion.fromSdCard
36 import com.android.virtualization.terminal.ImageArchive.Companion.getDefault
37 import com.android.virtualization.terminal.InstallerActivity.InstallProgressListener
38 import com.android.virtualization.terminal.InstallerActivity.InstallerServiceConnection
39 import com.android.virtualization.terminal.MainActivity.Companion.TAG
40 import com.google.android.material.progressindicator.LinearProgressIndicator
41 import com.google.android.material.snackbar.Snackbar
42 import java.io.IOException
43 import java.lang.Exception
44 import java.lang.ref.WeakReference
45 
46 public class InstallerActivity : BaseActivity() {
47     private lateinit var waitForWifiCheckbox: CheckBox
48     private lateinit var installButton: TextView
49 
50     private var service: IInstallerService? = null
51     private var installerServiceConnection: ServiceConnection? = null
52     private lateinit var installProgressListener: InstallProgressListener
53     private var installRequested = false
54     private val installCompleted = ConditionVariable()
55 
onCreatenull56     public override fun onCreate(savedInstanceState: Bundle?) {
57         super.onCreate(savedInstanceState)
58         setResult(RESULT_CANCELED)
59 
60         installProgressListener = InstallProgressListener(this)
61 
62         setContentView(R.layout.activity_installer)
63         updateSizeEstimation(ESTIMATED_IMG_SIZE_BYTES)
64         measureImageSizeAndUpdateDescription()
65 
66         waitForWifiCheckbox = findViewById<CheckBox>(R.id.installer_wait_for_wifi_checkbox)
67         installButton = findViewById<TextView>(R.id.installer_install_button)
68 
69         installButton.setOnClickListener(View.OnClickListener { requestInstall() })
70 
71         val intent = Intent(this, InstallerService::class.java)
72         installerServiceConnection = InstallerServiceConnection(this)
73         if (!bindService(intent, installerServiceConnection!!, BIND_AUTO_CREATE)) {
74             handleInternalError(Exception("Failed to connect to installer service"))
75         }
76     }
77 
updateSizeEstimationnull78     private fun updateSizeEstimation(est: Long) {
79         val desc =
80             getString(R.string.installer_desc_text_format, Formatter.formatShortFileSize(this, est))
81         runOnUiThread {
82             val view = findViewById<TextView>(R.id.installer_desc)
83             view.text = desc
84         }
85     }
86 
measureImageSizeAndUpdateDescriptionnull87     private fun measureImageSizeAndUpdateDescription() {
88         Thread {
89                 val est: Long =
90                     try {
91                         getDefault().getSize()
92                     } catch (e: IOException) {
93                         Log.w(TAG, "Failed to measure image size.", e)
94                         return@Thread
95                     }
96                 updateSizeEstimation(est)
97             }
98             .start()
99     }
100 
onResumenull101     override fun onResume() {
102         super.onResume()
103 
104         if (Build.isDebuggable() && fromSdCard().exists()) {
105             showSnackBar("Auto installing", Snackbar.LENGTH_LONG)
106             requestInstall()
107         }
108     }
109 
onDestroynull110     public override fun onDestroy() {
111         if (installerServiceConnection != null) {
112             unbindService(installerServiceConnection!!)
113             installerServiceConnection = null
114         }
115 
116         super.onDestroy()
117     }
118 
onKeyUpnull119     override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
120         if (keyCode == KeyEvent.KEYCODE_BUTTON_START) {
121             requestInstall()
122             return true
123         }
124         return super.onKeyUp(keyCode, event)
125     }
126 
127     @VisibleForTesting
waitForInstallCompletednull128     public fun waitForInstallCompleted(timeoutMillis: Long): Boolean {
129         return installCompleted.block(timeoutMillis)
130     }
131 
showSnackBarnull132     private fun showSnackBar(message: String, length: Int) {
133         val snackBar = Snackbar.make(findViewById<View>(android.R.id.content), message, length)
134         snackBar.anchorView = waitForWifiCheckbox
135         snackBar.show()
136     }
137 
handleInternalErrornull138     fun handleInternalError(e: Exception) {
139         if (Build.isDebuggable()) {
140             showSnackBar(
141                 e.message + ". File a bugreport to go/ferrochrome-bug",
142                 Snackbar.LENGTH_INDEFINITE,
143             )
144         }
145         Log.e(TAG, "Internal error", e)
146         finishWithResult(RESULT_CANCELED)
147     }
148 
finishWithResultnull149     private fun finishWithResult(resultCode: Int) {
150         if (resultCode == RESULT_OK) {
151             installCompleted.open()
152         }
153         setResult(resultCode)
154         finish()
155     }
156 
setInstallEnablednull157     private fun setInstallEnabled(enabled: Boolean) {
158         installButton.setEnabled(enabled)
159         waitForWifiCheckbox.setEnabled(enabled)
160         val progressBar = findViewById<LinearProgressIndicator>(R.id.installer_progress)
161         progressBar.visibility = if (enabled) View.INVISIBLE else View.VISIBLE
162 
163         val resId =
164             if (enabled) R.string.installer_install_button_enabled_text
165             else R.string.installer_install_button_disabled_text
166         installButton.text = getString(resId)
167     }
168 
169     @MainThread
requestInstallnull170     private fun requestInstall() {
171         setInstallEnabled(/* enabled= */ false)
172 
173         if (service != null) {
174             try {
175                 service!!.requestInstall(waitForWifiCheckbox.isChecked)
176             } catch (e: RemoteException) {
177                 handleInternalError(e)
178             }
179         } else {
180             Log.d(TAG, "requestInstall() is called, but not yet connected")
181             installRequested = true
182         }
183     }
184 
185     @MainThread
handleInstallerServiceConnectednull186     fun handleInstallerServiceConnected() {
187         try {
188             service!!.setProgressListener(installProgressListener)
189             if (service!!.isInstalled()) {
190                 // Finishing this activity will trigger MainActivity::onResume(),
191                 // and VM will be started from there.
192                 finishWithResult(RESULT_OK)
193                 return
194             }
195 
196             if (installRequested) {
197                 requestInstall()
198             } else if (service!!.isInstalling()) {
199                 setInstallEnabled(false)
200             }
201         } catch (e: RemoteException) {
202             handleInternalError(e)
203         }
204     }
205 
206     @MainThread
handleInstallerServiceDisconnectednull207     fun handleInstallerServiceDisconnected() {
208         handleInternalError(Exception("InstallerService is destroyed while in use"))
209     }
210 
211     @MainThread
handleInstallErrornull212     private fun handleInstallError(displayText: String) {
213         showSnackBar(displayText, Snackbar.LENGTH_LONG)
214         setInstallEnabled(true)
215     }
216 
217     private class InstallProgressListener(activity: InstallerActivity) :
218         IInstallProgressListener.Stub() {
219         private val activity: WeakReference<InstallerActivity> =
220             WeakReference<InstallerActivity>(activity)
221 
onCompletednull222         override fun onCompleted() {
223             val activity = activity.get()
224             if (activity == null) {
225                 // Ignore incoming connection or disconnection after activity is destroyed.
226                 return
227             }
228 
229             // MainActivity will be resume and handle rest of progress.
230             activity.finishWithResult(RESULT_OK)
231         }
232 
onErrornull233         override fun onError(displayText: String) {
234             val context = activity.get()
235             if (context == null) {
236                 // Ignore incoming connection or disconnection after activity is destroyed.
237                 return
238             }
239 
240             context.runOnUiThread {
241                 val activity = activity.get()
242                 if (activity == null) {
243                     // Ignore incoming connection or disconnection after activity is
244                     // destroyed.
245                     return@runOnUiThread
246                 }
247                 activity.handleInstallError(displayText)
248             }
249         }
250     }
251 
252     @MainThread
253     class InstallerServiceConnection internal constructor(activity: InstallerActivity) :
254         ServiceConnection {
255         private val activity: WeakReference<InstallerActivity> =
256             WeakReference<InstallerActivity>(activity)
257 
onServiceConnectednull258         override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
259             val activity = activity.get()
260             if (activity == null || activity.installerServiceConnection == null) {
261                 // Ignore incoming connection or disconnection after activity is destroyed.
262                 return
263             }
264             if (service == null) {
265                 activity.handleInternalError(Exception("service shouldn't be null"))
266             }
267 
268             activity.service = IInstallerService.Stub.asInterface(service)
269             activity.handleInstallerServiceConnected()
270         }
271 
onServiceDisconnectednull272         override fun onServiceDisconnected(name: ComponentName?) {
273             val activity = activity.get()
274             if (activity == null || activity.installerServiceConnection == null) {
275                 // Ignore incoming connection or disconnection after activity is destroyed.
276                 return
277             }
278 
279             activity.unbindService(activity.installerServiceConnection!!)
280             activity.installerServiceConnection = null
281             activity.handleInstallerServiceDisconnected()
282         }
283     }
284 
285     companion object {
286         private val ESTIMATED_IMG_SIZE_BYTES = FileUtils.parseSize("550MB")
287     }
288 }
289