Merge changes I39e5d8f9,I01e89a6b into main
* changes:
Parse resize2fs output correctly
Make error cause text view scrollable
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
deleted file mode 100644
index aeae5dd..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-public abstract class BaseActivity extends AppCompatActivity {
- private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- NotificationManager notificationManager = getSystemService(NotificationManager.class);
- if (notificationManager.getNotificationChannel(this.getPackageName()) == null) {
- NotificationChannel channel =
- new NotificationChannel(
- this.getPackageName(),
- getString(R.string.app_name),
- NotificationManager.IMPORTANCE_HIGH);
- notificationManager.createNotificationChannel(channel);
- }
-
- if (!(this instanceof ErrorActivity)) {
- Thread currentThread = Thread.currentThread();
- if (!(currentThread.getUncaughtExceptionHandler()
- instanceof TerminalExceptionHandler)) {
- currentThread.setUncaughtExceptionHandler(
- new TerminalExceptionHandler(getApplicationContext()));
- }
- }
- }
-
- @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/BaseActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt
new file mode 100644
index 0000000..e7ac8d9
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/BaseActivity.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.pm.PackageManager
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+
+abstract class BaseActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val notificationManager =
+ getSystemService<NotificationManager>(NotificationManager::class.java)
+ if (notificationManager.getNotificationChannel(this.packageName) == null) {
+ val channel =
+ NotificationChannel(
+ this.packageName,
+ getString(R.string.app_name),
+ NotificationManager.IMPORTANCE_HIGH,
+ )
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ if (this !is ErrorActivity) {
+ val currentThread = Thread.currentThread()
+ if (currentThread.uncaughtExceptionHandler !is TerminalExceptionHandler) {
+ currentThread.uncaughtExceptionHandler =
+ TerminalExceptionHandler(applicationContext)
+ }
+ }
+ }
+
+ public override fun onResume() {
+ super.onResume()
+
+ if (
+ applicationContext.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
+ PackageManager.PERMISSION_GRANTED
+ ) {
+ requestPermissions(
+ arrayOf<String>(Manifest.permission.POST_NOTIFICATIONS),
+ POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE,
+ )
+ }
+ }
+
+ companion object {
+ private const val POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
index 147a7e5..b4dffee 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
@@ -34,7 +34,6 @@
import io.grpc.stub.StreamObserver;
import java.util.Set;
-import java.util.stream.Collectors;
final class DebianServiceImpl extends DebianServiceGrpc.DebianServiceImplBase {
private final Context mContext;
@@ -56,11 +55,8 @@
public void reportVmActivePorts(
ReportVmActivePortsRequest request,
StreamObserver<ReportVmActivePortsResponse> responseObserver) {
- Log.d(TAG, "reportVmActivePorts: " + request.toString());
- mPortsStateManager.updateActivePorts(
- request.getPortsList().stream()
- .map(activePort -> activePort.getPort())
- .collect(Collectors.toSet()));
+ mPortsStateManager.updateActivePorts(request.getPortsList());
+ Log.d(TAG, "reportVmActivePorts: " + mPortsStateManager.getActivePorts());
ReportVmActivePortsResponse reply =
ReportVmActivePortsResponse.newBuilder().setSuccess(true).build();
responseObserver.onNext(reply);
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
deleted file mode 100644
index 1c62572..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ /dev/null
@@ -1,314 +0,0 @@
-/*
- * 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 static com.android.virtualization.terminal.MainActivity.TAG;
-
-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.ConditionVariable;
-import android.os.FileUtils;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.text.format.Formatter;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.View;
-import android.widget.CheckBox;
-import android.widget.TextView;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import com.google.android.material.progressindicator.LinearProgressIndicator;
-import com.google.android.material.snackbar.Snackbar;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-
-public class InstallerActivity extends BaseActivity {
- private static final long ESTIMATED_IMG_SIZE_BYTES = FileUtils.parseSize("550MB");
-
- private CheckBox mWaitForWifiCheckbox;
- private TextView mInstallButton;
-
- private IInstallerService mService;
- private ServiceConnection mInstallerServiceConnection;
- private InstallProgressListener mInstallProgressListener;
- private boolean mInstallRequested;
- private ConditionVariable mInstallCompleted = new ConditionVariable();
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setResult(RESULT_CANCELED);
-
- mInstallProgressListener = new InstallProgressListener(this);
-
- setContentView(R.layout.activity_installer);
- updateSizeEstimation(ESTIMATED_IMG_SIZE_BYTES);
- measureImageSizeAndUpdateDescription();
-
- 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)) {
- handleInternalError(new Exception("Failed to connect to installer service"));
- }
- }
-
- private void updateSizeEstimation(long est) {
- String desc =
- getString(
- R.string.installer_desc_text_format,
- Formatter.formatShortFileSize(this, est));
- runOnUiThread(
- () -> {
- TextView view = (TextView) findViewById(R.id.installer_desc);
- view.setText(desc);
- });
- }
-
- private void measureImageSizeAndUpdateDescription() {
- new Thread(
- () -> {
- long est;
- try {
- est = ImageArchive.getDefault().getSize();
- } catch (IOException e) {
- Log.w(TAG, "Failed to measure image size.", e);
- return;
- }
- updateSizeEstimation(est);
- })
- .start();
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- if (Build.isDebuggable() && ImageArchive.fromSdCard().exists()) {
- showSnackbar("Auto installing", Snackbar.LENGTH_LONG);
- requestInstall();
- }
- }
-
- @Override
- public void onDestroy() {
- if (mInstallerServiceConnection != null) {
- unbindService(mInstallerServiceConnection);
- mInstallerServiceConnection = null;
- }
-
- super.onDestroy();
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BUTTON_START) {
- requestInstall();
- return true;
- }
- return super.onKeyUp(keyCode, event);
- }
-
- @VisibleForTesting
- public boolean waitForInstallCompleted(long timeoutMillis) {
- return mInstallCompleted.block(timeoutMillis);
- }
-
- private void showSnackbar(String message, int length) {
- Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), message, length);
- snackbar.setAnchorView(mWaitForWifiCheckbox);
- snackbar.show();
- }
-
- public void handleInternalError(Exception e) {
- if (Build.isDebuggable()) {
- showSnackbar(
- e.getMessage() + ". File a bugreport to go/ferrochrome-bug",
- Snackbar.LENGTH_INDEFINITE);
- }
- Log.e(TAG, "Internal error", e);
- finishWithResult(RESULT_CANCELED);
- }
-
- private void finishWithResult(int resultCode) {
- if (resultCode == RESULT_OK) {
- mInstallCompleted.open();
- }
- setResult(resultCode);
- finish();
- }
-
- private void setInstallEnabled(boolean enable) {
- mInstallButton.setEnabled(enable);
- mWaitForWifiCheckbox.setEnabled(enable);
- LinearProgressIndicator progressBar = findViewById(R.id.installer_progress);
- if (enable) {
- progressBar.setVisibility(View.INVISIBLE);
- } else {
- progressBar.setVisibility(View.VISIBLE);
- }
-
- 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() {
- setInstallEnabled(/* enable= */ false);
-
- if (mService != null) {
- try {
- mService.requestInstall(mWaitForWifiCheckbox.isChecked());
- } catch (RemoteException e) {
- handleInternalError(e);
- }
- } else {
- Log.d(TAG, "requestInstall() is called, but not yet connected");
- 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()) {
- setInstallEnabled(false);
- }
- } catch (RemoteException e) {
- handleInternalError(e);
- }
- }
-
- @MainThread
- public void handleInstallerServiceDisconnected() {
- handleInternalError(new Exception("InstallerService is destroyed while in use"));
- }
-
- @MainThread
- private void handleInstallError(String displayText) {
- showSnackbar(displayText, Snackbar.LENGTH_LONG);
- setInstallEnabled(true);
- }
-
- 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);
- }
-
- @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.handleInstallError(displayText);
- });
- }
- }
-
- @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.handleInternalError(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/InstallerActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.kt
new file mode 100644
index 0000000..99c64f7
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.kt
@@ -0,0 +1,288 @@
+/*
+ * 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.annotation.MainThread
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.Bundle
+import android.os.ConditionVariable
+import android.os.FileUtils
+import android.os.IBinder
+import android.os.RemoteException
+import android.text.format.Formatter
+import android.util.Log
+import android.view.KeyEvent
+import android.view.View
+import android.widget.CheckBox
+import android.widget.TextView
+import com.android.internal.annotations.VisibleForTesting
+import com.android.virtualization.terminal.ImageArchive.Companion.fromSdCard
+import com.android.virtualization.terminal.ImageArchive.Companion.getDefault
+import com.android.virtualization.terminal.InstallerActivity.InstallProgressListener
+import com.android.virtualization.terminal.InstallerActivity.InstallerServiceConnection
+import com.android.virtualization.terminal.MainActivity.TAG
+import com.google.android.material.progressindicator.LinearProgressIndicator
+import com.google.android.material.snackbar.Snackbar
+import java.io.IOException
+import java.lang.Exception
+import java.lang.ref.WeakReference
+
+class InstallerActivity : BaseActivity() {
+ private lateinit var waitForWifiCheckbox: CheckBox
+ private lateinit var installButton: TextView
+
+ private var service: IInstallerService? = null
+ private var installerServiceConnection: ServiceConnection? = null
+ private lateinit var installProgressListener: InstallProgressListener
+ private var installRequested = false
+ private val installCompleted = ConditionVariable()
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setResult(RESULT_CANCELED)
+
+ installProgressListener = InstallProgressListener(this)
+
+ setContentView(R.layout.activity_installer)
+ updateSizeEstimation(ESTIMATED_IMG_SIZE_BYTES)
+ measureImageSizeAndUpdateDescription()
+
+ waitForWifiCheckbox = findViewById<CheckBox>(R.id.installer_wait_for_wifi_checkbox)
+ installButton = findViewById<TextView>(R.id.installer_install_button)
+
+ installButton.setOnClickListener(View.OnClickListener { requestInstall() })
+
+ val intent = Intent(this, InstallerService::class.java)
+ installerServiceConnection = InstallerServiceConnection(this)
+ if (!bindService(intent, installerServiceConnection!!, BIND_AUTO_CREATE)) {
+ handleInternalError(Exception("Failed to connect to installer service"))
+ }
+ }
+
+ private fun updateSizeEstimation(est: Long) {
+ val desc =
+ getString(R.string.installer_desc_text_format, Formatter.formatShortFileSize(this, est))
+ runOnUiThread {
+ val view = findViewById<TextView>(R.id.installer_desc)
+ view.text = desc
+ }
+ }
+
+ private fun measureImageSizeAndUpdateDescription() {
+ Thread {
+ val est: Long =
+ try {
+ getDefault().getSize()
+ } catch (e: IOException) {
+ Log.w(TAG, "Failed to measure image size.", e)
+ return@Thread
+ }
+ updateSizeEstimation(est)
+ }
+ .start()
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ if (Build.isDebuggable() && fromSdCard().exists()) {
+ showSnackBar("Auto installing", Snackbar.LENGTH_LONG)
+ requestInstall()
+ }
+ }
+
+ public override fun onDestroy() {
+ if (installerServiceConnection != null) {
+ unbindService(installerServiceConnection!!)
+ installerServiceConnection = null
+ }
+
+ super.onDestroy()
+ }
+
+ override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BUTTON_START) {
+ requestInstall()
+ return true
+ }
+ return super.onKeyUp(keyCode, event)
+ }
+
+ @VisibleForTesting
+ fun waitForInstallCompleted(timeoutMillis: Long): Boolean {
+ return installCompleted.block(timeoutMillis)
+ }
+
+ private fun showSnackBar(message: String, length: Int) {
+ val snackBar = Snackbar.make(findViewById<View>(android.R.id.content), message, length)
+ snackBar.anchorView = waitForWifiCheckbox
+ snackBar.show()
+ }
+
+ fun handleInternalError(e: Exception) {
+ if (Build.isDebuggable()) {
+ showSnackBar(
+ e.message + ". File a bugreport to go/ferrochrome-bug",
+ Snackbar.LENGTH_INDEFINITE,
+ )
+ }
+ Log.e(TAG, "Internal error", e)
+ finishWithResult(RESULT_CANCELED)
+ }
+
+ private fun finishWithResult(resultCode: Int) {
+ if (resultCode == RESULT_OK) {
+ installCompleted.open()
+ }
+ setResult(resultCode)
+ finish()
+ }
+
+ private fun setInstallEnabled(enabled: Boolean) {
+ installButton.setEnabled(enabled)
+ waitForWifiCheckbox.setEnabled(enabled)
+ val progressBar = findViewById<LinearProgressIndicator>(R.id.installer_progress)
+ progressBar.visibility = if (enabled) View.INVISIBLE else View.VISIBLE
+
+ val resId =
+ if (enabled) R.string.installer_install_button_enabled_text
+ else R.string.installer_install_button_disabled_text
+ installButton.text = getString(resId)
+ }
+
+ @MainThread
+ private fun requestInstall() {
+ setInstallEnabled(/* enabled= */ false)
+
+ if (service != null) {
+ try {
+ service!!.requestInstall(waitForWifiCheckbox.isChecked)
+ } catch (e: RemoteException) {
+ handleInternalError(e)
+ }
+ } else {
+ Log.d(TAG, "requestInstall() is called, but not yet connected")
+ installRequested = true
+ }
+ }
+
+ @MainThread
+ fun handleInstallerServiceConnected() {
+ try {
+ service!!.setProgressListener(installProgressListener)
+ if (service!!.isInstalled()) {
+ // Finishing this activity will trigger MainActivity::onResume(),
+ // and VM will be started from there.
+ finishWithResult(RESULT_OK)
+ return
+ }
+
+ if (installRequested) {
+ requestInstall()
+ } else if (service!!.isInstalling()) {
+ setInstallEnabled(false)
+ }
+ } catch (e: RemoteException) {
+ handleInternalError(e)
+ }
+ }
+
+ @MainThread
+ fun handleInstallerServiceDisconnected() {
+ handleInternalError(Exception("InstallerService is destroyed while in use"))
+ }
+
+ @MainThread
+ private fun handleInstallError(displayText: String) {
+ showSnackBar(displayText, Snackbar.LENGTH_LONG)
+ setInstallEnabled(true)
+ }
+
+ private class InstallProgressListener(activity: InstallerActivity) :
+ IInstallProgressListener.Stub() {
+ private val activity: WeakReference<InstallerActivity> =
+ WeakReference<InstallerActivity>(activity)
+
+ override fun onCompleted() {
+ val activity = activity.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)
+ }
+
+ override fun onError(displayText: String) {
+ val context = activity.get()
+ if (context == null) {
+ // Ignore incoming connection or disconnection after activity is destroyed.
+ return
+ }
+
+ context.runOnUiThread {
+ val activity = activity.get()
+ if (activity == null) {
+ // Ignore incoming connection or disconnection after activity is
+ // destroyed.
+ return@runOnUiThread
+ }
+ activity.handleInstallError(displayText)
+ }
+ }
+ }
+
+ @MainThread
+ class InstallerServiceConnection internal constructor(activity: InstallerActivity) :
+ ServiceConnection {
+ private val activity: WeakReference<InstallerActivity> =
+ WeakReference<InstallerActivity>(activity)
+
+ override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
+ val activity = activity.get()
+ if (activity == null || activity.installerServiceConnection == null) {
+ // Ignore incoming connection or disconnection after activity is destroyed.
+ return
+ }
+ if (service == null) {
+ activity.handleInternalError(Exception("service shouldn't be null"))
+ }
+
+ activity.service = IInstallerService.Stub.asInterface(service)
+ activity.handleInstallerServiceConnected()
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ val activity = activity.get()
+ if (activity == null || activity.installerServiceConnection == null) {
+ // Ignore incoming connection or disconnection after activity is destroyed.
+ return
+ }
+
+ activity.unbindService(activity.installerServiceConnection!!)
+ activity.installerServiceConnection = null
+ activity.handleInstallerServiceDisconnected()
+ }
+ }
+
+ companion object {
+ private val ESTIMATED_IMG_SIZE_BYTES = FileUtils.parseSize("550MB")
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
deleted file mode 100644
index 66ab414..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ /dev/null
@@ -1,355 +0,0 @@
-/*
- * 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 static com.android.virtualization.terminal.MainActivity.TAG;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-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.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.ref.WeakReference;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.nio.file.Path;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class InstallerService extends Service {
- 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 boolean mHasWifi;
-
- @GuardedBy("mLock")
- private IInstallProgressListener mListener;
-
- private ExecutorService mExecutorService;
- private ConnectivityManager mConnectivityManager;
- private MyNetworkCallback mNetworkCallback;
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- Intent intent = new Intent(this, MainActivity.class);
- PendingIntent pendingIntent =
- PendingIntent.getActivity(
- this, /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE);
- mNotification =
- new Notification.Builder(this, this.getPackageName())
- .setSilent(true)
- .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(
- new TerminalThreadFactory(getApplicationContext()));
-
- 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
- @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();
- }
- mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
- }
-
- private void requestInstall(boolean isWifiOnly) {
- synchronized (mLock) {
- if (mIsInstalling) {
- Log.i(TAG, "already installing..");
- return;
- } else {
- Log.i(TAG, "installing..");
- mIsInstalling = true;
- }
- }
-
- // 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);
-
- mExecutorService.execute(
- () -> {
- boolean success = downloadFromSdcard() || downloadFromUrl(isWifiOnly);
- stopForeground(STOP_FOREGROUND_REMOVE);
-
- synchronized (mLock) {
- mIsInstalling = false;
- }
- if (success) {
- notifyCompleted();
- }
- });
- }
-
- private boolean downloadFromSdcard() {
- ImageArchive archive = ImageArchive.fromSdCard();
-
- // Installing from sdcard is preferred, but only supported only in debuggable build.
- if (Build.isDebuggable() && archive.exists()) {
- Log.i(TAG, "trying to install /sdcard/linux/images.tar.gz");
-
- Path dest = InstalledImage.getDefault(this).getInstallDir();
- try {
- archive.installTo(dest, null);
- Log.i(TAG, "image is installed from /sdcard/linux/images.tar.gz");
- return true;
- } catch (IOException e) {
- Log.i(TAG, "Failed to install /sdcard/linux/images.tar.gz", e);
- }
- } else {
- Log.i(TAG, "Non-debuggable build doesn't support installation from /sdcard/linux");
- }
- return false;
- }
-
- private boolean checkForWifiOnly(boolean isWifiOnly) {
- if (!isWifiOnly) {
- return true;
- }
- synchronized (mLock) {
- return mHasWifi;
- }
- }
-
- // TODO(b/374015561): Support pause/resume download
- private boolean downloadFromUrl(boolean isWifiOnly) {
- 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;
- }
-
- Path dest = InstalledImage.getDefault(this).getInstallDir();
- try {
- ImageArchive.fromInternet()
- .installTo(
- dest,
- is -> {
- WifiCheckInputStream filter = new WifiCheckInputStream(is);
- filter.setWifiOnly(isWifiOnly);
- return filter;
- });
- } 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: " + e.getMessage(), e);
- notifyError(getString(R.string.installer_error_network));
- return false;
- } catch (IOException e) {
- Log.e(TAG, "Installation failed", e);
- 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) {
- 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(boolean isWifiOnly) {
- InstallerService service = ensureServiceConnected();
- synchronized (service.mLock) {
- service.requestInstall(isWifiOnly);
- }
- }
-
- @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 && InstalledImage.getDefault(service).isInstalled();
- }
- }
- }
-
- private final class WifiCheckInputStream extends InputStream {
- private static final int READ_BYTES = 1024;
-
- private final InputStream mInputStream;
- private boolean mIsWifiOnly;
-
- public WifiCheckInputStream(InputStream is) {
- super();
- mInputStream = is;
- }
-
- public void setWifiOnly(boolean isWifiOnly) {
- 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/java/com/android/virtualization/terminal/InstallerService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
new file mode 100644
index 0000000..9bd6a13
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.kt
@@ -0,0 +1,333 @@
+/*
+ * 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.PendingIntent
+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.util.Log
+import com.android.internal.annotations.GuardedBy
+import com.android.virtualization.terminal.ImageArchive.Companion.fromInternet
+import com.android.virtualization.terminal.ImageArchive.Companion.fromSdCard
+import com.android.virtualization.terminal.InstalledImage.Companion.getDefault
+import com.android.virtualization.terminal.InstallerService.InstallerServiceImpl
+import com.android.virtualization.terminal.InstallerService.WifiCheckInputStream.NoWifiException
+import com.android.virtualization.terminal.MainActivity.TAG
+import java.io.IOException
+import java.io.InputStream
+import java.lang.Exception
+import java.lang.RuntimeException
+import java.lang.ref.WeakReference
+import java.net.SocketException
+import java.net.UnknownHostException
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import kotlin.math.min
+
+class InstallerService : Service() {
+ private val lock = Any()
+
+ private lateinit var notification: Notification
+
+ @GuardedBy("lock") private var isInstalling = false
+
+ @GuardedBy("lock") private var hasWifi = false
+
+ @GuardedBy("lock") private var listener: IInstallProgressListener? = null
+
+ private lateinit var executorService: ExecutorService
+ private lateinit var connectivityManager: ConnectivityManager
+ private lateinit var networkCallback: MyNetworkCallback
+
+ override fun onCreate() {
+ super.onCreate()
+
+ val intent = Intent(this, MainActivity::class.java)
+ val pendingIntent =
+ PendingIntent.getActivity(
+ this,
+ /* requestCode= */ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE,
+ )
+ notification =
+ Notification.Builder(this, this.packageName)
+ .setSilent(true)
+ .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()
+
+ executorService =
+ Executors.newSingleThreadExecutor(TerminalThreadFactory(applicationContext))
+
+ connectivityManager = getSystemService<ConnectivityManager>(ConnectivityManager::class.java)
+ val defaultNetwork = connectivityManager.boundNetworkForProcess
+ if (defaultNetwork != null) {
+ val capability = connectivityManager.getNetworkCapabilities(defaultNetwork)
+ if (capability != null) {
+ hasWifi = capability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ }
+ }
+ networkCallback = MyNetworkCallback()
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return InstallerServiceImpl(this)
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+
+ Log.d(TAG, "Starting service ...")
+
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+
+ Log.d(TAG, "Service is destroyed")
+ executorService.shutdown()
+ connectivityManager.unregisterNetworkCallback(networkCallback)
+ }
+
+ private fun requestInstall(isWifiOnly: Boolean) {
+ synchronized(lock) {
+ if (isInstalling) {
+ Log.i(TAG, "already installing..")
+ return
+ } else {
+ Log.i(TAG, "installing..")
+ isInstalling = true
+ }
+ }
+
+ // Make service to be long running, even after unbind() when InstallerActivity is destroyed
+ // The service will still be destroyed if task is remove.
+ startService(Intent(this, InstallerService::class.java))
+ startForeground(
+ NOTIFICATION_ID,
+ notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
+ )
+
+ executorService.execute(
+ Runnable {
+ val success = downloadFromSdcard() || downloadFromUrl(isWifiOnly)
+ stopForeground(STOP_FOREGROUND_REMOVE)
+
+ synchronized(lock) { isInstalling = false }
+ if (success) {
+ notifyCompleted()
+ }
+ }
+ )
+ }
+
+ private fun downloadFromSdcard(): Boolean {
+ val archive = fromSdCard()
+
+ // Installing from sdcard is preferred, but only supported only in debuggable build.
+ if (Build.isDebuggable() && archive.exists()) {
+ Log.i(TAG, "trying to install /sdcard/linux/images.tar.gz")
+
+ val dest = getDefault(this).installDir
+ try {
+ archive.installTo(dest, null)
+ Log.i(TAG, "image is installed from /sdcard/linux/images.tar.gz")
+ return true
+ } catch (e: IOException) {
+ Log.i(TAG, "Failed to install /sdcard/linux/images.tar.gz", e)
+ }
+ } else {
+ Log.i(TAG, "Non-debuggable build doesn't support installation from /sdcard/linux")
+ }
+ return false
+ }
+
+ private fun checkForWifiOnly(isWifiOnly: Boolean): Boolean {
+ if (!isWifiOnly) {
+ return true
+ }
+ synchronized(lock) {
+ return hasWifi
+ }
+ }
+
+ // TODO(b/374015561): Support pause/resume download
+ private fun downloadFromUrl(isWifiOnly: Boolean): Boolean {
+ 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
+ }
+
+ val dest = getDefault(this).installDir
+ try {
+ fromInternet().installTo(dest) {
+ val filter = WifiCheckInputStream(it)
+ filter.setWifiOnly(isWifiOnly)
+ filter
+ }
+ } catch (e: NoWifiException) {
+ Log.e(TAG, "Install failed because of Wi-Fi is gone")
+ notifyError(getString(R.string.installer_error_no_wifi))
+ return false
+ } catch (e: UnknownHostException) {
+ // Log.e() doesn't print stack trace for UnknownHostException
+ Log.e(TAG, "Install failed: " + e.message, e)
+ notifyError(getString(R.string.installer_error_network))
+ return false
+ } catch (e: SocketException) {
+ Log.e(TAG, "Install failed: " + e.message, e)
+ notifyError(getString(R.string.installer_error_network))
+ return false
+ } catch (e: IOException) {
+ Log.e(TAG, "Installation failed", e)
+ notifyError(getString(R.string.installer_error_unknown))
+ return false
+ }
+ return true
+ }
+
+ private fun notifyError(displayText: String?) {
+ var listener: IInstallProgressListener
+ synchronized(lock) { listener = this@InstallerService.listener!! }
+
+ try {
+ listener.onError(displayText)
+ } catch (e: Exception) {
+ // ignore. Activity may not exist.
+ }
+ }
+
+ private fun notifyCompleted() {
+ var listener: IInstallProgressListener
+ synchronized(lock) { listener = this@InstallerService.listener!! }
+
+ try {
+ listener.onCompleted()
+ } catch (e: Exception) {
+ // ignore. Activity may not exist.
+ }
+ }
+
+ private class InstallerServiceImpl(service: InstallerService?) : IInstallerService.Stub() {
+ // Holds weak reference to avoid Context leak
+ private val mService: WeakReference<InstallerService> =
+ WeakReference<InstallerService>(service)
+
+ @Throws(RuntimeException::class)
+ fun ensureServiceConnected(): InstallerService {
+ val service: InstallerService? = mService.get()
+ if (service == null) {
+ throw RuntimeException(
+ "Internal error: Installer service is being accessed after destroyed"
+ )
+ }
+ return service
+ }
+
+ override fun requestInstall(isWifiOnly: Boolean) {
+ val service = ensureServiceConnected()
+ synchronized(service.lock) { service.requestInstall(isWifiOnly) }
+ }
+
+ override fun setProgressListener(listener: IInstallProgressListener) {
+ val service = ensureServiceConnected()
+ synchronized(service.lock) { service.listener = listener }
+ }
+
+ override fun isInstalling(): Boolean {
+ val service = ensureServiceConnected()
+ synchronized(service.lock) {
+ return service.isInstalling
+ }
+ }
+
+ override fun isInstalled(): Boolean {
+ val service = ensureServiceConnected()
+ synchronized(service.lock) {
+ return !service.isInstalling && getDefault(service).isInstalled()
+ }
+ }
+ }
+
+ private inner class WifiCheckInputStream(private val inputStream: InputStream) : InputStream() {
+ private var isWifiOnly = false
+
+ fun setWifiOnly(isWifiOnly: Boolean) {
+ this@WifiCheckInputStream.isWifiOnly = isWifiOnly
+ }
+
+ @Throws(IOException::class)
+ override fun read(buf: ByteArray?, offset: Int, numToRead: Int): Int {
+ var remaining = numToRead
+ var totalRead = 0
+ while (remaining > 0) {
+ if (!checkForWifiOnly(isWifiOnly)) {
+ throw NoWifiException()
+ }
+ val read =
+ this@WifiCheckInputStream.inputStream.read(
+ buf,
+ offset + totalRead,
+ min(READ_BYTES, remaining),
+ )
+ if (read <= 0) {
+ break
+ }
+ totalRead += read
+ remaining -= read
+ }
+ return totalRead
+ }
+
+ @Throws(IOException::class)
+ override fun read(): Int {
+ if (!checkForWifiOnly(isWifiOnly)) {
+ throw NoWifiException()
+ }
+ return this@WifiCheckInputStream.inputStream.read()
+ }
+
+ inner class NoWifiException : SocketException()
+ }
+
+ private inner class MyNetworkCallback : ConnectivityManager.NetworkCallback() {
+ override fun onCapabilitiesChanged(network: Network, capability: NetworkCapabilities) {
+ synchronized(lock) {
+ hasWifi = capability.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
+ }
+ }
+ }
+
+ companion object {
+ private const val NOTIFICATION_ID = 1313 // any unique number among notifications
+ private const val READ_BYTES = 1024
+ }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
index 7a07dfe..30729c4 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.kt
@@ -76,7 +76,11 @@
val title = getString(R.string.settings_port_forwarding_notification_title)
val content =
- context.getString(R.string.settings_port_forwarding_notification_content, port)
+ context.getString(
+ R.string.settings_port_forwarding_notification_content,
+ port,
+ portsStateManager.getActivePortInfo(port)?.comm,
+ )
val acceptText = getString(R.string.settings_port_forwarding_notification_accept)
val denyText = getString(R.string.settings_port_forwarding_notification_deny)
val icon = Icon.createWithResource(context, R.drawable.ic_launcher_foreground)
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt
index 317ca8e..c5335ac 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.kt
@@ -18,6 +18,7 @@
import android.content.Context
import android.content.SharedPreferences
import com.android.internal.annotations.GuardedBy
+import com.android.virtualization.terminal.proto.ActivePort
import java.util.HashSet
/**
@@ -27,7 +28,7 @@
class PortsStateManager private constructor(private val sharedPref: SharedPreferences) {
private val lock = Any()
- @GuardedBy("lock") private val activePorts: MutableSet<Int> = hashSetOf()
+ @GuardedBy("lock") private val activePorts: MutableMap<Int, ActivePort> = hashMapOf()
@GuardedBy("lock")
private val enabledPorts: MutableSet<Int> =
@@ -44,7 +45,13 @@
fun getActivePorts(): Set<Int> {
synchronized(lock) {
- return HashSet<Int>(activePorts)
+ return HashSet<Int>(activePorts.keys)
+ }
+ }
+
+ fun getActivePortInfo(port: Int): ActivePort? {
+ synchronized(lock) {
+ return activePorts[port]
}
}
@@ -54,11 +61,11 @@
}
}
- fun updateActivePorts(ports: Set<Int>) {
+ fun updateActivePorts(ports: List<ActivePort>) {
val oldPorts = getActivePorts()
synchronized(lock) {
activePorts.clear()
- activePorts.addAll(ports)
+ activePorts.putAll(ports.associateBy { it.port })
}
notifyPortsStateUpdated(oldPorts, getActivePorts())
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActiveAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActiveAdapter.kt
index 1bfa390..7076084 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActiveAdapter.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActiveAdapter.kt
@@ -15,6 +15,7 @@
*/
package com.android.virtualization.terminal
+import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -22,12 +23,14 @@
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.materialswitch.MaterialSwitch
-class SettingsPortForwardingActiveAdapter(private val mPortsStateManager: PortsStateManager) :
- SettingsPortForwardingBaseAdapter<SettingsPortForwardingActiveAdapter.ViewHolder>() {
+class SettingsPortForwardingActiveAdapter(
+ private val portsStateManager: PortsStateManager,
+ private val context: Context,
+) : SettingsPortForwardingBaseAdapter<SettingsPortForwardingActiveAdapter.ViewHolder>() {
override fun getItems(): ArrayList<SettingsPortForwardingItem> {
- val enabledPorts = mPortsStateManager.getEnabledPorts()
- return mPortsStateManager
+ val enabledPorts = portsStateManager.getEnabledPorts()
+ return portsStateManager
.getActivePorts()
.map { SettingsPortForwardingItem(it, enabledPorts.contains(it)) }
.toCollection(ArrayList())
@@ -47,13 +50,18 @@
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
- val port = mItems[position].port
- viewHolder.port.text = port.toString()
+ val port = items[position].port
+ viewHolder.port.text =
+ context.getString(
+ R.string.settings_port_forwarding_active_ports_content,
+ port,
+ portsStateManager.getActivePortInfo(port)?.comm,
+ )
viewHolder.enabledSwitch.contentDescription = viewHolder.port.text
viewHolder.enabledSwitch.setOnCheckedChangeListener(null)
- viewHolder.enabledSwitch.isChecked = mItems[position].enabled
+ viewHolder.enabledSwitch.isChecked = items[position].enabled
viewHolder.enabledSwitch.setOnCheckedChangeListener { _, isChecked ->
- mPortsStateManager.updateEnabledPort(port, isChecked)
+ portsStateManager.updateEnabledPort(port, isChecked)
}
}
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
index b4ca77b..db3926d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -31,30 +31,30 @@
private const val PORT_RANGE_MAX: Int = 65535
class SettingsPortForwardingActivity : AppCompatActivity() {
- private lateinit var mPortsStateManager: PortsStateManager
- private lateinit var mPortsStateListener: Listener
- private lateinit var mActivePortsAdapter: SettingsPortForwardingActiveAdapter
- private lateinit var mInactivePortsAdapter: SettingsPortForwardingInactiveAdapter
+ private lateinit var portsStateManager: PortsStateManager
+ private lateinit var portsStateListener: Listener
+ private lateinit var activePortsAdapter: SettingsPortForwardingActiveAdapter
+ private lateinit var inactivePortsAdapter: SettingsPortForwardingInactiveAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_port_forwarding)
- mPortsStateManager = PortsStateManager.getInstance(this)
+ portsStateManager = PortsStateManager.getInstance(this)
- mActivePortsAdapter = SettingsPortForwardingActiveAdapter(mPortsStateManager)
+ activePortsAdapter = SettingsPortForwardingActiveAdapter(portsStateManager, this)
val activeRecyclerView: RecyclerView =
findViewById(R.id.settings_port_forwarding_active_recycler_view)
activeRecyclerView.layoutManager = LinearLayoutManager(this)
- activeRecyclerView.adapter = mActivePortsAdapter
+ activeRecyclerView.adapter = activePortsAdapter
- mInactivePortsAdapter = SettingsPortForwardingInactiveAdapter(mPortsStateManager, this)
+ inactivePortsAdapter = SettingsPortForwardingInactiveAdapter(portsStateManager, this)
val inactiveRecyclerView: RecyclerView =
findViewById(R.id.settings_port_forwarding_inactive_recycler_view)
inactiveRecyclerView.layoutManager = LinearLayoutManager(this)
- inactiveRecyclerView.adapter = mInactivePortsAdapter
+ inactiveRecyclerView.adapter = inactivePortsAdapter
- mPortsStateListener = Listener()
+ portsStateListener = Listener()
val addButton = findViewById<ImageButton>(R.id.settings_port_forwarding_inactive_add_button)
addButton.setOnClickListener {
@@ -71,7 +71,7 @@
R.id.settings_port_forwarding_inactive_add_dialog_text
)!!
val port = editText.text.toString().toInt()
- mPortsStateManager.updateEnabledPort(port, true)
+ portsStateManager.updateEnabledPort(port, true)
}
.setNegativeButton(R.string.settings_port_forwarding_dialog_cancel, null)
.create()
@@ -121,8 +121,8 @@
)
positiveButton.setEnabled(false)
} else if (
- mPortsStateManager.getActivePorts().contains(port) ||
- mPortsStateManager.getEnabledPorts().contains(port)
+ portsStateManager.getActivePorts().contains(port) ||
+ portsStateManager.getEnabledPorts().contains(port)
) {
editText.setError(
getString(
@@ -141,19 +141,19 @@
private fun refreshAdapters() {
runOnUiThread {
- mActivePortsAdapter.refreshItems()
- mInactivePortsAdapter.refreshItems()
+ activePortsAdapter.refreshItems()
+ inactivePortsAdapter.refreshItems()
}
}
override fun onResume() {
super.onResume()
- mPortsStateManager.registerListener(mPortsStateListener)
+ portsStateManager.registerListener(portsStateListener)
refreshAdapters()
}
override fun onPause() {
- mPortsStateManager.unregisterListener(mPortsStateListener)
+ portsStateManager.unregisterListener(portsStateListener)
super.onPause()
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingBaseAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingBaseAdapter.kt
index 4595372..5b8d022 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingBaseAdapter.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingBaseAdapter.kt
@@ -21,10 +21,10 @@
abstract class SettingsPortForwardingBaseAdapter<T : RecyclerView.ViewHolder>() :
RecyclerView.Adapter<T>() {
- var mItems: SortedList<SettingsPortForwardingItem>
+ var items: SortedList<SettingsPortForwardingItem>
init {
- mItems =
+ items =
SortedList(
SettingsPortForwardingItem::class.java,
object : SortedListAdapterCallback<SettingsPortForwardingItem>(this) {
@@ -52,11 +52,11 @@
)
}
- override fun getItemCount() = mItems.size()
+ override fun getItemCount() = items.size()
abstract fun getItems(): ArrayList<SettingsPortForwardingItem>
fun refreshItems() {
- mItems.replaceAll(getItems())
+ items.replaceAll(getItems())
}
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingInactiveAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingInactiveAdapter.kt
index d572129..e1fe468 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingInactiveAdapter.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingInactiveAdapter.kt
@@ -24,14 +24,14 @@
import androidx.recyclerview.widget.RecyclerView
class SettingsPortForwardingInactiveAdapter(
- private val mPortsStateManager: PortsStateManager,
- private val mContext: Context,
+ private val portsStateManager: PortsStateManager,
+ private val context: Context,
) : SettingsPortForwardingBaseAdapter<SettingsPortForwardingInactiveAdapter.ViewHolder>() {
override fun getItems(): ArrayList<SettingsPortForwardingItem> {
- return mPortsStateManager
+ return portsStateManager
.getEnabledPorts()
- .subtract(mPortsStateManager.getActivePorts())
+ .subtract(portsStateManager.getActivePorts())
.map { SettingsPortForwardingItem(it, true) }
.toCollection(ArrayList())
}
@@ -50,15 +50,15 @@
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
- val port = mItems[position].port
+ val port = items[position].port
viewHolder.port.text = port.toString()
viewHolder.closeButton.contentDescription =
- mContext.getString(
+ context.getString(
R.string.settings_port_forwarding_other_enabled_port_close_button,
port,
)
viewHolder.closeButton.setOnClickListener { _ ->
- mPortsStateManager.updateEnabledPort(port, false)
+ portsStateManager.updateEnabledPort(port, false)
}
}
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
index 18a39fa..0f990c5 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/TerminalView.kt
@@ -94,7 +94,7 @@
// AccessibilityDelegate to the parent view where the events are sent to. And to guarantee that
// the parent view exists, wait until the WebView is attached to the window by when the parent
// must exist.
- private val mA11yEventFilter: AccessibilityDelegate =
+ private val a11yEventFilter: AccessibilityDelegate =
object : AccessibilityDelegate() {
override fun onRequestSendAccessibilityEvent(
host: ViewGroup,
@@ -122,11 +122,11 @@
super.onAttachedToWindow()
if (a11yManager.isEnabled) {
val parent = getParent() as View
- parent.setAccessibilityDelegate(mA11yEventFilter)
+ parent.setAccessibilityDelegate(a11yEventFilter)
}
}
- private val mA11yNodeProvider: AccessibilityNodeProvider =
+ private val a11yNodeProvider: AccessibilityNodeProvider =
object : AccessibilityNodeProvider() {
/** Returns the original NodeProvider that WebView implements. */
private fun getParent(): AccessibilityNodeProvider? {
@@ -262,7 +262,7 @@
override fun getAccessibilityNodeProvider(): AccessibilityNodeProvider? {
val p = super.getAccessibilityNodeProvider()
if (p != null && a11yManager.isEnabled) {
- return mA11yNodeProvider
+ return a11yNodeProvider
}
return p
}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index 2796b86..5271e8b 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -60,12 +60,12 @@
class VmLauncherService : Service() {
// TODO: using lateinit for some fields to avoid null
- private var mExecutorService: ExecutorService? = null
- private var mVirtualMachine: VirtualMachine? = null
- private var mResultReceiver: ResultReceiver? = null
- private var mServer: Server? = null
- private var mDebianService: DebianServiceImpl? = null
- private var mPortNotifier: PortNotifier? = null
+ private var executorService: ExecutorService? = null
+ private var virtualMachine: VirtualMachine? = null
+ private var resultReceiver: ResultReceiver? = null
+ private var server: Server? = null
+ private var debianService: DebianServiceImpl? = null
+ private var portNotifier: PortNotifier? = null
interface VmLauncherServiceCallback {
fun onVmStart()
@@ -81,7 +81,7 @@
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent.action == ACTION_STOP_VM_LAUNCHER_SERVICE) {
- if (mDebianService != null && mDebianService!!.shutdownDebian()) {
+ if (debianService != null && debianService!!.shutdownDebian()) {
// During shutdown, change the notification content to indicate that it's closing
val notification = createNotificationForTerminalClose()
getSystemService<NotificationManager?>(NotificationManager::class.java)
@@ -92,11 +92,11 @@
}
return START_NOT_STICKY
}
- if (mVirtualMachine != null) {
+ if (virtualMachine != null) {
Log.d(TAG, "VM instance is already started")
return START_NOT_STICKY
}
- mExecutorService = Executors.newCachedThreadPool(TerminalThreadFactory(applicationContext))
+ executorService = Executors.newCachedThreadPool(TerminalThreadFactory(applicationContext))
val image = InstalledImage.getDefault(this)
val json = ConfigJson.from(this, image.configPath)
@@ -117,28 +117,28 @@
Trace.endSection()
Trace.beginAsyncSection("debianBoot", 0)
- mVirtualMachine = runner.vm
- mResultReceiver =
+ virtualMachine = runner.vm
+ resultReceiver =
intent.getParcelableExtra<ResultReceiver?>(
Intent.EXTRA_RESULT_RECEIVER,
ResultReceiver::class.java,
)
runner.exitStatus.thenAcceptAsync { success: Boolean ->
- mResultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
+ resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
stopSelf()
}
- val logPath = getFileStreamPath(mVirtualMachine!!.name + ".log").toPath()
- Logger.setup(mVirtualMachine!!, logPath, mExecutorService!!)
+ val logPath = getFileStreamPath(virtualMachine!!.name + ".log").toPath()
+ Logger.setup(virtualMachine!!, logPath, executorService!!)
val notification =
intent.getParcelableExtra<Notification?>(EXTRA_NOTIFICATION, Notification::class.java)
startForeground(this.hashCode(), notification)
- mResultReceiver!!.send(RESULT_START, null)
+ resultReceiver!!.send(RESULT_START, null)
- mPortNotifier = PortNotifier(this)
+ portNotifier = PortNotifier(this)
// TODO: dedup this part
val nsdManager = getSystemService<NsdManager?>(NsdManager::class.java)
@@ -147,7 +147,7 @@
info.serviceName = "ttyd"
nsdManager.registerServiceInfoCallback(
info,
- mExecutorService!!,
+ executorService!!,
object : NsdManager.ServiceInfoCallback {
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {}
@@ -245,11 +245,11 @@
try {
// TODO(b/372666638): gRPC for java doesn't support vsock for now.
val port = 0
- mDebianService = DebianServiceImpl(this)
- mServer =
+ debianService = DebianServiceImpl(this)
+ server =
OkHttpServerBuilder.forPort(port, InsecureServerCredentials.create())
.intercept(interceptor)
- .addService(mDebianService)
+ .addService(debianService)
.build()
.start()
} catch (e: IOException) {
@@ -257,13 +257,13 @@
return
}
- mExecutorService!!.execute(
+ executorService!!.execute(
Runnable {
// TODO(b/373533555): we can use mDNS for that.
val debianServicePortFile = File(filesDir, "debian_service_port")
try {
FileOutputStream(debianServicePortFile).use { writer ->
- writer.write(mServer!!.port.toString().toByteArray())
+ writer.write(server!!.port.toString().toByteArray())
}
} catch (e: IOException) {
Log.d(TAG, "cannot write grpc port number", e)
@@ -273,28 +273,28 @@
}
override fun onDestroy() {
- mPortNotifier?.stop()
+ portNotifier?.stop()
getSystemService<NotificationManager?>(NotificationManager::class.java).cancelAll()
stopDebianServer()
- if (mVirtualMachine != null) {
- if (mVirtualMachine!!.getStatus() == VirtualMachine.STATUS_RUNNING) {
+ if (virtualMachine != null) {
+ if (virtualMachine!!.getStatus() == VirtualMachine.STATUS_RUNNING) {
try {
- mVirtualMachine!!.stop()
+ virtualMachine!!.stop()
stopForeground(STOP_FOREGROUND_REMOVE)
} catch (e: VirtualMachineException) {
Log.e(TAG, "failed to stop a VM instance", e)
}
}
- mExecutorService?.shutdownNow()
- mExecutorService = null
- mVirtualMachine = null
+ executorService?.shutdownNow()
+ executorService = null
+ virtualMachine = null
}
super.onDestroy()
}
private fun stopDebianServer() {
- mDebianService?.killForwarderHost()
- mServer?.shutdown()
+ debianService?.killForwarderHost()
+ server?.shutdown()
}
companion object {
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 40c354e..d3440d3 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -87,6 +87,8 @@
<string name="settings_port_forwarding_sub_title">Allow/deny listening ports</string>
<!-- Title for active ports setting in port forwarding [CHAR LIMIT=none] -->
<string name="settings_port_forwarding_active_ports_title">Listening ports</string>
+ <!-- Text content for active ports setting in port forwarding [CHAR LIMIT=none] -->
+ <string name="settings_port_forwarding_active_ports_content"><xliff:g id="port_number" example="8000">%1$d</xliff:g> (<xliff:g id="process_name" example="undefined">%2$s</xliff:g>)</string>
<!-- Title for other enabled ports setting in port forwarding [CHAR LIMIT=none] -->
<string name="settings_port_forwarding_other_enabled_ports_title">Saved allowed ports</string>
<!-- Description of add button for other enabled ports. Used for talkback. [CHAR LIMIT=16] -->
@@ -112,7 +114,7 @@
<!-- Notification title for a new active port [CHAR LIMIT=none] -->
<string name="settings_port_forwarding_notification_title">Terminal is requesting to open a new port</string>
<!-- Notification content for a new active port [CHAR LIMIT=none] -->
- <string name="settings_port_forwarding_notification_content">Port requested: <xliff:g id="port_number" example="8080">%d</xliff:g></string>
+ <string name="settings_port_forwarding_notification_content">Port requested: <xliff:g id="port_number" example="8000">%1$d</xliff:g> (<xliff:g id="process_name" example="undefined">%2$s</xliff:g>)</string>
<!-- Notification action accept [CHAR LIMIT=16] -->
<string name="settings_port_forwarding_notification_accept">Accept</string>
<!-- Notification action deny [CHAR LIMIT=16] -->
diff --git a/guest/forwarder_guest_launcher/src/main.rs b/guest/forwarder_guest_launcher/src/main.rs
index bed8965..963a531 100644
--- a/guest/forwarder_guest_launcher/src/main.rs
+++ b/guest/forwarder_guest_launcher/src/main.rs
@@ -22,7 +22,7 @@
use futures::stream::StreamExt;
use log::{debug, error};
use serde::Deserialize;
-use std::collections::HashSet;
+use std::collections::HashMap;
use std::process::Stdio;
use tokio::io::BufReader;
use tokio::process::Command;
@@ -46,6 +46,8 @@
ip: i8,
lport: i32,
rport: i32,
+ #[serde(alias = "C-COMM")]
+ c_comm: String,
newstate: String,
}
@@ -88,12 +90,12 @@
}
async fn send_active_ports_report(
- listening_ports: HashSet<i32>,
+ listening_ports: HashMap<i32, ActivePort>,
client: &mut DebianServiceClient<Channel>,
) -> Result<(), Box<dyn std::error::Error>> {
let res = client
.report_vm_active_ports(Request::new(ReportVmActivePortsRequest {
- ports: listening_ports.into_iter().map(|port| ActivePort { port }).collect(),
+ ports: listening_ports.values().cloned().collect(),
}))
.await?
.into_inner();
@@ -126,12 +128,16 @@
// TODO(b/340126051): Consider using NETLINK_SOCK_DIAG for the optimization.
let listeners = listeners::get_all()?;
// TODO(b/340126051): Support distinguished port forwarding for ipv6 as well.
- let mut listening_ports: HashSet<_> = listeners
+ let mut listening_ports: HashMap<_, _> = listeners
.iter()
- .map(|x| x.socket)
- .filter(|x| x.is_ipv4())
- .map(|x| x.port().into())
- .filter(|x| is_forwardable_port(*x))
+ .filter(|x| x.socket.is_ipv4())
+ .map(|x| {
+ (
+ x.socket.port().into(),
+ ActivePort { port: x.socket.port().into(), comm: x.process.name.to_string() },
+ )
+ })
+ .filter(|(x, _)| is_forwardable_port(*x))
.collect();
send_active_ports_report(listening_ports.clone(), &mut client).await?;
@@ -149,7 +155,7 @@
}
match row.newstate.as_str() {
TCPSTATES_STATE_LISTEN => {
- listening_ports.insert(row.lport);
+ listening_ports.insert(row.lport, ActivePort { port: row.lport, comm: row.c_comm });
}
TCPSTATES_STATE_CLOSE => {
listening_ports.remove(&row.lport);
diff --git a/libs/debian_service/proto/DebianService.proto b/libs/debian_service/proto/DebianService.proto
index 7ab0af7..43955fa 100644
--- a/libs/debian_service/proto/DebianService.proto
+++ b/libs/debian_service/proto/DebianService.proto
@@ -31,9 +31,9 @@
int32 cid = 1;
}
-// TODO(b/382998551): Add more information about the port.
message ActivePort {
int32 port = 1;
+ string comm = 2;
}
message ReportVmActivePortsRequest {