• 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.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