• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 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.os.Build
19 import android.os.Environment
20 import android.util.Log
21 import com.android.virtualization.terminal.MainActivity.Companion.TAG
22 import java.io.BufferedInputStream
23 import java.io.FileInputStream
24 import java.io.IOException
25 import java.io.InputStream
26 import java.lang.RuntimeException
27 import java.net.HttpURLConnection
28 import java.net.MalformedURLException
29 import java.net.URL
30 import java.nio.file.Files
31 import java.nio.file.Path
32 import java.nio.file.StandardCopyOption
33 import java.util.function.Function
34 import org.apache.commons.compress.archivers.ArchiveEntry
35 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
36 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
37 
38 /**
39  * ImageArchive models the archive file (images.tar.gz) where VM payload files are in. This class
40  * provides methods for handling the archive file, most importantly installing it.
41  */
42 internal class ImageArchive {
43     // Only one can be non-null
44     private sealed class Source<out A, out B>
45 
46     private data class UrlSource<out Url>(val value: Url) : Source<Url, Nothing>()
47 
48     private data class PathSource<out Path>(val value: Path) : Source<Nothing, Path>()
49 
50     private val source: Source<URL, Path>
51 
52     private constructor(url: URL) {
53         source = UrlSource(url)
54     }
55 
56     private constructor(path: Path) {
57         source = PathSource(path)
58     }
59 
60     /** Tests if ImageArchive exists on the medium. */
61     fun exists(): Boolean {
62         return when (source) {
63             is UrlSource -> true
64             is PathSource -> Files.exists(source.value)
65         }
66     }
67 
68     /** Returns path to the archive. */
69     fun getPath(): String {
70         return when (source) {
71             is UrlSource -> source.value.toString()
72             is PathSource -> source.value.toString()
73         }
74     }
75 
76     /** Returns size of the archive in bytes */
77     @Throws(IOException::class)
78     fun getSize(): Long {
79         check(exists()) { "Cannot get size of non existing archive" }
80         return when (source) {
81             is UrlSource -> {
82                 val conn = source.value.openConnection() as HttpURLConnection
83                 try {
84                     conn.requestMethod = "HEAD"
85                     conn.getInputStream()
86                     return conn.contentLength.toLong()
87                 } finally {
88                     conn.disconnect()
89                 }
90             }
91             is PathSource -> Files.size(source.value)
92         }
93     }
94 
95     @Throws(IOException::class)
96     private fun getInputStream(filter: Function<InputStream, InputStream>?): InputStream? {
97         val bufStream =
98             BufferedInputStream(
99                 when (source) {
100                     is UrlSource -> source.value.openStream()
101                     is PathSource -> FileInputStream(source.value.toFile())
102                 }
103             )
104         return filter?.apply(bufStream) ?: bufStream
105     }
106 
107     /**
108      * Installs this ImageArchive to a directory pointed by path. filter can be supplied to provide
109      * an additional input stream which will be used during the installation.
110      */
111     @Throws(IOException::class)
112     fun installTo(dir: Path, filter: Function<InputStream, InputStream>?) {
113         val source =
114             when (source) {
115                 is PathSource -> source.value.toString()
116                 is UrlSource -> source.value.toString()
117             }
118         Log.d(TAG, "Installing. source: $source, destination: $dir")
119         TarArchiveInputStream(GzipCompressorInputStream(getInputStream(filter))).use { tarStream ->
120             Files.createDirectories(dir)
121             var entry: ArchiveEntry?
122             while ((tarStream.nextEntry.also { entry = it }) != null) {
123                 val to = dir.resolve(entry!!.getName())
124                 if (Files.isDirectory(to)) {
125                     Files.createDirectories(to)
126                     continue
127                 }
128                 Files.copy(tarStream, to, StandardCopyOption.REPLACE_EXISTING)
129             }
130         }
131         commitInstallationAt(dir)
132     }
133 
134     @Throws(IOException::class)
135     private fun commitInstallationAt(dir: Path) {
136         // To save storage, delete the source archive on the disk.
137         if (source is PathSource) {
138             Files.deleteIfExists(source.value)
139         }
140 
141         // Mark the completion
142         val marker = dir.resolve(InstalledImage.MARKER_FILENAME)
143         Files.createFile(marker)
144     }
145 
146     companion object {
147         private const val DIR_IN_SDCARD = "linux"
148         private const val ARCHIVE_NAME = "images.tar.gz"
149         private val BUILD_TAG = Integer.toString(Build.VERSION.SDK_INT_FULL)
150         private val HOST_URL = "https://dl.google.com/android/ferrochrome/$BUILD_TAG"
151 
152         fun getSdcardPathForTesting(): Path {
153             return Environment.getExternalStoragePublicDirectory(DIR_IN_SDCARD).toPath()
154         }
155 
156         /**
157          * Creates ImageArchive which is located in the sdcard. This archive is for testing only.
158          */
159         fun fromSdCard(): ImageArchive {
160             return ImageArchive(getSdcardPathForTesting().resolve(ARCHIVE_NAME))
161         }
162 
163         /**
164          * Creates ImageArchive which is hosted in the Google server. This is the official archive.
165          */
166         fun fromInternet(): ImageArchive {
167             val arch =
168                 if (listOf<String?>(*Build.SUPPORTED_ABIS).contains("x86_64")) "x86_64"
169                 else "aarch64"
170             try {
171                 return ImageArchive(URL("$HOST_URL/$arch/$ARCHIVE_NAME"))
172             } catch (e: MalformedURLException) {
173                 // cannot happen
174                 throw RuntimeException(e)
175             }
176         }
177 
178         /**
179          * Creates ImageArchive from either SdCard or Internet. SdCard is used only when the build
180          * is debuggable and the file actually exists.
181          */
182         fun getDefault(): ImageArchive {
183             val archive = fromSdCard()
184             return if (Build.isDebuggable() && archive.exists()) {
185                 archive
186             } else {
187                 fromInternet()
188             }
189         }
190     }
191 }
192