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