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.app.Notification 19 import android.app.PendingIntent 20 import android.app.Service 21 import android.content.Intent 22 import android.content.pm.ServiceInfo 23 import android.net.ConnectivityManager 24 import android.net.Network 25 import android.net.NetworkCapabilities 26 import android.os.Build 27 import android.os.IBinder 28 import android.util.Log 29 import com.android.internal.annotations.GuardedBy 30 import com.android.virtualization.terminal.ImageArchive.Companion.fromInternet 31 import com.android.virtualization.terminal.ImageArchive.Companion.fromSdCard 32 import com.android.virtualization.terminal.InstalledImage.Companion.getDefault 33 import com.android.virtualization.terminal.InstallerService.InstallerServiceImpl 34 import com.android.virtualization.terminal.InstallerService.WifiCheckInputStream.NoWifiException 35 import com.android.virtualization.terminal.MainActivity.Companion.TAG 36 import java.io.IOException 37 import java.io.InputStream 38 import java.lang.Exception 39 import java.lang.RuntimeException 40 import java.lang.ref.WeakReference 41 import java.net.SocketException 42 import java.net.UnknownHostException 43 import java.util.concurrent.ExecutorService 44 import java.util.concurrent.Executors 45 import kotlin.math.min 46 47 class InstallerService : Service() { 48 private val lock = Any() 49 50 private lateinit var notification: Notification 51 52 @GuardedBy("lock") private var isInstalling = false 53 54 @GuardedBy("lock") private var hasWifi = false 55 56 @GuardedBy("lock") private var listener: IInstallProgressListener? = null 57 58 private lateinit var executorService: ExecutorService 59 private lateinit var connectivityManager: ConnectivityManager 60 private lateinit var networkCallback: MyNetworkCallback 61 onCreatenull62 override fun onCreate() { 63 super.onCreate() 64 65 val intent = Intent(this, MainActivity::class.java) 66 val pendingIntent = 67 PendingIntent.getActivity( 68 this, 69 /* requestCode= */ 0, 70 intent, 71 PendingIntent.FLAG_IMMUTABLE, 72 ) 73 notification = 74 Notification.Builder(this, Application.CHANNEL_LONG_RUNNING_ID) 75 .setSilent(true) 76 .setSmallIcon(R.drawable.ic_launcher_foreground) 77 .setContentTitle(getString(R.string.installer_notif_title_text)) 78 .setContentText(getString(R.string.installer_notif_desc_text)) 79 .setOngoing(true) 80 .setContentIntent(pendingIntent) 81 .build() 82 83 executorService = 84 Executors.newSingleThreadExecutor(TerminalThreadFactory(applicationContext)) 85 86 connectivityManager = getSystemService<ConnectivityManager>(ConnectivityManager::class.java) 87 val defaultNetwork = connectivityManager.boundNetworkForProcess 88 if (defaultNetwork != null) { 89 val capability = connectivityManager.getNetworkCapabilities(defaultNetwork) 90 if (capability != null) { 91 hasWifi = capability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) 92 } 93 } 94 networkCallback = MyNetworkCallback() 95 connectivityManager.registerDefaultNetworkCallback(networkCallback) 96 } 97 onBindnull98 override fun onBind(intent: Intent?): IBinder? { 99 return InstallerServiceImpl(this) 100 } 101 onStartCommandnull102 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 103 super.onStartCommand(intent, flags, startId) 104 105 Log.d(TAG, "Starting service ...") 106 107 return START_STICKY 108 } 109 onDestroynull110 override fun onDestroy() { 111 super.onDestroy() 112 113 Log.d(TAG, "Service is destroyed") 114 executorService.shutdown() 115 connectivityManager.unregisterNetworkCallback(networkCallback) 116 } 117 requestInstallnull118 private fun requestInstall(isWifiOnly: Boolean) { 119 synchronized(lock) { 120 if (isInstalling) { 121 Log.i(TAG, "already installing..") 122 return 123 } else { 124 Log.i(TAG, "installing..") 125 isInstalling = true 126 } 127 } 128 129 // Make service to be long running, even after unbind() when InstallerActivity is destroyed 130 // The service will still be destroyed if task is remove. 131 startService(Intent(this, InstallerService::class.java)) 132 startForeground( 133 NOTIFICATION_ID, 134 notification, 135 ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE, 136 ) 137 138 executorService.execute( 139 Runnable { 140 val success = downloadFromSdcard() || downloadFromUrl(isWifiOnly) 141 stopForeground(STOP_FOREGROUND_REMOVE) 142 143 synchronized(lock) { isInstalling = false } 144 if (success) { 145 notifyCompleted() 146 } 147 } 148 ) 149 } 150 downloadFromSdcardnull151 private fun downloadFromSdcard(): Boolean { 152 val archive = fromSdCard() 153 val archive_path = archive.getPath() 154 155 // Installing from sdcard is preferred, but only supported only in debuggable build. 156 if (!Build.isDebuggable()) { 157 Log.i(TAG, "Non-debuggable build doesn't support installation from $archive_path") 158 return false 159 } 160 if (!archive.exists()) { 161 return false 162 } 163 164 Log.i(TAG, "trying to install $archive_path") 165 166 val dest = getDefault(this).installDir 167 try { 168 archive.installTo(dest, null) 169 Log.i(TAG, "image is installed from $archive_path") 170 return true 171 } catch (e: IOException) { 172 Log.i(TAG, "Failed to install $archive_path", e) 173 } 174 return false 175 } 176 checkForWifiOnlynull177 private fun checkForWifiOnly(isWifiOnly: Boolean): Boolean { 178 if (!isWifiOnly) { 179 return true 180 } 181 synchronized(lock) { 182 return hasWifi 183 } 184 } 185 186 // TODO(b/374015561): Support pause/resume download downloadFromUrlnull187 private fun downloadFromUrl(isWifiOnly: Boolean): Boolean { 188 if (!checkForWifiOnly(isWifiOnly)) { 189 Log.e(TAG, "Install isn't started because Wifi isn't available") 190 notifyError(getString(R.string.installer_error_no_wifi)) 191 return false 192 } 193 194 val dest = getDefault(this).installDir 195 try { 196 fromInternet().installTo(dest) { 197 val filter = WifiCheckInputStream(it) 198 filter.setWifiOnly(isWifiOnly) 199 filter 200 } 201 } catch (e: NoWifiException) { 202 Log.e(TAG, "Install failed because of Wi-Fi is gone") 203 notifyError(getString(R.string.installer_error_no_wifi)) 204 return false 205 } catch (e: UnknownHostException) { 206 // Log.e() doesn't print stack trace for UnknownHostException 207 Log.e(TAG, "Install failed: " + e.message, e) 208 notifyError(getString(R.string.installer_error_network)) 209 return false 210 } catch (e: SocketException) { 211 Log.e(TAG, "Install failed: " + e.message, e) 212 notifyError(getString(R.string.installer_error_network)) 213 return false 214 } catch (e: IOException) { 215 Log.e(TAG, "Installation failed", e) 216 notifyError(getString(R.string.installer_error_unknown)) 217 return false 218 } 219 return true 220 } 221 notifyErrornull222 private fun notifyError(displayText: String?) { 223 var listener: IInstallProgressListener 224 synchronized(lock) { listener = this@InstallerService.listener!! } 225 226 try { 227 listener.onError(displayText) 228 } catch (e: Exception) { 229 // ignore. Activity may not exist. 230 } 231 } 232 notifyCompletednull233 private fun notifyCompleted() { 234 var listener: IInstallProgressListener 235 synchronized(lock) { listener = this@InstallerService.listener!! } 236 237 try { 238 listener.onCompleted() 239 } catch (e: Exception) { 240 // ignore. Activity may not exist. 241 } 242 } 243 244 private class InstallerServiceImpl(service: InstallerService?) : IInstallerService.Stub() { 245 // Holds weak reference to avoid Context leak 246 private val mService: WeakReference<InstallerService> = 247 WeakReference<InstallerService>(service) 248 249 @Throws(RuntimeException::class) ensureServiceConnectednull250 fun ensureServiceConnected(): InstallerService { 251 val service: InstallerService? = mService.get() 252 if (service == null) { 253 throw RuntimeException( 254 "Internal error: Installer service is being accessed after destroyed" 255 ) 256 } 257 return service 258 } 259 requestInstallnull260 override fun requestInstall(isWifiOnly: Boolean) { 261 val service = ensureServiceConnected() 262 synchronized(service.lock) { service.requestInstall(isWifiOnly) } 263 } 264 setProgressListenernull265 override fun setProgressListener(listener: IInstallProgressListener) { 266 val service = ensureServiceConnected() 267 synchronized(service.lock) { service.listener = listener } 268 } 269 isInstallingnull270 override fun isInstalling(): Boolean { 271 val service = ensureServiceConnected() 272 synchronized(service.lock) { 273 return service.isInstalling 274 } 275 } 276 isInstallednull277 override fun isInstalled(): Boolean { 278 val service = ensureServiceConnected() 279 synchronized(service.lock) { 280 return !service.isInstalling && getDefault(service).isInstalled() 281 } 282 } 283 } 284 285 private inner class WifiCheckInputStream(private val inputStream: InputStream) : InputStream() { 286 private var isWifiOnly = false 287 setWifiOnlynull288 fun setWifiOnly(isWifiOnly: Boolean) { 289 this@WifiCheckInputStream.isWifiOnly = isWifiOnly 290 } 291 292 @Throws(IOException::class) readnull293 override fun read(buf: ByteArray?, offset: Int, numToRead: Int): Int { 294 var remaining = numToRead 295 var totalRead = 0 296 while (remaining > 0) { 297 if (!checkForWifiOnly(isWifiOnly)) { 298 throw NoWifiException() 299 } 300 val read = 301 this@WifiCheckInputStream.inputStream.read( 302 buf, 303 offset + totalRead, 304 min(READ_BYTES, remaining), 305 ) 306 if (read <= 0) { 307 break 308 } 309 totalRead += read 310 remaining -= read 311 } 312 return totalRead 313 } 314 315 @Throws(IOException::class) readnull316 override fun read(): Int { 317 if (!checkForWifiOnly(isWifiOnly)) { 318 throw NoWifiException() 319 } 320 return this@WifiCheckInputStream.inputStream.read() 321 } 322 323 inner class NoWifiException : SocketException() 324 } 325 326 private inner class MyNetworkCallback : ConnectivityManager.NetworkCallback() { onCapabilitiesChangednull327 override fun onCapabilitiesChanged(network: Network, capability: NetworkCapabilities) { 328 synchronized(lock) { 329 hasWifi = capability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) 330 } 331 } 332 } 333 334 companion object { 335 private const val NOTIFICATION_ID = 1313 // any unique number among notifications 336 private const val READ_BYTES = 1024 337 } 338 } 339