VmTerminalApp: Add UI skeleton for download activity and service

Lanuch flow around installation.
  - Step 1) MainActivity launch VM if installed.
  - Step 2) If not installed, launch InstallerActivity.
  - Step 3) When user clicks install, InstallerService installs image
            as a foreground service. Installation would be ongoing
            and notification will be there.
  - Step 4) When installation is done, resume to the MainActivity.
            If InstallerActivity was shown, it will be finished.
            If not, MainActivity would only be resumed via user's
            action.

Proper download logic will be handled in the next CL

BYPASS_LARGE_CHANGE_WARNING=~200 lines are file headers and resources.

Bug: 369740847
Test: Manually
Change-Id: I18af2dedc998998ae14dbf9a9146a0ca91bc5778
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index 932ca76..5e45c02 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -11,6 +11,7 @@
     resource_dirs: ["res"],
     asset_dirs: ["assets"],
     static_libs: [
+        "VmTerminalApp.aidl-java",
         "vm_launcher_lib",
         "androidx-constraintlayout_constraintlayout",
         "com.google.android.material_material",
@@ -26,3 +27,18 @@
         "com.android.virt",
     ],
 }
+
+aidl_interface {
+    name: "VmTerminalApp.aidl",
+    srcs: ["aidl/**/*.aidl"],
+    unstable: true,
+    local_include_dir: "aidl",
+    backend: {
+        java: {
+            enabled: true,
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
+    },
+}
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 28b5436..67ef199 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -55,6 +55,11 @@
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
         </activity>
+        <service android:name=".InstallerService"
+            android:foregroundServiceType="specialUse"
+            android:value="Prepares Linux image"
+            android:exported="false"
+            android:stopWithTask="true" />
 
         <service
             android:name="com.android.virtualization.vmlauncher.VmLauncherService"
diff --git a/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl
new file mode 100644
index 0000000..1f51723
--- /dev/null
+++ b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallProgressListener.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+package com.android.virtualization.terminal;
+
+// TODO(b/374015561): Provide progress update
+oneway interface IInstallProgressListener {
+    void onCompleted();
+}
diff --git a/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl
new file mode 100644
index 0000000..daf1fa4
--- /dev/null
+++ b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+package com.android.virtualization.terminal;
+
+import com.android.virtualization.terminal.IInstallProgressListener;
+
+interface IInstallerService {
+    void requestInstall();
+    void setProgressListener(in IInstallProgressListener listener);
+
+    boolean isInstalling();
+    boolean isInstalled();
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
new file mode 100644
index 0000000..66552d5
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
@@ -0,0 +1,38 @@
+/*
+ * 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.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.Manifest;
+import android.content.pm.PackageManager;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+public abstract class BaseActivity extends AppCompatActivity {
+    private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        if (getApplicationContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
+                != PackageManager.PERMISSION_GRANTED) {
+            requestPermissions(
+                    new String[] {Manifest.permission.POST_NOTIFICATIONS},
+                    POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE);
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index a49ea72..25780a5 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -16,56 +16,199 @@
 
 package com.android.virtualization.terminal;
 
-import android.app.Activity;
+import android.annotation.MainThread;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.FileUtils;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.format.Formatter;
 import android.util.Log;
+import android.widget.CheckBox;
 import android.widget.TextView;
+import android.widget.Toast;
 
-import com.android.virtualization.vmlauncher.InstallUtils;
-
+import java.lang.ref.WeakReference;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
-public class InstallerActivity extends Activity {
+public class InstallerActivity extends BaseActivity {
     private static final String TAG = "LinuxInstaller";
 
-    ExecutorService executorService = Executors.newSingleThreadExecutor();
+    private static final long ESTIMATED_IMG_SIZE_BYTES = FileUtils.parseSize("350MB");
+
+    private ExecutorService mExecutorService;
+    private CheckBox mWaitForWifiCheckbox;
+    private TextView mInstallButton;
+
+    private IInstallerService mService;
+    private ServiceConnection mInstallerServiceConnection;
+    private InstallProgressListener mInstallProgressListener;
+    private boolean mInstallRequested;
 
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
+    public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setResult(RESULT_CANCELED);
 
+        mInstallProgressListener = new InstallProgressListener(this);
+
         setContentView(R.layout.activity_installer);
 
-        executorService.execute(this::installLinuxImage);
+        TextView desc = (TextView) findViewById(R.id.installer_desc);
+        desc.setText(
+                getString(
+                        R.string.installer_desc_text_format,
+                        Formatter.formatShortFileSize(this, ESTIMATED_IMG_SIZE_BYTES)));
+
+        mWaitForWifiCheckbox = (CheckBox) findViewById(R.id.installer_wait_for_wifi_checkbox);
+        mInstallButton = (TextView) findViewById(R.id.installer_install_button);
+
+        mInstallButton.setOnClickListener(
+                (event) -> {
+                    requestInstall();
+                });
+
+        Intent intent = new Intent(this, InstallerService.class);
+        mInstallerServiceConnection = new InstallerServiceConnection(this);
+        if (!bindService(intent, mInstallerServiceConnection, Context.BIND_AUTO_CREATE)) {
+            handleCriticalError(new Exception("Failed to connect to installer service"));
+        }
     }
 
-    private void installLinuxImage() {
-        Log.d(TAG, "installLinuxImage");
-        // Installing from sdcard is supported only in debuggable build.
-        if (Build.isDebuggable()) {
-            updateStatus("try /sdcard/linux/images.tar.gz");
-            if (InstallUtils.installImageFromExternalStorage(this)) {
-                Log.d(TAG, "success / sdcard");
-                updateStatus("image is installed from /sdcard/linux/images.tar.gz");
-                setResult(RESULT_OK);
-                finish();
-                return;
-            }
-            Log.d(TAG, "fail / sdcard");
-            updateStatus("There is no /sdcard/linux/images.tar.gz");
+    @Override
+    public void onDestroy() {
+        if (mInstallerServiceConnection != null) {
+            unbindService(mInstallerServiceConnection);
+            mInstallerServiceConnection = null;
         }
-        setResult(RESULT_CANCELED, null);
+
+        super.onDestroy();
+    }
+
+    public void handleCriticalError(Exception e) {
+        if (Build.isDebuggable()) {
+            Toast.makeText(
+                            this,
+                            e.getMessage() + ". File a bugreport to go/ferrochrome-bug",
+                            Toast.LENGTH_LONG)
+                    .show();
+        }
+        Log.e(TAG, "Internal error", e);
+        finishWithResult(RESULT_CANCELED);
+    }
+
+    private void finishWithResult(int resultCode) {
+        setResult(resultCode);
         finish();
     }
 
-    private void updateStatus(String line) {
-        runOnUiThread(
-                () -> {
-                    TextView statusView = findViewById(R.id.status_txt_view);
-                    statusView.append(line + "\n");
-                });
+    private void preventInstall() {
+        mWaitForWifiCheckbox.setEnabled(false);
+        mInstallButton.setEnabled(false);
+        mInstallButton.setText(getString(R.string.installer_install_button_disabled_text));
+    }
+
+    @MainThread
+    private void requestInstall() {
+        preventInstall();
+
+        if (mService != null) {
+            try {
+                mService.requestInstall();
+            } catch (RemoteException e) {
+                handleCriticalError(e);
+            }
+        } else {
+            mInstallRequested = true;
+        }
+    }
+
+    @MainThread
+    public void handleInstallerServiceConnected() {
+        try {
+            mService.setProgressListener(mInstallProgressListener);
+            if (mService.isInstalled()) {
+                // Finishing this activity will trigger MainActivity::onResume(),
+                // and VM will be started from there.
+                finishWithResult(RESULT_OK);
+                return;
+            }
+
+            if (mInstallRequested) {
+                requestInstall();
+            } else if (mService.isInstalling()) {
+                preventInstall();
+            }
+        } catch (RemoteException e) {
+            handleCriticalError(e);
+        }
+    }
+
+    @MainThread
+    public void handleInstallerServiceDisconnected() {
+        handleCriticalError(new Exception("InstallerService is destroyed while in use"));
+    }
+
+    private static class InstallProgressListener extends IInstallProgressListener.Stub {
+        private final WeakReference<InstallerActivity> mActivity;
+
+        InstallProgressListener(InstallerActivity activity) {
+            mActivity = new WeakReference<>(activity);
+        }
+
+        @Override
+        public void onCompleted() {
+            InstallerActivity activity = mActivity.get();
+            if (activity == null) {
+                // Ignore incoming connection or disconnection after activity is destroyed.
+                return;
+            }
+
+            // MainActivity will be resume and handle rest of progress.
+            activity.finishWithResult(RESULT_OK);
+        }
+    }
+
+    @MainThread
+    public static final class InstallerServiceConnection implements ServiceConnection {
+        private final WeakReference<InstallerActivity> mActivity;
+
+        InstallerServiceConnection(InstallerActivity activity) {
+            mActivity = new WeakReference<>(activity);
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            InstallerActivity activity = mActivity.get();
+            if (activity == null || activity.mInstallerServiceConnection == null) {
+                // Ignore incoming connection or disconnection after activity is destroyed.
+                return;
+            }
+            if (service == null) {
+                activity.handleCriticalError(new Exception("service shouldn't be null"));
+            }
+
+            activity.mService = IInstallerService.Stub.asInterface(service);
+            activity.handleInstallerServiceConnected();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            InstallerActivity activity = mActivity.get();
+            if (activity == null || activity.mInstallerServiceConnection == null) {
+                // Ignore incoming connection or disconnection after activity is destroyed.
+                return;
+            }
+
+            if (activity.mInstallerServiceConnection != null) {
+                activity.unbindService(activity.mInstallerServiceConnection);
+                activity.mInstallerServiceConnection = null;
+            }
+            activity.handleInstallerServiceDisconnected();
+        }
     }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
new file mode 100644
index 0000000..f2c2867
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -0,0 +1,213 @@
+/*
+ * 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.
+ */
+
+package com.android.virtualization.terminal;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.virtualization.vmlauncher.InstallUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class InstallerService extends Service {
+    private static final String TAG = "InstallerService";
+
+    private static final String NOTIFICATION_CHANNEL_ID = "installer";
+    private static final int NOTIFICATION_ID = 1313; // any unique number among notifications
+
+    private final Object mLock = new Object();
+
+    private Notification mNotification;
+
+    @GuardedBy("mLock")
+    private boolean mIsInstalling;
+
+    @GuardedBy("mLock")
+    private IInstallProgressListener mListener;
+
+    private ExecutorService mExecutorService;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        // Create mandatory notification
+        NotificationManager manager = getSystemService(NotificationManager.class);
+        if (manager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
+            NotificationChannel channel =
+                    new NotificationChannel(
+                            NOTIFICATION_CHANNEL_ID,
+                            getString(R.string.installer_notif_title_text),
+                            NotificationManager.IMPORTANCE_DEFAULT);
+            manager.createNotificationChannel(channel);
+        }
+
+        Intent intent = new Intent(this, MainActivity.class);
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(
+                        this, /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE);
+        mNotification =
+                new Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
+                        .setSmallIcon(R.drawable.ic_launcher_foreground)
+                        .setContentTitle(getString(R.string.installer_notif_title_text))
+                        .setContentText(getString(R.string.installer_notif_desc_text))
+                        .setOngoing(true)
+                        .setContentIntent(pendingIntent)
+                        .build();
+
+        mExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new InstallerServiceImpl(this);
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        super.onStartCommand(intent, flags, startId);
+
+        Log.d(TAG, "Starting service ...");
+
+        return START_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+
+        Log.d(TAG, "Service is destroyed");
+        if (mExecutorService != null) {
+            mExecutorService.shutdown();
+        }
+    }
+
+    private void requestInstall() {
+        Log.i(TAG, "Installing..");
+
+        // Make service to be long running, even after unbind() when InstallerActivity is destroyed
+        // The service will still be destroyed if task is remove.
+        startService(new Intent(this, InstallerService.class));
+        startForeground(
+                NOTIFICATION_ID, mNotification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
+        synchronized (mLock) {
+            mIsInstalling = true;
+        }
+
+        mExecutorService.execute(
+                () -> {
+                    Log.d(TAG, "installLinuxImage");
+
+                    // 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();
+                });
+    }
+
+    private void notifyCompleted() {
+        IInstallProgressListener listener;
+        synchronized (mLock) {
+            listener = mListener;
+        }
+
+        try {
+            listener.onCompleted();
+        } catch (Exception e) {
+            // ignore. Activity may not exist.
+        }
+    }
+
+    private static final class InstallerServiceImpl extends IInstallerService.Stub {
+        // Holds weak reference to avoid Context leak
+        private final WeakReference<InstallerService> mService;
+
+        public InstallerServiceImpl(InstallerService service) {
+            mService = new WeakReference<>(service);
+        }
+
+        private InstallerService ensureServiceConnected() throws RuntimeException {
+            InstallerService service = mService.get();
+            if (service == null) {
+                throw new RuntimeException(
+                        "Internal error: Installer service is being accessed after destroyed");
+            }
+            return service;
+        }
+
+        @Override
+        public void requestInstall() {
+            InstallerService service = ensureServiceConnected();
+            synchronized (service.mLock) {
+                service.requestInstall();
+            }
+        }
+
+        @Override
+        public void setProgressListener(IInstallProgressListener listener) {
+            InstallerService service = ensureServiceConnected();
+            synchronized (service.mLock) {
+                service.mListener = listener;
+            }
+        }
+
+        @Override
+        public boolean isInstalling() {
+            InstallerService service = ensureServiceConnected();
+            synchronized (service.mLock) {
+                return service.mIsInstalling;
+            }
+        }
+
+        @Override
+        public boolean isInstalled() {
+            InstallerService service = ensureServiceConnected();
+            synchronized (service.mLock) {
+                return !service.mIsInstalling && InstallUtils.isImageInstalled(service);
+            }
+        }
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 0a750e3..206f5da 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -15,14 +15,12 @@
  */
 package com.android.virtualization.terminal;
 
-import android.Manifest;
 import android.app.Notification;
 import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.graphics.drawable.Icon;
 import android.graphics.fonts.FontStyle;
@@ -45,8 +43,6 @@
 import android.webkit.WebViewClient;
 import android.widget.Toast;
 
-import androidx.appcompat.app.AppCompatActivity;
-
 import com.android.virtualization.vmlauncher.InstallUtils;
 import com.android.virtualization.vmlauncher.VmLauncherServices;
 
@@ -68,7 +64,7 @@
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
 
-public class MainActivity extends AppCompatActivity
+public class MainActivity extends BaseActivity
         implements VmLauncherServices.VmLauncherServiceCallback,
                 AccessibilityManager.TouchExplorationStateChangeListener {
 
@@ -99,13 +95,12 @@
                     .show();
         }
 
-        checkAndRequestPostNotificationsPermission();
-
         NotificationManager notificationManager = getSystemService(NotificationManager.class);
-        NotificationChannel notificationChannel =
-                new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
-        assert notificationManager != null;
-        notificationManager.createNotificationChannel(notificationChannel);
+        if (notificationManager.getNotificationChannel(TAG) == null) {
+            NotificationChannel notificationChannel =
+                    new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
+            notificationManager.createNotificationChannel(notificationChannel);
+        }
 
         setContentView(R.layout.activity_headless);
 
@@ -338,15 +333,6 @@
         return;
     }
 
-    private void checkAndRequestPostNotificationsPermission() {
-        if (getApplicationContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
-                != PackageManager.PERMISSION_GRANTED) {
-            requestPermissions(
-                    new String[]{Manifest.permission.POST_NOTIFICATIONS},
-                    POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE);
-        }
-    }
-
     @Override
     protected void onDestroy() {
         getSystemService(AccessibilityManager.class).removeTouchExplorationStateChangeListener(this);
diff --git a/android/TerminalApp/res/drawable/ic_lock_open.xml b/android/TerminalApp/res/drawable/ic_lock_open.xml
new file mode 100644
index 0000000..c623592
--- /dev/null
+++ b/android/TerminalApp/res/drawable/ic_lock_open.xml
@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M220,326L610,326L610,230Q610,175.83 572.12,137.92Q534.24,100 480.12,100Q426,100 388,137.92Q350,175.83 350,230L290,230Q290,151 345.61,95.5Q401.21,40 480.11,40Q559,40 614.5,95.58Q670,151.15 670,230L670,326L740,326Q764.75,326 782.38,343.62Q800,361.25 800,386L800,820Q800,844.75 782.38,862.37Q764.75,880 740,880L220,880Q195.25,880 177.63,862.37Q160,844.75 160,820L160,386Q160,361.25 177.63,343.62Q195.25,326 220,326ZM220,820L740,820Q740,820 740,820Q740,820 740,820L740,386Q740,386 740,386Q740,386 740,386L220,386Q220,386 220,386Q220,386 220,386L220,820Q220,820 220,820Q220,820 220,820ZM480.17,680Q512,680 534.5,657.97Q557,635.94 557,605Q557,575 534.33,550.5Q511.66,526 479.83,526Q448,526 425.5,550.5Q403,575 403,605.5Q403,636 425.67,658Q448.34,680 480.17,680ZM220,820Q220,820 220,820Q220,820 220,820L220,386Q220,386 220,386Q220,386 220,386L220,386Q220,386 220,386Q220,386 220,386L220,820Q220,820 220,820Q220,820 220,820Z"/>
+</vector>
+
diff --git a/android/TerminalApp/res/layout/activity_installer.xml b/android/TerminalApp/res/layout/activity_installer.xml
index 3967167..c375cb8 100644
--- a/android/TerminalApp/res/layout/activity_installer.xml
+++ b/android/TerminalApp/res/layout/activity_installer.xml
@@ -6,10 +6,51 @@
     android:fitsSystemWindows="true"
     android:paddingLeft="16dp"
     android:paddingRight="16dp">
-  <TextView
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content"
-      android:textSize="28sp"
-      android:id="@+id/status_txt_view"/>
+    <TextView
+        android:id="@+id/installer_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentStart="true"
+        android:textSize="32sp"
+        android:text="@string/installer_title_text" />
+
+    <ImageView
+        android:id="@+id/installer_icon"
+        android:layout_width="match_parent"
+        android:layout_height="300dp"
+        android:padding="10dp"
+        android:layout_below="@id/installer_title"
+        android:layout_alignParentStart="true"
+        android:src="@drawable/ic_lock_open"
+        android:adjustViewBounds="true" />
+
+    <TextView
+        android:id="@+id/installer_desc"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:layout_below="@id/installer_icon"
+        android:layout_alignParentStart="true"
+        android:singleLine="false"
+        android:textSize="24sp" />
+
+    <CheckBox
+        android:id="@+id/installer_wait_for_wifi_checkbox"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:layout_alignParentEnd="true"
+        android:layout_below="@id/installer_desc"
+        android:text="@string/installer_wait_for_wifi_checkbox_text" />
+
+    <Button
+        android:id="@+id/installer_install_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="10dp"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentEnd="true"
+        android:text="@string/installer_install_button_enabled_text" />
 
 </RelativeLayout>
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 0cdb939..7cacd3b 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -20,6 +20,23 @@
     <!-- Application name of this terminal app shown in the launcher. This app provides computer terminal to connect to virtual machine. [CHAR LIMIT=16] -->
     <string name="app_name">Terminal</string>
 
+    <!-- Installer activity title [CHAR LIMIT=none] -->
+    <string name="installer_title_text">Install Linux terminal</string>
+    <!-- Installer activity description format [CHAR LIMIT=none] -->
+    <string name="installer_desc_text_format">To launch Linux terminal, you need to download roughly <xliff:g id="expected_size" example="350GB">%1$s</xliff:g> of data over network.\nWould you proceed?</string>
+    <!-- Checkbox at the installer activity to wait for Wi-Fi on metered network to prevent from paying network traffic [CHAR LIMIT=none] -->
+    <string name="installer_wait_for_wifi_checkbox_text">Wait for Wi-Fi on metered network</string>
+    <!-- Button at the installer activity to confirm installation [CHAR LIMIT=16] -->
+    <string name="installer_install_button_enabled_text">Install</string>
+    <!-- Button at the installer activity to when installation is already in progress [CHAR LIMIT=16] -->
+    <string name="installer_install_button_disabled_text">Installing</string>
+    <!-- Toast message at installer activity when network doesn't meet[CHAR LIMIT=none] -->
+    <string name="installer_install_network_error_message">Network error. Check connection and retry.</string>
+    <!-- Notification title for installer [CHAR LIMIT=64] -->
+    <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>
+
     <!-- Action bar icon name for the settings view CHAR LIMIT=none] -->
     <string name="action_settings">Settings</string>
 
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
index 849cc24..3731854 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -62,8 +62,8 @@
 
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
-        if (isVmRunning()) {
-            Log.d(TAG, "there is already the running VM instance");
+        if (mVirtualMachine != null) {
+            Log.d(TAG, "VM instance is already started");
             return START_NOT_STICKY;
         }
         mExecutorService = Executors.newCachedThreadPool();
@@ -114,12 +114,14 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        if (isVmRunning()) {
-            try {
-                mVirtualMachine.stop();
-                stopForeground(STOP_FOREGROUND_REMOVE);
-            } catch (VirtualMachineException e) {
-                Log.e(TAG, "failed to stop a VM instance", e);
+        if (mVirtualMachine != null) {
+            if (mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING) {
+                try {
+                    mVirtualMachine.stop();
+                    stopForeground(STOP_FOREGROUND_REMOVE);
+                } catch (VirtualMachineException e) {
+                    Log.e(TAG, "failed to stop a VM instance", e);
+                }
             }
             mExecutorService.shutdownNow();
             mExecutorService = null;
@@ -128,11 +130,6 @@
         stopDebianServer();
     }
 
-    private boolean isVmRunning() {
-        return mVirtualMachine != null
-                && mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING;
-    }
-
     private void startDebianServer() {
         new Thread(
                         () -> {