Merge "VmTerminalApp: Implement Wi-Fi only checkbox in installer" into main
diff --git a/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl
index daf1fa4..1ae1951 100644
--- a/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl
+++ b/android/TerminalApp/aidl/com/android/virtualization/terminal/IInstallerService.aidl
@@ -19,7 +19,7 @@
 import com.android.virtualization.terminal.IInstallProgressListener;
 
 interface IInstallerService {
-    void requestInstall();
+    void requestInstall(boolean isWifiOnly);
     void setProgressListener(in IInstallProgressListener listener);
 
     boolean isInstalling();
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index 4d8c00a..a49403c 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -168,7 +168,7 @@
 
         if (mService != null) {
             try {
-                mService.requestInstall();
+                mService.requestInstall(mWaitForWifiCheckbox.isChecked());
             } catch (RemoteException e) {
                 handleCriticalError(e);
             }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index f839c64..5d4c4ad 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -21,11 +21,15 @@
 import android.app.Service;
 import android.content.Intent;
 import android.content.pm.ServiceInfo;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.os.Build;
 import android.os.IBinder;
 import android.os.SELinux;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.GuardedBy;
@@ -37,7 +41,9 @@
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.lang.ref.WeakReference;
+import java.net.SocketException;
 import java.net.URL;
 import java.net.UnknownHostException;
 import java.nio.file.Files;
@@ -69,9 +75,14 @@
     private boolean mIsInstalling;
 
     @GuardedBy("mLock")
+    private boolean mHasWifi;
+
+    @GuardedBy("mLock")
     private IInstallProgressListener mListener;
 
     private ExecutorService mExecutorService;
+    private ConnectivityManager mConnectivityManager;
+    private MyNetworkCallback mNetworkCallback;
 
     @Override
     public void onCreate() {
@@ -91,6 +102,18 @@
                         .build();
 
         mExecutorService = Executors.newSingleThreadExecutor();
+
+        mConnectivityManager = getSystemService(ConnectivityManager.class);
+        Network defaultNetwork = mConnectivityManager.getBoundNetworkForProcess();
+        if (defaultNetwork != null) {
+            NetworkCapabilities capability =
+                    mConnectivityManager.getNetworkCapabilities(defaultNetwork);
+            if (capability != null) {
+                mHasWifi = capability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI);
+            }
+        }
+        mNetworkCallback = new MyNetworkCallback();
+        mConnectivityManager.registerDefaultNetworkCallback(mNetworkCallback);
     }
 
     @Nullable
@@ -116,9 +139,10 @@
         if (mExecutorService != null) {
             mExecutorService.shutdown();
         }
+        mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
     }
 
-    private void requestInstall() {
+    private void requestInstall(boolean isWifiOnly) {
         synchronized (mLock) {
             if (mIsInstalling) {
                 Log.i(TAG, "already installing..");
@@ -137,8 +161,7 @@
 
         mExecutorService.execute(
                 () -> {
-                    // TODO(b/374015561): Provide progress update
-                    boolean success = downloadFromSdcard() || downloadFromUrl();
+                    boolean success = downloadFromSdcard() || downloadFromUrl(isWifiOnly);
                     if (success) {
                         reLabelImagesSELinuxContext();
                     }
@@ -187,15 +210,31 @@
         return false;
     }
 
+    private boolean checkForWifiOnly(boolean isWifiOnly) {
+        if (!isWifiOnly) {
+            return true;
+        }
+        synchronized (mLock) {
+            return mHasWifi;
+        }
+    }
+
     // TODO(b/374015561): Support pause/resume download
-    // TODO(b/374015561): Wait for Wi-Fi on metered network if requested.
-    private boolean downloadFromUrl() {
+    private boolean downloadFromUrl(boolean isWifiOnly) {
         Log.i(TAG, "trying to download from " + IMAGE_URL);
 
+        if (!checkForWifiOnly(isWifiOnly)) {
+            Log.e(TAG, "Install isn't started because Wifi isn't available");
+            notifyError(getString(R.string.installer_error_no_wifi));
+            return false;
+        }
+
         try (BufferedInputStream inputStream =
                         new BufferedInputStream(new URL(IMAGE_URL).openStream());
+                WifiCheckInputStream wifiInputStream =
+                        new WifiCheckInputStream(inputStream, isWifiOnly);
                 TarArchiveInputStream tar =
-                        new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) {
+                        new TarArchiveInputStream(new GzipCompressorInputStream(wifiInputStream))) {
             ArchiveEntry entry;
             Path baseDir = InstallUtils.getInternalStorageDir(this).toPath();
             Files.createDirectories(baseDir);
@@ -207,20 +246,22 @@
                     Files.copy(tar, extractTo, StandardCopyOption.REPLACE_EXISTING);
                 }
             }
-        } catch (UnknownHostException e) {
+        } catch (WifiCheckInputStream.NoWifiException e) {
+            Log.e(TAG, "Install failed because of Wi-Fi is gone");
+            notifyError(getString(R.string.installer_error_no_wifi));
+            return false;
+        } catch (UnknownHostException | SocketException 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));
+            Log.e(TAG, "Install failed: " + e.getMessage(), e);
+            notifyError(getString(R.string.installer_error_network));
             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;
         }
@@ -271,10 +312,10 @@
         }
 
         @Override
-        public void requestInstall() {
+        public void requestInstall(boolean isWifiOnly) {
             InstallerService service = ensureServiceConnected();
             synchronized (service.mLock) {
-                service.requestInstall();
+                service.requestInstall(isWifiOnly);
             }
         }
 
@@ -302,4 +343,57 @@
             }
         }
     }
+
+    private final class WifiCheckInputStream extends InputStream {
+        private static final int READ_BYTES = 1024;
+
+        private final InputStream mInputStream;
+        private final boolean mIsWifiOnly;
+
+        public WifiCheckInputStream(InputStream is, boolean isWifiOnly) {
+            super();
+            mInputStream = is;
+            mIsWifiOnly = isWifiOnly;
+        }
+
+        @Override
+        public int read(byte[] buf, int offset, int numToRead) throws IOException {
+            int totalRead = 0;
+            while (numToRead > 0) {
+                if (!checkForWifiOnly(mIsWifiOnly)) {
+                    throw new NoWifiException();
+                }
+                int read =
+                        mInputStream.read(buf, offset + totalRead, Math.min(READ_BYTES, numToRead));
+                if (read <= 0) {
+                    break;
+                }
+                totalRead += read;
+                numToRead -= read;
+            }
+            return totalRead;
+        }
+
+        @Override
+        public int read() throws IOException {
+            if (!checkForWifiOnly(mIsWifiOnly)) {
+                throw new NoWifiException();
+            }
+            return mInputStream.read();
+        }
+
+        private static final class NoWifiException extends SocketException {
+            // empty
+        }
+    }
+
+    private final class MyNetworkCallback extends ConnectivityManager.NetworkCallback {
+        @Override
+        public void onCapabilitiesChanged(
+                @NonNull Network network, @NonNull NetworkCapabilities capability) {
+            synchronized (mLock) {
+                mHasWifi = capability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI);
+            }
+        }
+    }
 }
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 300cbbc..d498286 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -38,6 +38,8 @@
     <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 because Wi-Fi isn't available although required [CHAR LIMIT=none] -->
+    <string name="installer_error_no_wifi">Failed to install because Wi-Fi isn\'t available</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>