diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml
index 66038da7e2..c6b36cfa6d 100644
--- a/play-services-core/src/main/AndroidManifest.xml
+++ b/play-services-core/src/main/AndroidManifest.xml
@@ -136,8 +136,16 @@
android:protectionLevel="signature"/>
+
+
+
+
+
+
+
+
diff --git a/play-services-wearable/core/src/main/AndroidManifest.xml b/play-services-wearable/core/src/main/AndroidManifest.xml
index 9df69b73bd..e254e84af0 100644
--- a/play-services-wearable/core/src/main/AndroidManifest.xml
+++ b/play-services-wearable/core/src/main/AndroidManifest.xml
@@ -6,6 +6,16 @@
+
+
+
+
+
+
+
+
diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/BluetoothWearableConnection.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/BluetoothWearableConnection.java
new file mode 100644
index 0000000000..ce6bbf7930
--- /dev/null
+++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/BluetoothWearableConnection.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2013-2019 microG Project Team
+ *
+ * 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 org.microg.gms.wearable;
+
+import android.bluetooth.BluetoothSocket;
+
+import org.microg.wearable.WearableConnection;
+import org.microg.wearable.proto.MessagePiece;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+
+/**
+ * Bluetooth transport implementation for Wearable connections.
+ * Uses RFCOMM sockets to communicate with WearOS devices over Bluetooth Classic.
+ */
+public class BluetoothWearableConnection extends WearableConnection {
+ private final int MAX_PIECE_SIZE = 20 * 1024 * 1024; // 20MB limit
+ private final BluetoothSocket socket;
+ private final DataInputStream is;
+ private final DataOutputStream os;
+
+ public BluetoothWearableConnection(BluetoothSocket socket, Listener listener) throws IOException {
+ super(listener);
+ this.socket = socket;
+ this.is = new DataInputStream(socket.getInputStream());
+ this.os = new DataOutputStream(socket.getOutputStream());
+ }
+
+ @Override
+ protected void writeMessagePiece(MessagePiece piece) throws IOException {
+ byte[] bytes = piece.encode();
+ os.writeInt(bytes.length);
+ os.write(bytes);
+ os.flush();
+ }
+
+ @Override
+ protected MessagePiece readMessagePiece() throws IOException {
+ int len = is.readInt();
+ if (len > MAX_PIECE_SIZE || len < 0) {
+ throw new IOException("Piece size " + len + " exceeded limit of " + MAX_PIECE_SIZE + " bytes.");
+ }
+ byte[] bytes = new byte[len];
+ is.readFully(bytes);
+
+ // Use reflection to call wire.parseFrom() to work around Wire version incompatibility
+ // The inherited 'wire' instance from WearableConnection uses Wire 1.6.1 API
+ try {
+ Method parseFrom = wire.getClass().getMethod("parseFrom", byte[].class, Class.class);
+ return (MessagePiece) parseFrom.invoke(wire, bytes, MessagePiece.class);
+ } catch (Exception e) {
+ throw new IOException("Failed to deserialize MessagePiece: " + e.getMessage(), e);
+ }
+ }
+
+ public String getRemoteAddress() {
+ return socket.getRemoteDevice().getAddress();
+ }
+
+ @Override
+ public void close() throws IOException {
+ socket.close();
+ }
+}
diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java
index 1f0ed12669..45361c43c2 100644
--- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java
+++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableImpl.java
@@ -27,6 +27,9 @@
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
import androidx.annotation.Nullable;
@@ -69,6 +72,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import okio.ByteString;
@@ -76,6 +80,8 @@
public class WearableImpl {
private static final String TAG = "GmsWear";
+ // Standard Serial Port Profile UUID for Bluetooth Classic
+ private static final UUID UUID_WEAR = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private static final int WEAR_TCP_PORT = 5601;
@@ -85,8 +91,10 @@ public class WearableImpl {
private final Map> listeners = new HashMap>();
private final Set connectedNodes = new HashSet();
private final Map activeConnections = new HashMap();
+ private final Set pendingConnections = Collections.synchronizedSet(new HashSet());
private RpcHelper rpcHelper;
private SocketConnectionThread sct;
+ private ConnectionThread btThread;
private ConnectionConfiguration[] configurations;
private boolean configurationsUpdated = false;
private ClockworkNodePreferences clockworkNodePreferences;
@@ -105,6 +113,13 @@ public WearableImpl(Context context, NodeDatabaseHelper nodeDatabase, Configurat
networkHandlerLock.countDown();
Looper.loop();
}).start();
+
+ // Start Bluetooth connection thread if Bluetooth is available
+ BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (btAdapter != null) {
+ btThread = new ConnectionThread();
+ btThread.start();
+ }
}
public String getLocalNodeId() {
@@ -249,15 +264,25 @@ public void syncToPeer(String peerNodeId, String nodeId, long seqId) {
void syncRecordToAll(DataItemRecord record) {
- for (String nodeId : new ArrayList(activeConnections.keySet())) {
+ ArrayList nodeIds;
+ synchronized (activeConnections) {
+ nodeIds = new ArrayList(activeConnections.keySet());
+ }
+ for (String nodeId : nodeIds) {
syncRecordToPeer(nodeId, record);
}
}
private boolean syncRecordToPeer(String nodeId, DataItemRecord record) {
+ WearableConnection connection;
+ synchronized (activeConnections) {
+ connection = activeConnections.get(nodeId);
+ }
+ if (connection == null) return false;
+
for (Asset asset : record.dataItem.getAssets().values()) {
try {
- syncAssetToPeer(nodeId, record, asset);
+ syncAssetToPeer(connection, record, asset);
} catch (Exception e) {
Log.w(TAG, "Could not sync asset " + asset + " for " + nodeId + " and " + record, e);
closeConnection(nodeId);
@@ -267,7 +292,7 @@ private boolean syncRecordToPeer(String nodeId, DataItemRecord record) {
try {
SetDataItem item = record.toSetDataItem();
- activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().setDataItem(item).build());
+ connection.writeMessage(new RootMessage.Builder().setDataItem(item).build());
} catch (Exception e) {
Log.w(TAG, e);
closeConnection(nodeId);
@@ -276,12 +301,12 @@ private boolean syncRecordToPeer(String nodeId, DataItemRecord record) {
return true;
}
- private void syncAssetToPeer(String nodeId, DataItemRecord record, Asset asset) throws IOException {
+ private void syncAssetToPeer(WearableConnection connection, DataItemRecord record, Asset asset) throws IOException {
RootMessage announceMessage = new RootMessage.Builder().setAsset(new SetAsset.Builder()
.digest(asset.getDigest())
.appkeys(new AppKeys(Collections.singletonList(new AppKey(record.packageName, record.signatureDigest))))
.build()).hasAsset(true).build();
- activeConnections.get(nodeId).writeMessage(announceMessage);
+ connection.writeMessage(announceMessage);
File assetFile = createAssetFile(asset.getDigest());
String fileName = calculateDigest(announceMessage.encode());
FileInputStream fis = new FileInputStream(assetFile);
@@ -290,11 +315,11 @@ private void syncAssetToPeer(String nodeId, DataItemRecord record, Asset asset)
int c = 0;
while ((c = fis.read(arr)) > 0) {
if (lastPiece != null) {
- activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, false, lastPiece, null)).build());
+ connection.writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, false, lastPiece, null)).build());
}
lastPiece = ByteString.of(arr, 0, c);
}
- activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, true, lastPiece, asset.getDigest())).build());
+ connection.writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, true, lastPiece, asset.getDigest())).build());
}
public void addAssetToDatabase(Asset asset, List appKeys) {
@@ -349,7 +374,9 @@ public void onConnectReceived(WearableConnection connection, String nodeId, Conn
}
}
Log.d(TAG, "Adding connection to list of open connections: " + connection + " with connect " + connect);
- activeConnections.put(connect.id, connection);
+ synchronized (activeConnections) {
+ activeConnections.put(connect.id, connection);
+ }
onPeerConnected(new NodeParcelable(connect.id, connect.name));
// Fetch missing assets
Cursor cursor = nodeDatabase.listMissingAssets();
@@ -380,7 +407,9 @@ public void onDisconnectReceived(WearableConnection connection, Connect connect)
}
}
Log.d(TAG, "Removing connection from list of open connections: " + connection);
- activeConnections.remove(connect.id);
+ synchronized (activeConnections) {
+ activeConnections.remove(connect.id);
+ }
onPeerDisconnected(new NodeParcelable(connect.id, connect.name));
}
@@ -569,23 +598,60 @@ record = DataItemRecord.fromCursor(cursor);
private IWearableListener getListener(String packageName, String action, Uri uri) {
Intent intent = new Intent(action);
intent.setPackage(packageName);
- intent.setData(uri);
+ intent.setData(uri);
return RemoteListenerProxy.get(context, intent, IWearableListener.class, "com.google.android.gms.wearable.BIND_LISTENER");
}
- private void closeConnection(String nodeId) {
- WearableConnection connection = activeConnections.get(nodeId);
- try {
- connection.close();
- } catch (IOException e1) {
- Log.w(TAG, e1);
+ public boolean isPendingConnection(String address) {
+ return pendingConnections.contains(address);
+ }
+
+ public boolean isConnectedByAddress(String address) {
+ synchronized (activeConnections) {
+ for (WearableConnection conn : activeConnections.values()) {
+ if (conn instanceof BluetoothWearableConnection) {
+ if (((BluetoothWearableConnection) conn).getRemoteAddress().equals(address)) {
+ return true;
+ }
+ }
+ }
}
- if (connection == sct.getWearableConnection()) {
- sct.close();
- sct = null;
+ return false;
+ }
+
+ public String getNodeIdByAddress(String address) {
+ synchronized (activeConnections) {
+ for (Map.Entry entry : activeConnections.entrySet()) {
+ if (entry.getValue() instanceof BluetoothWearableConnection) {
+ if (((BluetoothWearableConnection) entry.getValue()).getRemoteAddress().equals(address)) {
+ return entry.getKey();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public void closeConnection(String nodeId) {
+ WearableConnection connection;
+ synchronized (activeConnections) {
+ connection = activeConnections.get(nodeId);
+ activeConnections.remove(nodeId);
+ }
+
+ if (connection != null) {
+ try {
+ connection.close();
+ } catch (IOException e1) {
+ Log.w(TAG, e1);
+ }
+ if (sct != null && connection == sct.getWearableConnection()) {
+ sct.close();
+ sct = null;
+ }
}
- activeConnections.remove(nodeId);
+
for (ConnectionConfiguration config : getConfigurations()) {
if (nodeId.equals(config.nodeId) || nodeId.equals(config.peerNodeId)) {
config.connected = false;
@@ -621,6 +687,12 @@ public int sendMessage(String packageName, String targetNodeId, String path, byt
return -1;
}
+ public java.util.Set getConnectedNodes() {
+ synchronized (activeConnections) {
+ return new java.util.HashSet<>(activeConnections.keySet());
+ }
+ }
+
public void stop() {
try {
this.networkHandlerLock.await();
@@ -628,6 +700,10 @@ public void stop() {
} catch (InterruptedException e) {
Log.w(TAG, e);
}
+ if (btThread != null) {
+ btThread.interrupt();
+ btThread = null;
+ }
}
private class ListenerInfo {
@@ -639,4 +715,125 @@ private ListenerInfo(IWearableListener listener, IntentFilter[] filters) {
this.filters = filters;
}
}
+
+ private class ConnectionThread extends Thread {
+
+
+ @Override
+ public void run() {
+ while (!isInterrupted()) {
+ try {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter != null && adapter.isEnabled()) {
+ // Check BLUETOOTH_CONNECT permission for Android 12+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ if (context.checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ != android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ Log.w(TAG, "BLUETOOTH_CONNECT permission not granted, skipping device scan");
+ Thread.sleep(10000);
+ continue;
+ }
+ }
+
+ Set bondedDevices = adapter.getBondedDevices();
+ if (bondedDevices != null) {
+ for (BluetoothDevice device : bondedDevices) {
+ // Synchronized check for existing connections to this device
+ boolean isConnected = false;
+
+ // Check active connections
+ synchronized (activeConnections) {
+ for (WearableConnection conn : activeConnections.values()) {
+ if (conn instanceof BluetoothWearableConnection) {
+ if (((BluetoothWearableConnection) conn).getRemoteAddress().equals(device.getAddress())) {
+ isConnected = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // Check pending connections (race condition fix)
+ if (!isConnected && pendingConnections.contains(device.getAddress())) {
+ isConnected = true;
+ }
+
+ if (isConnected) {
+ continue;
+ }
+
+ pendingConnections.add(device.getAddress());
+ Log.d(TAG, "Attempting BT connection to " + device.getName() + " (" + device.getAddress() + ")");
+
+ BluetoothSocket socket = null;
+ try {
+ // Create RFCOMM socket using SPP UUID
+ socket = device.createRfcommSocketToServiceRecord(UUID_WEAR);
+ socket.connect();
+
+ if (socket.isConnected()) {
+ Log.d(TAG, "Successfully connected via Bluetooth to " + device.getName());
+
+ // Create wearable connection wrapper
+ ConnectionConfiguration config = new ConnectionConfiguration(device.getName(), device.getAddress(), 3, 0, true);
+ MessageHandler messageHandler = new MessageHandler(context, WearableImpl.this, config);
+ BluetoothWearableConnection connection = new BluetoothWearableConnection(socket, messageHandler);
+
+ // Enable auto-close on error
+ // connection.setListener(messageHandler); // Implied by constructor
+
+ // Start message processing thread
+ new Thread(connection).start();
+
+ try {
+ // Send our identity to the watch
+ String localId = getLocalNodeId();
+
+ // TODO: We should probably get the actual device name from Settings?
+ // Using "Phone" for now as it seems to be the default GMS behavior.
+ connection.writeMessage(
+ new RootMessage.Builder()
+ .connect(new Connect.Builder()
+ .id(localId)
+ .name("Phone")
+ .networkId(localId)
+ .peerAndroidId(0L)
+ .peerVersion(2) // Need at least version 2 for modern WearOS
+ .build())
+ .build()
+ );
+ } catch (IOException e) {
+ Log.w(TAG, "Handshake failed, closing connection", e);
+ connection.close();
+ }
+ }
+ } catch (IOException e) {
+ Log.d(TAG, "BT connection failed: " + e.getMessage());
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException closeErr) {
+ // Ignore
+ }
+ }
+ } finally {
+ pendingConnections.remove(device.getAddress());
+ }
+
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Error in Bluetooth ConnectionThread", e);
+ }
+
+ // Wait before next scan attempt
+ try {
+ Thread.sleep(10000);
+ } catch (InterruptedException e) {
+ break;
+ }
+ }
+ }
+ }
}
diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java
index c9f3194ede..342565bb62 100644
--- a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java
+++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableService.java
@@ -33,18 +33,22 @@ public WearableService() {
super("GmsWearSvc", GmsService.WEAR);
}
+ public static volatile WearableImpl impl;
+
@Override
public void onCreate() {
super.onCreate();
ConfigurationDatabaseHelper configurationDatabaseHelper = new ConfigurationDatabaseHelper(getApplicationContext());
NodeDatabaseHelper nodeDatabaseHelper = new NodeDatabaseHelper(getApplicationContext());
wearable = new WearableImpl(getApplicationContext(), nodeDatabaseHelper, configurationDatabaseHelper);
+ impl = wearable;
}
@Override
public void onDestroy() {
super.onDestroy();
wearable.stop();
+ impl = null;
}
@Override
diff --git a/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java
new file mode 100644
index 0000000000..7dabc5abe7
--- /dev/null
+++ b/play-services-wearable/core/src/main/java/org/microg/gms/wearable/WearableSettingsActivity.java
@@ -0,0 +1,182 @@
+package org.microg.gms.wearable;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.os.Bundle;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.view.View;
+import android.widget.Toast;
+
+import org.microg.gms.wearable.core.R;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+public class WearableSettingsActivity extends Activity {
+
+ private ListView listView;
+ private TextView emptyView;
+ private WearableDeviceAdapter deviceAdapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.wearable_settings_activity);
+
+ listView = findViewById(R.id.device_list);
+ emptyView = findViewById(R.id.empty_view);
+ listView.setEmptyView(emptyView);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ refreshList();
+ }
+
+ private void refreshList() {
+ ArrayList deviceList = new ArrayList<>();
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+ if (adapter == null || !adapter.isEnabled()) {
+ emptyView.setText("Bluetooth is disabled");
+ return;
+ }
+
+ // Check BLUETOOTH_CONNECT permission for Android 12+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ if (checkSelfPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ != android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ emptyView.setText("Bluetooth permission not granted");
+ return;
+ }
+ }
+
+ Set bondedDevices = adapter.getBondedDevices();
+ if (bondedDevices != null) {
+ deviceList.addAll(bondedDevices);
+ }
+
+ Set connectedNodes = null;
+ WearableImpl service = WearableService.impl;
+ if (service != null) {
+ connectedNodes = service.getConnectedNodes();
+ }
+
+ deviceAdapter = new WearableDeviceAdapter(this, deviceList, connectedNodes);
+ listView.setAdapter(deviceAdapter);
+
+ listView.setOnItemClickListener((parent, view, position, id) -> {
+ BluetoothDevice device = deviceAdapter.getItem(position);
+ boolean isConnected = false;
+ WearableImpl currentService = WearableService.impl;
+ if (currentService != null) {
+ // Check exact Bluetooth address match
+ isConnected = currentService.isConnectedByAddress(device.getAddress());
+ }
+
+ android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(this);
+ builder.setTitle(device.getName());
+
+ if (isConnected) {
+ builder.setMessage("This device is currently connected via MicroG.");
+ builder.setPositiveButton("Disconnect", (dialog, which) -> {
+ if (WearableService.impl != null) {
+ String nodeId = WearableService.impl.getNodeIdByAddress(device.getAddress());
+ if (nodeId != null) {
+ WearableService.impl.closeConnection(nodeId);
+ Toast.makeText(this, "Disconnected", Toast.LENGTH_SHORT).show();
+ refreshList();
+ } else {
+ Toast.makeText(this, "Could not find connection for device", Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ } else {
+ builder.setMessage("This device is acting as a WearOS peer.");
+ builder.setPositiveButton("Connect", (dialog, which) -> {
+ // Connection is automatic, but we can trigger a scan or hint
+ Toast.makeText(this, "MicroG is managing connections automatically.", Toast.LENGTH_SHORT).show();
+ });
+ }
+ builder.setNeutralButton("Bluetooth Settings", (dialog, which) -> {
+ try {
+ startActivity(new android.content.Intent(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS));
+ } catch (Exception e) {}
+ });
+ builder.setNegativeButton("Cancel", null);
+ builder.show();
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(android.view.Menu menu) {
+ menu.add(0, 1, 0, "Scan for Devices")
+ .setIcon(android.R.drawable.ic_menu_search)
+ .setShowAsAction(android.view.MenuItem.SHOW_AS_ACTION_ALWAYS);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(android.view.MenuItem item) {
+ if (item.getItemId() == 1) {
+ try {
+ startActivity(new android.content.Intent(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS));
+ } catch (Exception e) {
+ Toast.makeText(this, "Cannot open Bluetooth settings", Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private static class WearableDeviceAdapter extends ArrayAdapter {
+ private final Set connectedNodes;
+
+ public WearableDeviceAdapter(android.content.Context context, ArrayList devices, Set connectedNodes) {
+ super(context, 0, devices);
+ this.connectedNodes = connectedNodes;
+ }
+
+ @Override
+ @android.annotation.SuppressLint("MissingPermission")
+ public android.view.View getView(int position, android.view.View convertView, android.view.ViewGroup parent) {
+ if (convertView == null) {
+ convertView = android.view.LayoutInflater.from(getContext()).inflate(R.layout.wearable_device_item, parent, false);
+ }
+
+ BluetoothDevice device = getItem(position);
+ TextView nameView = convertView.findViewById(R.id.device_name);
+ TextView addressView = convertView.findViewById(R.id.device_address);
+ TextView statusView = convertView.findViewById(R.id.device_status);
+ android.widget.ImageView iconView = convertView.findViewById(R.id.device_icon);
+
+ nameView.setText(device.getName());
+ addressView.setText(device.getAddress());
+
+ // Accurate status logic
+ boolean isConnected = false;
+ if (WearableService.impl != null) {
+ isConnected = WearableService.impl.isConnectedByAddress(device.getAddress());
+ } else if (connectedNodes != null && connectedNodes.contains(device.getAddress())) {
+ // Fallback
+ isConnected = true;
+ }
+
+ if (isConnected) {
+ statusView.setText("Connected");
+ statusView.setTextColor(getContext().getResources().getColor(android.R.color.holo_green_dark));
+ iconView.setAlpha(1.0f);
+ } else {
+ statusView.setText("Bonded");
+ statusView.setTextColor(getContext().getResources().getColor(android.R.color.darker_gray));
+ iconView.setAlpha(0.6f);
+ }
+
+ return convertView;
+ }
+ }
+}
diff --git a/play-services-wearable/core/src/main/res/layout/wearable_device_item.xml b/play-services-wearable/core/src/main/res/layout/wearable_device_item.xml
new file mode 100644
index 0000000000..0e88c4dbed
--- /dev/null
+++ b/play-services-wearable/core/src/main/res/layout/wearable_device_item.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/play-services-wearable/core/src/main/res/layout/wearable_settings_activity.xml b/play-services-wearable/core/src/main/res/layout/wearable_settings_activity.xml
new file mode 100644
index 0000000000..ba4714d5e6
--- /dev/null
+++ b/play-services-wearable/core/src/main/res/layout/wearable_settings_activity.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/play-services-wearable/core/src/main/res/values/strings.xml b/play-services-wearable/core/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..08f60e89cb
--- /dev/null
+++ b/play-services-wearable/core/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+
+ Wearable Devices
+ Manage Bluetooth connection to WearOS devices
+ No paired Bluetooth devices found.
+ Connected
+ Disconnected
+ Connect
+