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] -->