VmTerminalApp: Request for recovery for old image
Screenshot: http://shortn/_BfwncVTC0Z
Bug: 402607985
Test: Manually with manually pushed 25Q1 image to /sdcard
Change-Id: Ie01bfdfa472592dc7470a60faab22918a1311877
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index aa702a3..48b53dd 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -65,6 +65,8 @@
<activity android:name=".ErrorActivity"
android:label="@string/error_title"
android:process=":error" />
+ <activity android:name=".UpgradeActivity"
+ android:label="@string/upgrade_title" />
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
android:value="true" />
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
index 50aaa33..04d6813 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstalledImage.kt
@@ -67,6 +67,16 @@
}
}
+ fun isOlderThanCurrentVersion(): Boolean {
+ val year =
+ try {
+ buildId.split(" ").last().toInt()
+ } catch (_: Exception) {
+ 0
+ }
+ return year < RELEASE_YEAR
+ }
+
@Throws(IOException::class)
fun uninstallAndBackup(): Path {
Files.delete(marker)
@@ -191,6 +201,7 @@
const val MARKER_FILENAME: String = "completed"
const val RESIZE_STEP_BYTES: Long = 4 shl 20 // 4 MiB
+ const val RELEASE_YEAR: Int = 2025
/** Returns InstalledImage for a given app context */
fun getDefault(context: Context): InstalledImage {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
index 52afef4..8d74bad 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.kt
@@ -103,7 +103,13 @@
// if installer is launched, it will be handled in onActivityResult
if (!launchInstaller) {
- if (!Environment.isExternalStorageManager()) {
+ if (image.isOlderThanCurrentVersion()) {
+ val intent = Intent(this, UpgradeActivity::class.java)
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
+ // Explicitly finish to make sure that user can't go back from ErrorActivity.
+ finish()
+ } else if (!Environment.isExternalStorageManager()) {
requestStoragePermissions(this, manageExternalStorageActivityResultLauncher)
} else {
startVm()
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/UpgradeActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/UpgradeActivity.kt
new file mode 100644
index 0000000..357de94
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/UpgradeActivity.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025 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.
+ */
+package com.android.virtualization.terminal
+
+import android.annotation.MainThread
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import com.google.android.material.snackbar.Snackbar
+import java.io.IOException
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class UpgradeActivity : BaseActivity() {
+ private lateinit var executorService: ExecutorService
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ executorService =
+ Executors.newSingleThreadExecutor(TerminalThreadFactory(applicationContext))
+
+ setContentView(R.layout.activity_upgrade)
+
+ val button = findViewById<View>(R.id.upgrade)
+ button.setOnClickListener { _ -> upgrade() }
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ executorService.shutdown()
+ }
+
+ private fun upgrade() {
+ findViewById<View>(R.id.progress).visibility = View.VISIBLE
+
+ executorService.execute {
+ val image = InstalledImage.getDefault(this)
+ try {
+ image.uninstallAndBackup()
+ } catch (e: IOException) {
+ Snackbar.make(
+ findViewById<View>(android.R.id.content),
+ R.string.upgrade_error,
+ Snackbar.LENGTH_SHORT,
+ )
+ .show()
+ Log.e(MainActivity.Companion.TAG, "Failed to upgrade ", e)
+ return@execute
+ }
+
+ runOnUiThread {
+ findViewById<View>(R.id.progress).visibility = View.INVISIBLE
+ restartTerminal()
+ }
+ }
+ }
+
+ @MainThread
+ private fun restartTerminal() {
+ val intent = baseContext.packageManager.getLaunchIntentForPackage(baseContext.packageName)
+ intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ finish()
+ startActivity(intent)
+ }
+}
diff --git a/android/TerminalApp/res/layout/activity_upgrade.xml b/android/TerminalApp/res/layout/activity_upgrade.xml
new file mode 100644
index 0000000..13e8404
--- /dev/null
+++ b/android/TerminalApp/res/layout/activity_upgrade.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2025 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.
+ -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ tools:context=".UpgradeActivity">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/upgrade_title"
+ android:layout_marginVertical="24dp"
+ android:layout_marginHorizontal="24dp"
+ android:layout_alignParentTop="true"
+ android:hyphenationFrequency="full"
+ android:textSize="48sp" />
+
+ <com.google.android.material.progressindicator.LinearProgressIndicator
+ android:id="@+id/progress"
+ android:indeterminate="true"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_below="@id/title"
+ android:visibility="invisible" />
+
+ <TextView
+ android:id="@+id/desc"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/upgrade_desc"
+ android:lineSpacingExtra="5sp"
+ android:layout_marginTop="20dp"
+ android:layout_marginHorizontal="48dp"
+ android:layout_below="@id/progress"
+ android:textSize="20sp" />
+
+ <Button
+ android:id="@+id/upgrade"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:layout_marginBottom="32dp"
+ android:layout_marginHorizontal="40dp"
+ android:backgroundTint="?attr/colorPrimaryDark"
+ android:text="@string/upgrade_button" />
+
+</RelativeLayout>
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 273032e..a0e33e5 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -161,6 +161,15 @@
<!-- Error page that shows detailed error code (error reason) for bugreport. [CHAR LIMIT=none] -->
<string name="error_code">Error code: <xliff:g id="error_code" example="ACCESS_DENIED">%s</xliff:g></string>
+ <!-- Upgrade page's title. Tells users that you'll need to upgrade [CHAR LIMIT=none] -->
+ <string name="upgrade_title">Upgrade to newer terminal</string>
+ <!-- Upgrade page's description. Tell users that can't use as-is, and also explains next step. (/mnt/backup is the path which is supposed not to be translated) [CHAR LIMIT=none] -->
+ <string name="upgrade_desc">Linux terminal you were using is out of date. Please upgrade to proceed.\nData will be backed at <xliff:g id="path" example="/mnt/backup">/mnt/backup</xliff:g></string>
+ <!-- Upgrade page's button to start upgrade. [CHAR LIMIT=16] -->
+ <string name="upgrade_button">Upgrade</string>
+ <!-- Upgrade page's error toast message when upgrade failed. [CHAR LIMIT=none] -->
+ <string name="upgrade_error">Upgrade failed</string>
+
<!-- Notification action button for settings [CHAR LIMIT=20] -->
<string name="service_notification_settings">Settings</string>
<!-- Notification title for foreground service notification [CHAR LIMIT=none] -->