diff --git a/samples/VirtualDeviceManager/client/AndroidManifest.xml b/samples/VirtualDeviceManager/client/AndroidManifest.xml index 7462387f4..f3624c770 100644 --- a/samples/VirtualDeviceManager/client/AndroidManifest.xml +++ b/samples/VirtualDeviceManager/client/AndroidManifest.xml @@ -18,6 +18,7 @@ android:theme="@style/Theme.AppCompat.Light.NoActionBar"> diff --git a/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectionManager.java b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectionManager.java index 4b1ff7207..4d3836c62 100644 --- a/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectionManager.java +++ b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/ConnectionManager.java @@ -39,6 +39,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.util.Log; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import dagger.hilt.android.qualifiers.ApplicationContext; @@ -48,7 +49,6 @@ import java.net.Inet6Address; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -68,6 +68,9 @@ public class ConnectionManager { @ApplicationContext private final Context mContext; private final ConnectivityManager mConnectivityManager; private final Handler mBackgroundHandler; + private final Object mSessionLock = new Object(); + + @GuardedBy("mSessionLock") private DiscoverySession mDiscoverySession; /** Simple data structure to allow clients to query the current status. */ @@ -76,6 +79,7 @@ public class ConnectionManager { public boolean connected = false; } + @GuardedBy("mSessionLock") private final ConnectionStatus mConnectionStatus = new ConnectionStatus(); /** Simple callback to notify connection and disconnection events. */ @@ -93,8 +97,8 @@ public class ConnectionManager { default void onError(String message) {} } - private final List mConnectionCallbacks = - Collections.synchronizedList(new ArrayList<>()); + @GuardedBy("mConnectionCallbacks") + private final List mConnectionCallbacks = new ArrayList<>(); private final RemoteIo.StreamClosedCallback mStreamClosedCallback = this::disconnect; @@ -115,22 +119,28 @@ public class ConnectionManager { /** Registers a listener for connection events. */ public void addConnectionCallback(ConnectionCallback callback) { - mConnectionCallbacks.add(callback); + synchronized (mConnectionCallbacks) { + mConnectionCallbacks.add(callback); + } } /** Registers a listener for connection events. */ public void removeConnectionCallback(ConnectionCallback callback) { - mConnectionCallbacks.remove(callback); + synchronized (mConnectionCallbacks) { + mConnectionCallbacks.remove(callback); + } } /** Returns the current connection status. */ public ConnectionStatus getConnectionStatus() { - return mConnectionStatus; + synchronized (mSessionLock) { + return mConnectionStatus; + } } /** Publish a local service so remote devices can discover this device. */ public void startHostSession() { - if (mConnectionStatus.connected) { + if (isConnected()) { return; } var unused = createSession().thenAccept(wifiAwareSession -> wifiAwareSession.publish( @@ -141,7 +151,7 @@ public class ConnectionManager { /** Looks for published services from remote devices and subscribes to them. */ public void startClientSession() { - if (mConnectionStatus.connected) { + if (isConnected()) { return; } var unused = createSession().thenAccept(wifiAwareSession -> wifiAwareSession.subscribe( @@ -150,6 +160,12 @@ public class ConnectionManager { mBackgroundHandler)); } + private boolean isConnected() { + synchronized (mSessionLock) { + return mConnectionStatus.connected; + } + } + private CompletableFuture createSession() { CompletableFuture wifiAwareSessionFuture = new CompletableFuture<>(); WifiAwareManager wifiAwareManager = mContext.getSystemService(WifiAwareManager.class); @@ -184,53 +200,73 @@ public class ConnectionManager { /** Explicitly terminate any existing connection. */ public void disconnect() { - if (mDiscoverySession != null) { - mDiscoverySession.close(); - mDiscoverySession = null; - } - mConnectionStatus.remoteDeviceName = null; - mConnectionStatus.connected = false; - for (ConnectionCallback callback : mConnectionCallbacks) { - callback.onDisconnected(); + synchronized (mSessionLock) { + if (mDiscoverySession != null) { + mDiscoverySession.close(); + mDiscoverySession = null; + } + mConnectionStatus.remoteDeviceName = null; + mConnectionStatus.connected = false; + synchronized (mConnectionCallbacks) { + for (ConnectionCallback callback : mConnectionCallbacks) { + callback.onDisconnected(); + } + } } } private void onSocketAvailable(Socket socket) throws IOException { mRemoteIo.initialize(socket.getInputStream(), mStreamClosedCallback); mRemoteIo.initialize(socket.getOutputStream(), mStreamClosedCallback); - mConnectionStatus.connected = true; - for (ConnectionCallback callback : mConnectionCallbacks) { - callback.onConnected(mConnectionStatus.remoteDeviceName); + synchronized (mSessionLock) { + mConnectionStatus.connected = true; + synchronized (mConnectionCallbacks) { + for (ConnectionCallback callback : mConnectionCallbacks) { + callback.onConnected(mConnectionStatus.remoteDeviceName); + } + } } } private void onError(String message) { Log.e(TAG, "Error: " + message); - for (ConnectionCallback callback : mConnectionCallbacks) { - callback.onError(message); + synchronized (mConnectionCallbacks) { + for (ConnectionCallback callback : mConnectionCallbacks) { + callback.onError(message); + } } } private class VdmDiscoverySessionCallback extends DiscoverySessionCallback { + + @GuardedBy("mSessionLock") private NetworkCallback mNetworkCallback; @Override public void onSessionTerminated() { disconnect(); - if (mNetworkCallback != null) { - mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + synchronized (mSessionLock) { + if (mNetworkCallback != null) { + mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + } } } void sendLocalEndpointId(PeerHandle peerHandle) { - mDiscoverySession.sendMessage(peerHandle, 0, getLocalEndpointId().getBytes()); + synchronized (mSessionLock) { + mDiscoverySession.sendMessage(peerHandle, 0, getLocalEndpointId().getBytes()); + } } void onConnecting(byte[] remoteDeviceName) { - mConnectionStatus.remoteDeviceName = new String(remoteDeviceName); - Log.e(TAG, "Connecting to " + mConnectionStatus.remoteDeviceName); - for (ConnectionCallback callback : mConnectionCallbacks) { - callback.onConnecting(mConnectionStatus.remoteDeviceName); + synchronized (mSessionLock) { + mConnectionStatus.remoteDeviceName = new String(remoteDeviceName); + Log.e(TAG, "Connecting to " + mConnectionStatus.remoteDeviceName); + synchronized (mConnectionCallbacks) { + for (ConnectionCallback callback : mConnectionCallbacks) { + callback.onConnecting(mConnectionStatus.remoteDeviceName); + } + } } } @@ -246,8 +282,10 @@ public class ConnectionManager { .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE) .setNetworkSpecifier(networkSpecifierBuilder.build()) .build(); - mNetworkCallback = networkCallback; - mConnectivityManager.requestNetwork(networkRequest, networkCallback); + synchronized (mSessionLock) { + mNetworkCallback = networkCallback; + mConnectivityManager.requestNetwork(networkRequest, mNetworkCallback); + } } } @@ -255,12 +293,14 @@ public class ConnectionManager { @Override public void onPublishStarted(@NonNull PublishDiscoverySession session) { - mDiscoverySession = session; + synchronized (mSessionLock) { + mDiscoverySession = session; + } } @Override public void onMessageReceived(PeerHandle peerHandle, byte[] message) { - if (mConnectionStatus.connected) { + if (isConnected()) { return; } @@ -284,7 +324,9 @@ public class ConnectionManager { @Override public void onSubscribeStarted(@NonNull SubscribeDiscoverySession session) { - mDiscoverySession = session; + synchronized (mSessionLock) { + mDiscoverySession = session; + } } @Override @@ -295,7 +337,7 @@ public class ConnectionManager { @Override public void onMessageReceived(PeerHandle peerHandle, byte[] message) { - if (mConnectionStatus.connected) { + if (isConnected()) { return; } onConnecting(message); @@ -316,7 +358,7 @@ public class ConnectionManager { @Override public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { - if (mConnectionStatus.connected) { + if (isConnected()) { return; } diff --git a/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/RemoteIo.java b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/RemoteIo.java index 4f22f5560..71a62d87e 100644 --- a/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/RemoteIo.java +++ b/samples/VirtualDeviceManager/common/src/com/example/android/vdmdemo/common/RemoteIo.java @@ -21,12 +21,13 @@ import android.os.HandlerThread; import android.util.ArrayMap; import android.util.Log; +import androidx.annotation.GuardedBy; + import com.example.android.vdmdemo.common.RemoteEventProto.RemoteEvent; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Collections; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -44,12 +45,16 @@ public class RemoteIo { void onStreamClosed(); } + private final Object mLock = new Object(); + + @GuardedBy("mLock") private OutputStream mOutputStream = null; + private StreamClosedCallback mOutputStreamClosedCallback = null; private final Handler mSendMessageHandler; - private final Map mMessageConsumers = - Collections.synchronizedMap(new ArrayMap<>()); + @GuardedBy("mMessageConsumers") + private final Map mMessageConsumers = new ArrayMap<>(); @Inject RemoteIo() { @@ -60,50 +65,45 @@ public class RemoteIo { @SuppressWarnings("ThreadPriorityCheck") void initialize(InputStream inputStream, StreamClosedCallback inputStreamClosedCallback) { - Thread t = new Thread(() -> { - try { - while (true) { - RemoteEvent event = RemoteEvent.parseDelimitedFrom(inputStream); - if (event == null) { - break; - } - mMessageConsumers.values().forEach(consumer -> { - if (consumer != null) { - consumer.accept(event); - } - }); - } - } catch (IOException e) { - Log.e(TAG, "Failed to obtain event: " + e); - } - inputStreamClosedCallback.onStreamClosed(); - }); + Thread t = new Thread(new ReceiverRunnable(inputStream, inputStreamClosedCallback)); t.setPriority(Thread.MAX_PRIORITY); t.start(); } - synchronized void initialize( + void initialize( OutputStream outputStream, StreamClosedCallback outputStreamClosedCallback) { - mOutputStream = outputStream; - mOutputStreamClosedCallback = outputStreamClosedCallback; + synchronized (mLock) { + mOutputStream = outputStream; + mOutputStreamClosedCallback = outputStreamClosedCallback; + } } /** Registers a consumer for processing events coming from the remote device. */ public void addMessageConsumer(Consumer consumer) { - mMessageConsumers.put(consumer, new MessageConsumer(consumer)); + synchronized (mMessageConsumers) { + mMessageConsumers.put(consumer, new MessageConsumer(consumer)); + } } /** Unregisters a previously registered message consumer. */ public void removeMessageConsumer(Consumer consumer) { - if (mMessageConsumers.remove(consumer) == null) { - Log.w(TAG, "Failed to remove message consumer."); + synchronized (mMessageConsumers) { + if (mMessageConsumers.remove(consumer) == null) { + Log.w(TAG, "Failed to remove message consumer."); + } } } /** Sends an event to the remote device. */ - public synchronized void sendMessage(RemoteEvent event) { - if (mOutputStream != null) { - mSendMessageHandler.post(() -> { + public void sendMessage(RemoteEvent event) { + synchronized (mLock) { + if (mOutputStream == null) { + Log.e(TAG, "Failed to send event, RemoteIO not initialized."); + return; + } + } + mSendMessageHandler.post(() -> { + synchronized (mLock) { try { event.writeDelimitedTo(mOutputStream); mOutputStream.flush(); @@ -111,9 +111,36 @@ public class RemoteIo { mOutputStream = null; mOutputStreamClosedCallback.onStreamClosed(); } - }); - } else { - Log.e(TAG, "Failed to send event, RemoteIO not initialized."); + } + }); + } + + private class ReceiverRunnable implements Runnable { + + private final InputStream mInputStream; + private final StreamClosedCallback mInputStreamClosedCallback; + + ReceiverRunnable(InputStream inputStream, StreamClosedCallback inputStreamClosedCallback) { + mInputStream = inputStream; + mInputStreamClosedCallback = inputStreamClosedCallback; + } + + @Override + public void run() { + try { + while (true) { + RemoteEvent event = RemoteEvent.parseDelimitedFrom(mInputStream); + if (event == null) { + break; + } + synchronized (mMessageConsumers) { + mMessageConsumers.values().forEach(consumer -> consumer.accept(event)); + } + } + } catch (IOException e) { + Log.e(TAG, "Failed to obtain event: " + e); + } + mInputStreamClosedCallback.onStreamClosed(); } } diff --git a/samples/VirtualDeviceManager/setup.sh b/samples/VirtualDeviceManager/setup.sh index 8638ee279..390e09c38 100755 --- a/samples/VirtualDeviceManager/setup.sh +++ b/samples/VirtualDeviceManager/setup.sh @@ -20,7 +20,7 @@ function die() { } function run_cmd_or_die() { - "${@}" > /dev/null 2>&1 || die "Command failed: ${*}" + "${@}" > /dev/null || die "Command failed: ${*}" } function select_device() { @@ -30,6 +30,13 @@ function select_device() { done } +function install_app() { + if ! adb -s "${1}" install -r -d -g "${2}" > /dev/null 2>&1; then + adb -s "${1}" uninstall "com.example.android.vdmdemo.${3}" > /dev/null 2>&1 + run_cmd_or_die adb -s "${1}" install -r -d -g "${2}" + fi +} + [[ -f build/make/envsetup.sh ]] || die "Run this script from the root of the tree." DEVICE_COUNT=$(adb devices -l | tail -n +2 | head -n -1 | wc -l) @@ -41,36 +48,16 @@ HOST_SERIAL="" CLIENT_SERIAL="" echo -if ((DEVICE_COUNT > 1)); then - echo "Multiple devices found:" - for i in "${!DEVICE_SERIALS[@]}"; do - echo -e "${i}: ${DEVICE_SERIALS[${i}]}\t${DEVICE_NAMES[${i}]}" - done - echo "${DEVICE_COUNT}: Do not install this app" - echo - select_device "VDM Host" - HOST_INDEX=$? - select_device "VDM Client" - CLIENT_INDEX=$? -else - DEVICE_SERIAL=${DEVICE_SERIALS[0]} - DEVICE_NAME="${DEVICE_SERIAL} ${DEVICE_NAMES[0]}" - cat << EOF -0: VDM Host app -1: VDM Client app -2: All -3: None - -EOF - while :; do - read -r -p "Select apps to install to ${DEVICE_NAME} (0-3): " INDEX - ( [[ "${INDEX}" =~ ^[0-9]+$ ]] && ((INDEX >= 0 && INDEX <= 3)) ) || continue; - ((INDEX == 3)) && exit 0 - ((INDEX != 0 && INDEX != 2)) && HOST_INDEX=DEVICE_COUNT - ((INDEX != 1 && INDEX != 2)) && CLIENT_INDEX=DEVICE_COUNT - break - done -fi +echo "Available devices:" +for i in "${!DEVICE_SERIALS[@]}"; do + echo -e "${i}: ${DEVICE_SERIALS[${i}]}\t${DEVICE_NAMES[${i}]}" +done +echo "${DEVICE_COUNT}: Do not install this app" +echo +select_device "VDM Host" +HOST_INDEX=$? +select_device "VDM Client" +CLIENT_INDEX=$? echo if ((HOST_INDEX == DEVICE_COUNT)); then @@ -103,15 +90,13 @@ UNBUNDLED_BUILD_SDKS_FROM_SOURCE=true m -j "${APKS_TO_BUILD}" || die "Build fail if [[ -n "${CLIENT_SERIAL}" ]]; then echo echo "Installing VdmClient.apk to ${CLIENT_NAME}..." - adb -s "${CLIENT_SERIAL}" uninstall com.example.android.vdmdemo.client > /dev/null 2>&1 - run_cmd_or_die adb -s "${CLIENT_SERIAL}" install -r -d -g "${OUT}/system/app/VdmClient/VdmClient.apk" + install_app "${CLIENT_SERIAL}" "${OUT}/system/app/VdmClient/VdmClient.apk" client fi if [[ -n "${HOST_SERIAL}" ]]; then echo echo "Installing VdmDemos.apk to ${HOST_NAME}..." - adb -s "${HOST_SERIAL}" uninstall com.example.android.vdmdemo.demos > /dev/null 2>&1 - run_cmd_or_die adb -s "${HOST_SERIAL}" install -r -d -g "${OUT}/system/app/VdmDemos/VdmDemos.apk" + install_app "${CLIENT_SERIAL}" "${OUT}/system/app/VdmDemos/VdmDemos.apk" demos echo readonly PERM_BASENAME=com.example.android.vdmdemo.host.xml