VmTerminalApp: Initial skeleton for downloading from server
Bug: 369740847
Test: Manually
Change-Id: I74c62e77a66eed871729c0f9def044dfb92a479c
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 67ef199..61737fe 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.android.virtualization.terminal">
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
<uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
diff --git a/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl
index 1f51723..94e33d9 100644
--- a/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl
+++ b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl
@@ -19,4 +19,5 @@
// TODO(b/374015561): Provide progress update
oneway interface IInstallProgressListener {
void onCompleted();
+ void onError(in String displayText);
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index 25780a5..428fd91 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -106,15 +106,20 @@
finish();
}
- private void preventInstall() {
- mWaitForWifiCheckbox.setEnabled(false);
- mInstallButton.setEnabled(false);
- mInstallButton.setText(getString(R.string.installer_install_button_disabled_text));
+ private void setInstallEnabled(boolean enable) {
+ mInstallButton.setEnabled(enable);
+ mWaitForWifiCheckbox.setEnabled(enable);
+
+ int resId =
+ enable
+ ? R.string.installer_install_button_enabled_text
+ : R.string.installer_install_button_disabled_text;
+ mInstallButton.setText(getString(resId));
}
@MainThread
private void requestInstall() {
- preventInstall();
+ setInstallEnabled(/* enable= */ false);
if (mService != null) {
try {
@@ -123,6 +128,7 @@
handleCriticalError(e);
}
} else {
+ Log.d(TAG, "requestInstall() is called, but not yet connected");
mInstallRequested = true;
}
}
@@ -141,7 +147,7 @@
if (mInstallRequested) {
requestInstall();
} else if (mService.isInstalling()) {
- preventInstall();
+ setInstallEnabled(false);
}
} catch (RemoteException e) {
handleCriticalError(e);
@@ -153,6 +159,15 @@
handleCriticalError(new Exception("InstallerService is destroyed while in use"));
}
+ @MainThread
+ private void handleError(String displayText) {
+ // TODO(b/375542145): Display error with snackbar.
+ if (Build.isDebuggable()) {
+ Toast.makeText(this, displayText, Toast.LENGTH_LONG).show();
+ }
+ setInstallEnabled(true);
+ }
+
private static class InstallProgressListener extends IInstallProgressListener.Stub {
private final WeakReference<InstallerActivity> mActivity;
@@ -171,6 +186,27 @@
// MainActivity will be resume and handle rest of progress.
activity.finishWithResult(RESULT_OK);
}
+
+ @Override
+ public void onError(String displayText) {
+ InstallerActivity context = mActivity.get();
+ if (context == null) {
+ // Ignore incoming connection or disconnection after activity is destroyed.
+ return;
+ }
+
+ context.runOnUiThread(
+ () -> {
+ InstallerActivity activity = mActivity.get();
+ if (activity == null) {
+ // Ignore incoming connection or disconnection after activity is
+ // destroyed.
+ return;
+ }
+
+ activity.handleError(displayText);
+ });
+ }
}
@MainThread
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index f2c2867..b3102db 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -32,7 +32,18 @@
import com.android.internal.annotations.GuardedBy;
import com.android.virtualization.vmlauncher.InstallUtils;
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
import java.lang.ref.WeakReference;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -42,6 +53,10 @@
private static final String NOTIFICATION_CHANNEL_ID = "installer";
private static final int NOTIFICATION_ID = 1313; // any unique number among notifications
+ // TODO(b/369740847): Replace this URL with dl.google.com
+ private static final String IMAGE_URL =
+ "https://github.com/ikicha/debian_ci/releases/download/first/images.tar.gz";
+
private final Object mLock = new Object();
private Notification mNotification;
@@ -124,30 +139,89 @@
mExecutorService.execute(
() -> {
- Log.d(TAG, "installLinuxImage");
+ // TODO(b/374015561): Provide progress update
+ boolean success = downloadFromSdcard() || downloadFromUrl();
- // Installing from sdcard is preferred, but only supported only in debuggable
- // build.
- if (Build.isDebuggable()) {
- Log.i(TAG, "trying to install /sdcard/linux/images.tar.gz");
-
- // TODO(b/374015561): Provide progress update
- if (InstallUtils.installImageFromExternalStorage(this)) {
- Log.i(TAG, "image is installed from /sdcard/linux/images.tar.gz");
- } else {
- // TODO(b/374015561): Notify error
- Log.e(TAG, "Install failed");
- }
- }
stopForeground(STOP_FOREGROUND_REMOVE);
synchronized (mLock) {
mIsInstalling = false;
}
- notifyCompleted();
+ if (success) {
+ notifyCompleted();
+ }
});
}
+ private boolean downloadFromSdcard() {
+ // Installing from sdcard is preferred, but only supported only in debuggable build.
+ if (Build.isDebuggable()) {
+ Log.i(TAG, "trying to install /sdcard/linux/images.tar.gz");
+
+ if (InstallUtils.installImageFromExternalStorage(this)) {
+ Log.i(TAG, "image is installed from /sdcard/linux/images.tar.gz");
+ return true;
+ }
+ Log.i(TAG, "Failed to install /sdcard/linux/images.tar.gz");
+ } else {
+ Log.i(TAG, "Non-debuggable build doesn't support installation from /sdcard/linux");
+ }
+ return false;
+ }
+
+ // TODO(b/374015561): Support pause/resume download
+ // TODO(b/374015561): Wait for Wi-Fi on metered network if requested.
+ private boolean downloadFromUrl() {
+ Log.i(TAG, "trying to download from " + IMAGE_URL);
+
+ try (BufferedInputStream inputStream =
+ new BufferedInputStream(new URL(IMAGE_URL).openStream());
+ TarArchiveInputStream tar =
+ new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) {
+ ArchiveEntry entry;
+ Path baseDir = InstallUtils.getInternalStorageDir(this).toPath();
+ Files.createDirectories(baseDir);
+ while ((entry = tar.getNextEntry()) != null) {
+ Path extractTo = baseDir.resolve(entry.getName());
+ if (entry.isDirectory()) {
+ Files.createDirectories(extractTo);
+ } else {
+ Files.copy(tar, extractTo, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ } catch (UnknownHostException e) {
+ // Log.e() doesn't print stack trace for UnknownHostException
+ Log.e(TAG, "Install failed UnknownHostException: " + e.getMessage());
+ notifyError(getString(R.string.installer_install_network_error_message));
+ return false;
+ } catch (IOException e) {
+ // TODO(b/374015561): Provide more finer grained error message
+ Log.e(TAG, "Installation failed", e);
+ notifyError(getString(R.string.installer_error_unknown));
+ return false;
+ }
+
+ if (!InstallUtils.resolvePathInVmConfig(this)) {
+ // TODO(b/374015561): Provide more finer grained error message
+ notifyError(getString(R.string.installer_error_unknown));
+ return false;
+ }
+ return true;
+ }
+
+ private void notifyError(String displayText) {
+ IInstallProgressListener listener;
+ synchronized (mLock) {
+ listener = mListener;
+ }
+
+ try {
+ listener.onError(displayText);
+ } catch (Exception e) {
+ // ignore. Activity may not exist.
+ }
+ }
+
private void notifyCompleted() {
IInstallProgressListener listener;
synchronized (mLock) {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 206f5da..437e0be 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -25,7 +25,6 @@
import android.graphics.drawable.Icon;
import android.graphics.fonts.FontStyle;
import android.net.http.SslError;
-import android.os.Build;
import android.os.Bundle;
import android.system.ErrnoException;
import android.system.Os;
@@ -402,8 +401,7 @@
private boolean installIfNecessary() {
// If payload from external storage exists(only for debuggable build) or there is no
// installed image, launch installer activity.
- if ((Build.isDebuggable() && InstallUtils.payloadFromExternalStorageExists())
- || !InstallUtils.isImageInstalled(this)) {
+ if (!InstallUtils.isImageInstalled(this)) {
Intent intent = new Intent(this, InstallerActivity.class);
startActivityForResult(intent, REQUEST_CODE_INSTALLER);
return true;
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 7cacd3b..f8350a0 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -36,6 +36,10 @@
<string name="installer_notif_title_text">Installing Linux terminal</string>
<!-- Notification description for installer [CHAR LIMIT=none] -->
<string name="installer_notif_desc_text">Linux terminal will be started after finish</string>
+ <!-- Toast error message for install failure due to the network issue [CHAR LIMIT=none] -->
+ <string name="installer_error_network">Failed to install due to the network issue</string>
+ <!-- Toast error message for install failure due to the unidentified issue [CHAR LIMIT=none] -->
+ <string name="installer_error_unknown">Failed to install. Try again.</string>
<!-- Action bar icon name for the settings view CHAR LIMIT=none] -->
<string name="action_settings">Settings</string>