Merge changes Ifadaa709,I480d8411,Ieb8a961e into main
* changes:
Terminal: Resize sparse disk when storage balloon is turned off
TerminalApp: Hide disk resizing feature when storage ballooning is on
Terminal: Convert main_split_config.xml into Kotlin
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
index 086ff3d..59be4ae 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
@@ -83,7 +83,7 @@
}
@Throws(IOException::class)
- fun getSize(): Long {
+ fun getApparentSize(): Long {
return Files.size(rootPartition)
}
@@ -118,7 +118,7 @@
@Throws(IOException::class)
fun resize(desiredSize: Long): Long {
val roundedUpDesiredSize = roundUp(desiredSize)
- val curSize = getSize()
+ val curSize = getApparentSize()
runE2fsck(rootPartition)
@@ -130,7 +130,21 @@
allocateSpace(rootPartition, roundedUpDesiredSize)
}
resizeFilesystem(rootPartition, roundedUpDesiredSize)
- return getSize()
+ return getApparentSize()
+ }
+
+ @Throws(IOException::class)
+ fun shrinkToMinimumSize(): Long {
+ // Fix filesystem before resizing.
+ runE2fsck(rootPartition)
+
+ val p: String = rootPartition.toAbsolutePath().toString()
+ runCommand("/system/bin/resize2fs", "-M", p)
+ Log.d(TAG, "resize2fs -M completed: $rootPartition")
+
+ // resize2fs may result in an inconsistent filesystem state. Fix with e2fsck.
+ runE2fsck(rootPartition)
+ return getApparentSize()
}
@Throws(IOException::class)
@@ -213,7 +227,7 @@
}
}
- private fun roundUp(bytes: Long): Long {
+ internal fun roundUp(bytes: Long): Long {
// Round up every diskSizeStep MB
return ceil((bytes.toDouble()) / RESIZE_STEP_BYTES).toLong() * RESIZE_STEP_BYTES
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index d85242b..52afef4 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -50,6 +50,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.microdroid.test.common.DeviceProperties
import com.android.system.virtualmachine.flags.Flags
+import com.android.virtualization.terminal.ErrorActivity.Companion.start
import com.android.virtualization.terminal.VmLauncherService.VmLauncherServiceCallback
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
@@ -368,7 +369,7 @@
)
.build()
- val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getSize())
+ val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getApparentSize())
val intent =
VmLauncherService.getIntentForStart(
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
index a4a0a84..1183b46 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsActivity.kt
@@ -19,6 +19,7 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.android.system.virtualmachine.flags.Flags
import com.google.android.material.appbar.MaterialToolbar
class SettingsActivity : AppCompatActivity() {
@@ -29,27 +30,34 @@
val toolbar: MaterialToolbar = findViewById(R.id.settings_toolbar)
setSupportActionBar(toolbar)
- val settingsItems =
- arrayOf(
+ var settingsItems = mutableListOf<SettingsItem>()
+ if (!Flags.terminalStorageBalloon()) {
+ settingsItems.add(
SettingsItem(
resources.getString(R.string.settings_disk_resize_title),
resources.getString(R.string.settings_disk_resize_sub_title),
R.drawable.baseline_storage_24,
SettingsItemEnum.DiskResize,
- ),
- SettingsItem(
- resources.getString(R.string.settings_port_forwarding_title),
- resources.getString(R.string.settings_port_forwarding_sub_title),
- R.drawable.baseline_call_missed_outgoing_24,
- SettingsItemEnum.PortForwarding,
- ),
- SettingsItem(
- resources.getString(R.string.settings_recovery_title),
- resources.getString(R.string.settings_recovery_sub_title),
- R.drawable.baseline_settings_backup_restore_24,
- SettingsItemEnum.Recovery,
- ),
+ )
)
+ }
+ settingsItems.add(
+ SettingsItem(
+ resources.getString(R.string.settings_port_forwarding_title),
+ resources.getString(R.string.settings_port_forwarding_sub_title),
+ R.drawable.baseline_call_missed_outgoing_24,
+ SettingsItemEnum.PortForwarding,
+ )
+ )
+ settingsItems.add(
+ SettingsItem(
+ resources.getString(R.string.settings_recovery_title),
+ resources.getString(R.string.settings_recovery_sub_title),
+ R.drawable.baseline_settings_backup_restore_24,
+ SettingsItemEnum.Recovery,
+ )
+ )
+
val settingsListItemAdapter = SettingsItemAdapter(settingsItems)
val recyclerView: RecyclerView = findViewById(R.id.settings_list_recycler_view)
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
index af1ae95..144db40 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsDiskResizeActivity.kt
@@ -71,7 +71,7 @@
diskSizeStepMb = 1L shl resources.getInteger(R.integer.disk_size_round_up_step_size_in_mb)
val image = InstalledImage.getDefault(this)
- diskSizeMb = bytesToMb(image.getSize())
+ diskSizeMb = bytesToMb(image.getApparentSize())
val minDiskSizeMb = bytesToMb(image.getSmallestSizePossible()).coerceAtMost(diskSizeMb)
val usableSpaceMb =
bytesToMb(Environment.getDataDirectory().getUsableSpace()) and
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItemAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItemAdapter.kt
index 132d749..aa0c3f5 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItemAdapter.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsItemAdapter.kt
@@ -24,7 +24,7 @@
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
-class SettingsItemAdapter(private val dataSet: Array<SettingsItem>) :
+class SettingsItemAdapter(private val dataSet: List<SettingsItem>) :
RecyclerView.Adapter<SettingsItemAdapter.ViewHolder>() {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SplitInitializer.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SplitInitializer.kt
index 7562779..2653ba9 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SplitInitializer.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SplitInitializer.kt
@@ -15,16 +15,96 @@
*/
package com.android.virtualization.terminal
+import android.content.ComponentName
import android.content.Context
+import android.content.Intent
import androidx.startup.Initializer
+import androidx.window.embedding.ActivityFilter
+import androidx.window.embedding.EmbeddingAspectRatio
import androidx.window.embedding.RuleController
+import androidx.window.embedding.SplitAttributes
+import androidx.window.embedding.SplitPairFilter
+import androidx.window.embedding.SplitPairRule
+import androidx.window.embedding.SplitPlaceholderRule
+import androidx.window.embedding.SplitRule
+import com.android.system.virtualmachine.flags.Flags
class SplitInitializer : Initializer<RuleController> {
override fun create(context: Context): RuleController {
- return RuleController.getInstance(context).apply {
- setRules(RuleController.parseRules(context, R.xml.main_split_config))
+ val filters =
+ mutableSetOf(
+ SplitPairFilter(
+ ComponentName(context, SettingsActivity::class.java),
+ ComponentName(context, SettingsPortForwardingActivity::class.java),
+ null,
+ )
+ )
+
+ if (Flags.terminalStorageBalloon()) {
+ filters.add(
+ SplitPairFilter(
+ ComponentName(context, SettingsActivity::class.java),
+ ComponentName(context, SettingsDiskResizeActivity::class.java),
+ null,
+ )
+ )
}
+
+ filters.add(
+ SplitPairFilter(
+ ComponentName(context, SettingsActivity::class.java),
+ ComponentName(context, SettingsRecoveryActivity::class.java),
+ null,
+ )
+ )
+ val splitPairRules =
+ SplitPairRule.Builder(filters)
+ .setClearTop(true)
+ .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.ADJACENT)
+ .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
+ .setDefaultSplitAttributes(
+ SplitAttributes.Builder()
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .setSplitType(
+ SplitAttributes.SplitType.ratio(
+ context.resources.getFloat(R.dimen.activity_split_ratio)
+ )
+ )
+ .build()
+ )
+ .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ALWAYS_ALLOW)
+ .setMinWidthDp(context.resources.getInteger(R.integer.split_min_width))
+ .build()
+
+ val placeholderRule =
+ SplitPlaceholderRule.Builder(
+ setOf(
+ ActivityFilter(ComponentName(context, SettingsActivity::class.java), null)
+ ),
+ Intent(context, SettingsDiskResizeActivity::class.java),
+ )
+ .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ADJACENT)
+ .setDefaultSplitAttributes(
+ SplitAttributes.Builder()
+ .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
+ .setSplitType(
+ SplitAttributes.SplitType.ratio(
+ context.resources.getFloat(R.dimen.activity_split_ratio)
+ )
+ )
+ .build()
+ )
+ .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ALWAYS_ALLOW)
+ .setMinWidthDp(context.resources.getInteger(R.integer.split_min_width))
+ .setSticky(false)
+ .build()
+
+ val ruleController = RuleController.getInstance(context)
+ ruleController.addRule(splitPairRules)
+ ruleController.addRule(placeholderRule)
+
+ return ruleController
}
override fun dependencies(): List<Class<out Initializer<*>>> {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index f426ce6..d43a8d1 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -41,6 +41,7 @@
import android.widget.Toast
import androidx.annotation.WorkerThread
import com.android.system.virtualmachine.flags.Flags
+import com.android.virtualization.terminal.InstalledImage.Companion.roundUp
import com.android.virtualization.terminal.MainActivity.Companion.PREFIX
import com.android.virtualization.terminal.MainActivity.Companion.TAG
import io.grpc.Grpc
@@ -120,7 +121,7 @@
// Note: this doesn't always do the resizing. If the current image size is the same
// as the requested size which is rounded up to the page alignment, resizing is not
// done.
- val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getSize())
+ val diskSize = intent.getLongExtra(EXTRA_DISK_SIZE, image.getApparentSize())
mainWorkerThread.submit({
doStart(notification, displayInfo, diskSize, resultReceiver)
@@ -140,16 +141,18 @@
return START_NOT_STICKY
}
- private fun truncateDiskIfNecessary(image: InstalledImage) {
- val curSize = image.getSize()
- val physicalSize = image.getPhysicalSize()
-
- // Change the rootfs disk's apparent size to GUEST_SPARSE_DISK_SIZE_PERCENTAGE of the total
- // disk size.
- // Note that the physical size is not changed.
+ private fun calculateSparseDiskSize(): Long {
+ // With storage ballooning enabled, we create a sparse file with 95% of the total size.
val statFs = StatFs(filesDir.absolutePath)
val hostSize = statFs.totalBytes
- val expectedSize = hostSize * GUEST_SPARSE_DISK_SIZE_PERCENTAGE / 100
+ return roundUp(hostSize * GUEST_SPARSE_DISK_SIZE_PERCENTAGE / 100)
+ }
+
+ private fun truncateDiskIfNecessary(image: InstalledImage) {
+ val curSize = image.getApparentSize()
+ val physicalSize = image.getPhysicalSize()
+
+ val expectedSize = calculateSparseDiskSize()
Log.d(
TAG,
"rootfs apparent size=$curSize, physical size=$physicalSize, expectedSize=$expectedSize",
@@ -164,6 +167,38 @@
}
}
+ // Convert the rootfs disk to a non-sparse file.
+ private fun convertToNonSparseDiskIfNecessary(image: InstalledImage) {
+ try {
+ val curApparentSize = image.getApparentSize()
+ val curPhysicalSize = image.getPhysicalSize()
+ Log.d(TAG, "Current disk size: apparent=$curApparentSize, physical=$curPhysicalSize")
+
+ // If storage ballooning was enabled via Flags.terminalStorageBalloon() before but it's
+ // now disabled, the disk is still a sparse file whose apparent size is too large.
+ // We need to shrink it to the minimum size.
+ //
+ // The disk file is considered sparse if its apparent disk size matches the expected
+ // sparse disk size.
+ // In addition, we consider it sparse if the physical size is clearly smaller than its
+ // apparent size. This additional condition is a fallback for cases
+ // where the logic of calculating the expected sparse disk size since the disk is
+ // created.
+ if (
+ curApparentSize == calculateSparseDiskSize() ||
+ curPhysicalSize <
+ curApparentSize * EXPECTED_PHYSICAL_SIZE_PERCENTAGE_FOR_NON_SPARSE / 100
+ ) {
+ Log.d(TAG, "A sparse disk is detected. Shrink it to the minimum size.")
+ val newSize = image.shrinkToMinimumSize()
+ Log.d(TAG, "Shrink the disk image: $curApparentSize -> $newSize")
+ }
+ } catch (e: IOException) {
+ throw RuntimeException("Failed to shrink rootfs disk", e)
+ return
+ }
+ }
+
@WorkerThread
private fun doStart(
notification: Notification,
@@ -180,6 +215,10 @@
// When storage ballooning flag is enabled, convert rootfs disk into a sparse file.
truncateDiskIfNecessary(image)
} else {
+ // Convert rootfs disk into a sparse file if storage ballooning flag had been enabled
+ // and then disabled.
+ convertToNonSparseDiskIfNecessary(image)
+
// Note: this doesn't always do the resizing. If the current image size is the same as
// the requested size which is rounded up to the page alignment, resizing is not done.
image.resize(diskSize)
@@ -479,6 +518,7 @@
private const val KEY_TERMINAL_PORT = "port"
private const val GUEST_SPARSE_DISK_SIZE_PERCENTAGE = 95
+ private const val EXPECTED_PHYSICAL_SIZE_PERCENTAGE_FOR_NON_SPARSE = 90
private val VM_BOOT_TIMEOUT_SECONDS: Int =
{
diff --git a/android/TerminalApp/res/values/config.xml b/android/TerminalApp/res/values/config.xml
index 7f0b5e6..a2c9183 100644
--- a/android/TerminalApp/res/values/config.xml
+++ b/android/TerminalApp/res/values/config.xml
@@ -16,4 +16,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<bool name="terminal_portrait_only">true</bool>
+ <item name="activity_split_ratio" format="float" type="dimen">0.3</item>
</resources>
diff --git a/android/TerminalApp/res/values/dimens.xml b/android/TerminalApp/res/values/dimens.xml
deleted file mode 100644
index e00ef7c..0000000
--- a/android/TerminalApp/res/values/dimens.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2024 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources>
- <dimen name="activity_split_ratio">0.3</dimen>
-</resources>
\ No newline at end of file
diff --git a/android/TerminalApp/res/xml/main_split_config.xml b/android/TerminalApp/res/xml/main_split_config.xml
deleted file mode 100644
index bd0271b..0000000
--- a/android/TerminalApp/res/xml/main_split_config.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright 2024 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<resources xmlns:window="http://schemas.android.com/apk/res-auto">
-
- <!-- Define a split for the named activities. -->
- <SplitPairRule
- window:clearTop="true"
- window:finishPrimaryWithSecondary="adjacent"
- window:finishSecondaryWithPrimary="always"
- window:splitLayoutDirection="locale"
- window:splitMaxAspectRatioInPortrait="alwaysAllow"
- window:splitMinWidthDp="@integer/split_min_width"
- window:splitRatio="@dimen/activity_split_ratio">
- <SplitPairFilter
- window:primaryActivityName="com.android.virtualization.terminal.SettingsActivity"
- window:secondaryActivityName="com.android.virtualization.terminal.SettingsDiskResizeActivity" />
- <SplitPairFilter
- window:primaryActivityName="com.android.virtualization.terminal.SettingsActivity"
- window:secondaryActivityName="com.android.virtualization.terminal.SettingsPortForwardingActivity" />
- <SplitPairFilter
- window:primaryActivityName="com.android.virtualization.terminal.SettingsActivity"
- window:secondaryActivityName="com.android.virtualization.terminal.SettingsRecoveryActivity" />
- </SplitPairRule>
-
- <SplitPlaceholderRule
- window:placeholderActivityName="com.android.virtualization.terminal.SettingsDiskResizeActivity"
- window:finishPrimaryWithPlaceholder="adjacent"
- window:splitLayoutDirection="locale"
- window:splitMaxAspectRatioInPortrait="alwaysAllow"
- window:splitMinWidthDp="@integer/split_min_width"
- window:splitRatio="@dimen/activity_split_ratio"
- window:stickyPlaceholder="false">
- <ActivityFilter
- window:activityName="com.android.virtualization.terminal.SettingsActivity"/>
- </SplitPlaceholderRule>
-</resources>
\ No newline at end of file