diff --git a/extern/GmsApi b/extern/GmsApi index cf2927f2..417ec258 160000 --- a/extern/GmsApi +++ b/extern/GmsApi @@ -1 +1 @@ -Subproject commit cf2927f275c53099781dbf965acb7c7f8ed36e97 +Subproject commit 417ec2585f085c5a599e45aa9ee3b3df6239c628 diff --git a/extern/Wearable b/extern/Wearable index 796c8b6b..853f0382 160000 --- a/extern/Wearable +++ b/extern/Wearable @@ -1 +1 @@ -Subproject commit 796c8b6b87d564e3bc2e5000c6d4ed348c606083 +Subproject commit 853f0382b960fcfcad8d7d3655d7963a58095069 diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 1d967e60..87a1909a 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -129,6 +129,14 @@ + + + + + + getAssets() { return Collections.unmodifiableMap(new HashMap(assets)); } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("DataItemInternal{"); + sb.append("uri=").append(uri); + sb.append('}'); + return sb.toString(); + } } diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/DataItemRecord.java b/play-services-core/src/main/java/org/microg/gms/wearable/DataItemRecord.java index 9c8d85ce..af1b9294 100644 --- a/play-services-core/src/main/java/org/microg/gms/wearable/DataItemRecord.java +++ b/play-services-core/src/main/java/org/microg/gms/wearable/DataItemRecord.java @@ -19,6 +19,7 @@ package org.microg.gms.wearable; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; +import android.util.Log; import com.google.android.gms.common.data.DataHolder; import com.google.android.gms.wearable.Asset; diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/MessageHandler.java b/play-services-core/src/main/java/org/microg/gms/wearable/MessageHandler.java index 7da789ff..d2d0212e 100644 --- a/play-services-core/src/main/java/org/microg/gms/wearable/MessageHandler.java +++ b/play-services-core/src/main/java/org/microg/gms/wearable/MessageHandler.java @@ -44,7 +44,7 @@ import java.util.Arrays; public class MessageHandler extends ServerMessageListener { private static final String TAG = "GmsWearMsgHandler"; private final WearableImpl wearable; - private final String thisNodeId; + private final String oldConfigNodeId; private String peerNodeId; public MessageHandler(WearableImpl wearable, ConnectionConfiguration config) { @@ -54,28 +54,28 @@ public class MessageHandler extends ServerMessageListener { private MessageHandler(WearableImpl wearable, ConnectionConfiguration config, String name, String networkId, long androidId) { super(new Connect.Builder() .name(name) - .id(config.nodeId) + .id(wearable.getLocalNodeId()) .networkId(networkId) .peerAndroidId(androidId) .unknown4(3) .peerVersion(1) .build()); this.wearable = wearable; - this.thisNodeId = config.nodeId; + this.oldConfigNodeId = config.nodeId; } @Override public void onConnect(Connect connect) { super.onConnect(connect); peerNodeId = connect.id; - wearable.onConnectReceived(getConnection(), thisNodeId, connect); + wearable.onConnectReceived(getConnection(), oldConfigNodeId, connect); try { getConnection().writeMessage(new RootMessage.Builder().syncStart(new SyncStart.Builder() .receivedSeqId(-1L) .version(2) .syncTable(Arrays.asList( new SyncTableEntry.Builder().key("cloud").value(1L).build(), - new SyncTableEntry.Builder().key(thisNodeId).value(wearable.getCurrentSeqId(thisNodeId)).build(), // TODO + new SyncTableEntry.Builder().key(wearable.getLocalNodeId()).value(wearable.getCurrentSeqId(wearable.getLocalNodeId())).build(), // TODO new SyncTableEntry.Builder().key(peerNodeId).value(wearable.getCurrentSeqId(peerNodeId)).build() // TODO )).build()).build()); } catch (IOException e) { @@ -85,7 +85,10 @@ public class MessageHandler extends ServerMessageListener { @Override public void onDisconnected() { - wearable.onDisconnectReceived(getConnection(), thisNodeId, getRemoteConnect()); + Connect connect = getRemoteConnect(); + if (connect == null) + connect = new Connect.Builder().id(oldConfigNodeId).name("Wear device").build(); + wearable.onDisconnectReceived(getConnection(), connect); super.onDisconnected(); } @@ -120,13 +123,13 @@ public class MessageHandler extends ServerMessageListener { boolean hasLocalNode = false; if (syncStart.syncTable != null) { for (SyncTableEntry entry : syncStart.syncTable) { - wearable.syncToPeer(getConnection(), entry.key, entry.value); + wearable.syncToPeer(peerNodeId, entry.key, entry.value); if (wearable.getLocalNodeId().equals(entry.key)) hasLocalNode = true; } } else { Log.d(TAG, "No sync table given."); } - if (!hasLocalNode) wearable.syncToPeer(getConnection(), wearable.getLocalNodeId(), 0); + if (!hasLocalNode) wearable.syncToPeer(peerNodeId, wearable.getLocalNodeId(), 0); } @Override @@ -138,7 +141,7 @@ public class MessageHandler extends ServerMessageListener { @Override public void onRpcRequest(Request rpcRequest) { Log.d(TAG, "onRpcRequest: " + rpcRequest); - if (TextUtils.isEmpty(rpcRequest.targetNodeId) || rpcRequest.targetNodeId.equals(thisNodeId)) { + if (TextUtils.isEmpty(rpcRequest.targetNodeId) || rpcRequest.targetNodeId.equals(wearable.getLocalNodeId())) { MessageEventParcelable messageEvent = new MessageEventParcelable(); messageEvent.data = rpcRequest.rawData != null ? rpcRequest.rawData.toByteArray() : null; messageEvent.path = rpcRequest.path; diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java b/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java index 9ea6f3e9..dfef5bfc 100644 --- a/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java +++ b/play-services-core/src/main/java/org/microg/gms/wearable/NodeDatabaseHelper.java @@ -22,6 +22,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.text.TextUtils; +import android.util.Log; import com.google.android.gms.wearable.Asset; @@ -73,18 +74,25 @@ public class NodeDatabaseHelper extends SQLiteOpenHelper { String selection; if (path == null) { params = new String[]{packageName, signatureDigest}; - selection = "packageName =? AND signatureDigest =?"; + selection = "packageName = ? AND signatureDigest = ?"; } else if (host == null) { + if (path.endsWith("/")) path = path + "%"; params = new String[]{packageName, signatureDigest, path}; - selection = "packageName =? AND signatureDigest =? AND path =?"; + selection = "packageName = ? AND signatureDigest = ? AND path LIKE ?"; } else { + if (path.endsWith("/")) path = path + "%"; params = new String[]{packageName, signatureDigest, host, path}; - selection = "packageName =? AND signatureDigest =? AND host =? AND path =?"; + selection = "packageName = ? AND signatureDigest = ? AND host = ? AND path LIKE ?"; } selection += " AND deleted=0 AND assetsPresent !=0"; return getReadableDatabase().rawQuery("SELECT host AS host,path AS path,data AS data,\'\' AS tags,assetname AS asset_key,assets_digest AS asset_id FROM dataItemsAndAssets WHERE " + selection, params); } + public synchronized Cursor getDataItemsByHostAndPath(String packageName, String signatureDigest, String host, String path) { + Log.d(TAG, "getDataItemsByHostAndPath: " + packageName + ", " + signatureDigest + ", " + host + ", " + path); + return getDataItemsByHostAndPath(getReadableDatabase(), packageName, signatureDigest, host, path); + } + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion != VERSION) { @@ -240,7 +248,7 @@ public class NodeDatabaseHelper extends SQLiteOpenHelper { return res; } - public void putAsset(Asset asset, boolean dataPresent) { + public synchronized void putAsset(Asset asset, boolean dataPresent) { ContentValues cv = new ContentValues(); cv.put("digest", asset.getDigest()); cv.put("dataPresent", dataPresent ? 1 : 0); @@ -248,11 +256,39 @@ public class NodeDatabaseHelper extends SQLiteOpenHelper { getWritableDatabase().insertWithOnConflict("assets", null, cv, SQLiteDatabase.CONFLICT_REPLACE); } - public void allowAssetAccess(String digest, String packageName, String signatureDigest) { + public synchronized void allowAssetAccess(String digest, String packageName, String signatureDigest) { SQLiteDatabase db = getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("assets_digest", digest); cv.put("appkeys_id", getAppKey(db, packageName, signatureDigest)); - db.insert("assetsacls", null, cv); + db.insertWithOnConflict("assetsacls", null, cv, SQLiteDatabase.CONFLICT_REPLACE); + } + + public Cursor listMissingAssets() { + return getReadableDatabase().query("dataItemsAndAssets", GDIBHAP_FIELDS, "assetsPresent = 0 AND assets_digest NOT NULL", null, null, null, "packageName, signatureDigest, host, path"); + } + + public boolean hasAsset(Asset asset) { + Cursor cursor = getReadableDatabase().query("assets", new String[]{"dataPresent"}, "digest=?", new String[]{asset.getDigest()}, null, null, null); + if (cursor == null) return false; + try { + return (cursor.moveToNext() && cursor.getInt(0) == 1); + } finally { + cursor.close(); + } + } + + public synchronized void markAssetAsPresent(String digest) { + ContentValues cv = new ContentValues(); + cv.put("dataPresent", 1); + SQLiteDatabase db = getWritableDatabase(); + db.update("assets", cv, "digest=?", new String[]{digest}); + Cursor status = db.query("assetsReadyStatus", null, "nowReady != markedReady", null, null, null, null); + while (status.moveToNext()) { + cv = new ContentValues(); + cv.put("assetsPresent", status.getInt(status.getColumnIndex("nowReady"))); + db.update("dataitems", cv, "_id=?", new String[]{Integer.toString(status.getInt(status.getColumnIndex("dataitems_id")))}); + } + status.close(); } } diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/RpcHelper.java b/play-services-core/src/main/java/org/microg/gms/wearable/RpcHelper.java new file mode 100644 index 00000000..b3746029 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/wearable/RpcHelper.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2016 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.content.Context; +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Map; + +public class RpcHelper { + private final Map rpcStateMap = new HashMap(); + private final SharedPreferences preferences; + private final Context context; + + public RpcHelper(Context context) { + this.context = context; + this.preferences = context.getSharedPreferences("wearable.rpc_service.settings", 0); + } + + private String getRpcConnectionId(String packageName, String targetNodeId, String path) { + String mode = "lo"; + if (packageName.equals("com.google.android.wearable.app") && path.startsWith("/s3")) + mode = "hi"; + return targetNodeId + ":" + mode; + } + + public RpcHelper.RpcConnectionState useConnectionState(String packageName, String targetNodeId, String path) { + String rpcConnectionId = getRpcConnectionId(packageName, targetNodeId, path); + synchronized (rpcStateMap) { + if (!rpcStateMap.containsKey(rpcConnectionId)) { + int g = preferences.getInt(rpcConnectionId, 1)+1; + preferences.edit().putInt(rpcConnectionId, g).apply(); + rpcStateMap.put(rpcConnectionId, new RpcConnectionState(g)); + } + RpcHelper.RpcConnectionState res = rpcStateMap.get(rpcConnectionId); + res.lastRequestId++; + return res.freeze(); + } + } + + public static class RpcConnectionState { + public int generation; + public int lastRequestId; + + public RpcConnectionState(int generation) { + this.generation = generation; + } + + public RpcConnectionState freeze() { + RpcConnectionState res = new RpcConnectionState(generation); + res.lastRequestId = lastRequestId; + return res; + } + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/WearableImpl.java b/play-services-core/src/main/java/org/microg/gms/wearable/WearableImpl.java index 249da20d..d50539f5 100644 --- a/play-services-core/src/main/java/org/microg/gms/wearable/WearableImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/wearable/WearableImpl.java @@ -44,7 +44,9 @@ import org.microg.wearable.proto.AckAsset; import org.microg.wearable.proto.AppKey; import org.microg.wearable.proto.AppKeys; import org.microg.wearable.proto.Connect; +import org.microg.wearable.proto.FetchAsset; import org.microg.wearable.proto.FilePiece; +import org.microg.wearable.proto.Request; import org.microg.wearable.proto.RootMessage; import org.microg.wearable.proto.SetAsset; @@ -76,7 +78,8 @@ public class WearableImpl { private final ConfigurationDatabaseHelper configDatabase; private final Map> listeners = new HashMap>(); private final Set connectedNodes = new HashSet(); - private final Set activeConnections = new HashSet(); + private final Map activeConnections = new HashMap(); + private RpcHelper rpcHelper; private SocketConnectionThread sct; private ConnectionConfiguration[] configurations; private boolean configurationsUpdated = false; @@ -87,6 +90,7 @@ public class WearableImpl { this.nodeDatabase = nodeDatabase; this.configDatabase = configDatabase; this.clockworkNodePreferences = new ClockworkNodePreferences(context); + this.rpcHelper = new RpcHelper(context); } public String getLocalNodeId() { @@ -108,6 +112,13 @@ public class WearableImpl { public DataItemRecord putDataItem(DataItemRecord record) { nodeDatabase.putRecord(record); + if (!record.assetsAreReady) { + for (Asset asset : record.dataItem.getAssets().values()) { + if (!nodeDatabase.hasAsset(asset)) { + Log.d(TAG, "Asset is missing: " + asset); + } + } + } try { getListener(record.packageName, "com.google.android.gms.wearable.DATA_CHANGED", record.dataItem.uri) .onDataChanged(record.toEventDataHolder()); @@ -151,7 +162,7 @@ public class WearableImpl { return null; } - private File createAssetFile(String digest) { + public File createAssetFile(String digest) { File dir = new File(new File(context.getFilesDir(), "assets"), digest.substring(digest.length() - 2)); dir.mkdirs(); return new File(dir, digest + ".asset"); @@ -183,6 +194,7 @@ public class WearableImpl { if (newConfiguration.name.equals(configuration.name)) { newConfiguration.connected = configuration.connected; newConfiguration.peerNodeId = configuration.peerNodeId; + newConfiguration.nodeId = configuration.nodeId; break; } } @@ -211,52 +223,50 @@ public class WearableImpl { return context; } - public void syncToPeer(WearableConnection connection, String nodeId, long seqId) { - Log.d(TAG, "-- Start syncing over " + connection + ", nodeId " + nodeId + " starting with seqId " + seqId); + public void syncToPeer(String peerNodeId, String nodeId, long seqId) { + Log.d(TAG, "-- Start syncing over to " + peerNodeId + ", nodeId " + nodeId + " starting with seqId " + seqId); Cursor cursor = nodeDatabase.getModifiedDataItems(nodeId, seqId, true); if (cursor != null) { while (cursor.moveToNext()) { - if (!syncRecordToPeer(connection, DataItemRecord.fromCursor(cursor))) break; + if (!syncRecordToPeer(peerNodeId, DataItemRecord.fromCursor(cursor))) break; } cursor.close(); } - Log.d(TAG, "-- Done syncing over " + connection + ", nodeId " + nodeId + " starting with seqId " + seqId); + Log.d(TAG, "-- Done syncing over to " + peerNodeId + ", nodeId " + nodeId + " starting with seqId " + seqId); } private void syncRecordToAll(DataItemRecord record) { Log.d(TAG, "Syncing record " + record + " over " + activeConnections.size() + " connections."); - for (WearableConnection connection : new ArrayList(activeConnections)) { - if (!syncRecordToPeer(connection, record)) { - Log.d(TAG, "Removing connection as it seems not usable: " + connection); - activeConnections.remove(connection); - } + for (String nodeId : new ArrayList(activeConnections.keySet())) { + syncRecordToPeer(nodeId, record); } } - private boolean syncRecordToPeer(WearableConnection connection, DataItemRecord record) { + private boolean syncRecordToPeer(String nodeId, DataItemRecord record) { for (Asset asset : record.dataItem.getAssets().values()) { - syncAssetToPeer(connection, record, asset); + syncAssetToPeer(nodeId, record, asset); } - Log.d(TAG, "Sync over " + connection + ": " + record); + Log.d(TAG, "Sync over to " + nodeId + ": " + record); try { - connection.writeMessage(new RootMessage.Builder().setDataItem(record.toSetDataItem()).build()); + activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().setDataItem(record.toSetDataItem()).build()); } catch (IOException e) { + closeConnection(nodeId); Log.w(TAG, e); return false; } return true; } - private void syncAssetToPeer(WearableConnection connection, DataItemRecord record, Asset asset) { + private void syncAssetToPeer(String nodeId, DataItemRecord record, Asset asset) { try { - Log.d(TAG, "Sync over " + connection + ": " + asset); + Log.d(TAG, "Sync over to " + nodeId + ": " + asset); 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(); - connection.writeMessage(announceMessage); + activeConnections.get(nodeId).writeMessage(announceMessage); File assetFile = createAssetFile(asset.getDigest()); String fileName = calculateDigest(announceMessage.toByteArray()); FileInputStream fis = new FileInputStream(assetFile); @@ -265,15 +275,16 @@ public class WearableImpl { int c = 0; while ((c = fis.read(arr)) > 0) { if (lastPiece != null) { - Log.d(TAG, "Sync over " + connection + ": Asset piece for fileName " + fileName + ": " + lastPiece); - connection.writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, false, lastPiece, null)).build()); + Log.d(TAG, "Sync over to " + nodeId + ": Asset piece for fileName " + fileName + ": " + lastPiece); + activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, false, lastPiece, null)).build()); } lastPiece = ByteString.of(arr, 0, c); } - Log.d(TAG, "Sync over " + connection + ": Last asset piece for fileName " + fileName + ": " + lastPiece); - connection.writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, true, lastPiece, asset.getDigest())).build()); + Log.d(TAG, "Sync over to " + nodeId + ": Last asset piece for fileName " + fileName + ": " + lastPiece); + activeConnections.get(nodeId).writeMessage(new RootMessage.Builder().filePiece(new FilePiece(fileName, true, lastPiece, asset.getDigest())).build()); } catch (IOException e) { Log.w(TAG, e); + closeConnection(nodeId); } } @@ -303,7 +314,7 @@ public class WearableImpl { String digest = calculateDigest(Utils.readStreamToEnd(new FileInputStream(file))); if (digest.equals(finalPieceDigest)) { if (file.renameTo(createAssetFile(digest))) { - // TODO: Mark as stored in db + nodeDatabase.markAssetAsPresent(digest); connection.writeMessage(new RootMessage.Builder().ackAsset(new AckAsset(digest)).build()); } else { Log.w(TAG, "Could not rename to target file name. delete=" + file.delete()); @@ -320,23 +331,47 @@ public class WearableImpl { public void onConnectReceived(WearableConnection connection, String nodeId, Connect connect) { for (ConnectionConfiguration config : getConfigurations()) { if (config.nodeId.equals(nodeId)) { + if (config.nodeId != nodeId) { + config.nodeId = connect.id; + configDatabase.putConfiguration(config, nodeId); + } config.peerNodeId = connect.id; config.connected = true; } } - Log.d(TAG, "Adding connection to list of open connections: " + connection); - activeConnections.add(connection); + Log.d(TAG, "Adding connection to list of open connections: " + connection + " with connect " + connect); + activeConnections.put(connect.id, connection); onPeerConnected(new NodeParcelable(connect.id, connect.name)); + // Fetch missing assets + Cursor cursor = nodeDatabase.listMissingAssets(); + if (cursor != null) { + while (cursor.moveToNext()) { + try { + Log.d(TAG, "Fetch for " + cursor.getString(12)); + connection.writeMessage(new RootMessage.Builder() + .fetchAsset(new FetchAsset.Builder() + .assetName(cursor.getString(12)) + .packageName(cursor.getString(1)) + .signatureDigest(cursor.getString(2)) + .permission(false) + .build()).build()); + } catch (IOException e) { + Log.w(TAG, e); + closeConnection(connect.id); + } + } + cursor.close(); + } } - public void onDisconnectReceived(WearableConnection connection, String nodeId, Connect connect) { + public void onDisconnectReceived(WearableConnection connection, Connect connect) { for (ConnectionConfiguration config : getConfigurations()) { - if (config.nodeId.equals(nodeId)) { + if (config.nodeId.equals(connect.id)) { config.connected = false; } } Log.d(TAG, "Removing connection from list of open connections: " + connection); - activeConnections.remove(connection); + activeConnections.remove(connect.id); onPeerDisconnected(new NodeParcelable(connect.id, connect.name)); } @@ -421,7 +456,7 @@ public class WearableImpl { } dataHolderItems.moveToFirst(); dataHolderItems.moveToPrevious(); - return DataHolder.fromCursor(dataHolderItems, 0, null); + return new DataHolder(dataHolderItems, 0, null); } public DataHolder getDataItemForRecordAsHolder(DataItemRecord record) { @@ -431,7 +466,7 @@ public class WearableImpl { } dataHolderItems.moveToFirst(); dataHolderItems.moveToPrevious(); - return DataHolder.fromCursor(dataHolderItems, 0, null); + return new DataHolder(dataHolderItems, 0, null); } public synchronized void addListener(String packageName, IWearableListener listener) { @@ -451,6 +486,7 @@ public class WearableImpl { configDatabase.setEnabledState(name, true); configurationsUpdated = true; if (name.equals("server") && sct == null) { + Log.d(TAG, "Starting server on :" + WEAR_TCP_PORT); (sct = SocketConnectionThread.serverListen(WEAR_TCP_PORT, new MessageHandler(this, configDatabase.getConfiguration(name)))).start(); } } @@ -497,7 +533,7 @@ public class WearableImpl { } public DataItemRecord getDataItemByUri(Uri uri, String packageName) { - Cursor cursor = nodeDatabase.getDataItemsForDataHolderByHostAndPath(packageName, PackageUtils.firstSignatureDigest(context, packageName), uri.getHost(), uri.getPath()); + Cursor cursor = nodeDatabase.getDataItemsByHostAndPath(packageName, PackageUtils.firstSignatureDigest(context, packageName), uri.getHost(), uri.getPath()); DataItemRecord record = null; if (cursor != null) { if (cursor.moveToNext()) { @@ -505,6 +541,7 @@ public class WearableImpl { } cursor.close(); } + Log.d(TAG, "getDataItem: " + record); return record; } @@ -522,7 +559,49 @@ public class WearableImpl { } } - public void sendMessage(String targetNodeId, String path, byte[] data) { - Log.d(TAG, "sendMessage not yet implemented!"); + private void closeConnection(String nodeId) { + WearableConnection connection = activeConnections.get(nodeId); + try { + connection.close(); + } catch (IOException e1) { + Log.w(TAG, e1); + } + if (connection == sct.getWearableConnection()) { + sct.close(); + sct = null; + } + activeConnections.remove(nodeId); + for (ConnectionConfiguration config : getConfigurations()) { + if (config.nodeId.equals(nodeId) || config.peerNodeId.equals(nodeId)) { + config.connected = false; + } + } + onPeerDisconnected(new NodeParcelable(nodeId, "Wear device")); + Log.d(TAG, "Closed connection to " + nodeId + " on error"); + } + + public int sendMessage(String packageName, String targetNodeId, String path, byte[] data) { + if (activeConnections.containsKey(targetNodeId)) { + WearableConnection connection = activeConnections.get(targetNodeId); + RpcHelper.RpcConnectionState state = rpcHelper.useConnectionState(packageName, targetNodeId, path); + try { + connection.writeMessage(new RootMessage.Builder().rpcRequest(new Request.Builder() + .targetNodeId(targetNodeId) + .path(path) + .rawData(ByteString.of(data)) + .packageName(packageName) + .signatureDigest(PackageUtils.firstSignatureDigest(context, packageName)) + .sourceNodeId(getLocalNodeId()) + .generation(state.generation) + .requestId(state.lastRequestId) + .build()).build()); + } catch (IOException e) { + Log.w(TAG, "Error while writing, closing link", e); + closeConnection(targetNodeId); + return -1; + } + return (state.generation + 527) * 31 + state.lastRequestId; + } + return -1; } } diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/WearableLocationListener.java b/play-services-core/src/main/java/org/microg/gms/wearable/WearableLocationListener.java new file mode 100644 index 00000000..6510b3b9 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/wearable/WearableLocationListener.java @@ -0,0 +1,152 @@ +/* + * Copyright 2013-2016 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.content.Context; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.common.data.DataHolder; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.internal.ClientIdentity; +import com.google.android.gms.location.internal.LocationRequestInternal; +import com.google.android.gms.wearable.internal.AmsEntityUpdateParcelable; +import com.google.android.gms.wearable.internal.AncsNotificationParcelable; +import com.google.android.gms.wearable.internal.CapabilityInfoParcelable; +import com.google.android.gms.wearable.internal.ChannelEventParcelable; +import com.google.android.gms.wearable.internal.IWearableListener; +import com.google.android.gms.wearable.internal.MessageEventParcelable; +import com.google.android.gms.wearable.internal.NodeParcelable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class WearableLocationListener extends IWearableListener.Stub { + public static final String LOCATION_REQUESTS = "com/google/android/location/fused/wearable/LOCATION_REQUESTS"; + public static final String CAPABILITY_QUERY = "com/google/android/location/fused/wearable/CAPABILITY_QUERY"; + + private static final String TAG = "GmsWearLocListener"; + + private WearableLocationService locationService; + + public WearableLocationListener(WearableLocationService locationService) { + this.locationService = locationService; + } + + @Override + public void onDataChanged(DataHolder data) throws RemoteException { + } + + @Override + public void onMessageReceived(MessageEventParcelable messageEvent) throws RemoteException { + if (messageEvent.getPath().equals(LOCATION_REQUESTS)) { + //DataMap dataMap = DataMap.fromByteArray(messageEvent.getData()); + //locationService.onLocationRequests(messageEvent.getSourceNodeId(), parseLocationRequestList(dataMap, locationService), dataMap.getBoolean("TRIGGER_UPDATE", false)); + } else if (messageEvent.getPath().equals(CAPABILITY_QUERY)) { + locationService.onCapabilityQuery(messageEvent.getSourceNodeId()); + } + } + + @Override + public void onPeerConnected(NodeParcelable node) throws RemoteException { + } + + @Override + public void onPeerDisconnected(NodeParcelable node) throws RemoteException { + locationService.onLocationRequests(node.getId(), Collections.emptyList(), false); + } + + @Override + public void onConnectedNodes(List nodes) throws RemoteException { + } + + @Override + public void onNotificationReceived(AncsNotificationParcelable notification) throws RemoteException { + } + + @Override + public void onChannelEvent(ChannelEventParcelable channelEvent) throws RemoteException { + } + + @Override + public void onConnectedCapabilityChanged(CapabilityInfoParcelable capabilityInfo) throws RemoteException { + } + + @Override + public void onEntityUpdate(AmsEntityUpdateParcelable update) throws RemoteException { + } + + /*public static Collection parseLocationRequestList(DataMap dataMap, Context context) { + if (!dataMap.containsKey("REQUEST_LIST")) { + Log.w(TAG, "malformed DataMap: missing key REQUEST_LIST"); + return Collections.emptyList(); + } + List requestMapList = dataMap.getDataMapArrayList("REQUEST_LIST"); + List locationRequests = new ArrayList(); + for (DataMap map : requestMapList) { + locationRequests.add(parseLocationRequest(map, context)); + } + return locationRequests; + } + + private static LocationRequestInternal parseLocationRequest(DataMap dataMap, Context context) { + LocationRequestInternal request = new LocationRequestInternal(); + request.triggerUpdate = true; + request.request = new LocationRequest(); + request.clients = Collections.emptyList(); + + if (dataMap.containsKey("PRIORITY")) + request.request.setPriority(dataMap.getInt("PRIORITY", 0)); + if (dataMap.containsKey("INTERVAL_MS")) + request.request.setInterval(dataMap.getLong("INTERVAL_MS", 0)); + if (dataMap.containsKey("FASTEST_INTERVAL_MS")) + request.request.setFastestInterval(dataMap.getLong("FASTEST_INTERVAL_MS", 0)); + //if (dataMap.containsKey("MAX_WAIT_TIME_MS")) + if (dataMap.containsKey("SMALLEST_DISPLACEMENT_METERS")) + request.request.setSmallestDisplacement(dataMap.getFloat("SMALLEST_DISPLACEMENT_METERS", 0)); + if (dataMap.containsKey("NUM_UPDATES")) + request.request.setNumUpdates(dataMap.getInt("NUM_UPDATES", 0)); + if (dataMap.containsKey("EXPIRATION_DURATION_MS")) + request.request.setExpirationDuration(dataMap.getLong("EXPIRATION_DURATION_MS", 0)); + if (dataMap.containsKey("TAG")) + request.tag = dataMap.getString("TAG"); + if (dataMap.containsKey("CLIENTS_PACKAGE_ARRAY")) { + String[] packages = dataMap.getStringArray("CLIENTS_PACKAGE_ARRAY"); + if (packages != null) { + request.clients = new ArrayList(); + for (String packageName : packages) { + request.clients.add(generateClientIdentity(packageName, context)); + } + } + } + + return request; + } + + private static ClientIdentity generateClientIdentity(String packageName, Context context) { + return null; + try { + return new ClientIdentity(context.getPackageManager().getApplicationInfo(packageName, 0).uid, packageName); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Unknown client identity: " + packageName, e); + return new ClientIdentity(context.getApplicationInfo().uid, context.getPackageName()); + } + }*/ +} diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/WearableLocationService.java b/play-services-core/src/main/java/org/microg/gms/wearable/WearableLocationService.java new file mode 100644 index 00000000..0b194425 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/wearable/WearableLocationService.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2016 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.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.support.annotation.Nullable; + +import com.google.android.gms.location.internal.LocationRequestInternal; + +import java.util.Collection; + +public class WearableLocationService extends Service { + // TODO: Implement and use WearableListenerService + private static final String TAG = "GmsWearLocSvc"; + + private WearableLocationListener listener; + + @Override + public void onCreate() { + listener = new WearableLocationListener(this); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + if (intent.getAction().equals("com.google.android.gms.wearable.BIND_LISTENER")) { + return listener.asBinder(); + } + return null; + } + + public void onLocationRequests(String nodeId, Collection requests, boolean triggerUpdate) { + + } + + public void onCapabilityQuery(String nodeId) { + + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java b/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java index 2bd3a501..14df7e53 100644 --- a/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java +++ b/play-services-core/src/main/java/org/microg/gms/wearable/WearableServiceImpl.java @@ -19,24 +19,33 @@ package org.microg.gms.wearable; import android.content.Context; import android.net.Uri; import android.os.Parcel; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import com.google.android.gms.common.api.Status; +import com.google.android.gms.wearable.Asset; import com.google.android.gms.wearable.ConnectionConfiguration; import com.google.android.gms.wearable.internal.AddListenerRequest; +import com.google.android.gms.wearable.internal.AncsNotificationParcelable; import com.google.android.gms.wearable.internal.DeleteDataItemsResponse; +import com.google.android.gms.wearable.internal.GetCloudSyncSettingResponse; import com.google.android.gms.wearable.internal.GetConfigResponse; import com.google.android.gms.wearable.internal.GetConfigsResponse; import com.google.android.gms.wearable.internal.GetConnectedNodesResponse; import com.google.android.gms.wearable.internal.GetDataItemResponse; +import com.google.android.gms.wearable.internal.GetFdForAssetResponse; import com.google.android.gms.wearable.internal.GetLocalNodeResponse; +import com.google.android.gms.wearable.internal.IChannelStreamCallbacks; import com.google.android.gms.wearable.internal.IWearableCallbacks; import com.google.android.gms.wearable.internal.IWearableService; import com.google.android.gms.wearable.internal.NodeParcelable; import com.google.android.gms.wearable.internal.PutDataRequest; import com.google.android.gms.wearable.internal.PutDataResponse; import com.google.android.gms.wearable.internal.RemoveListenerRequest; +import com.google.android.gms.wearable.internal.SendMessageResponse; + +import java.io.FileNotFoundException; public class WearableServiceImpl extends IWearableService.Stub { private static final String TAG = "GmsWearSvcImpl"; @@ -51,6 +60,50 @@ public class WearableServiceImpl extends IWearableService.Stub { this.packageName = packageName; } + /* + * Config + */ + + @Override + public void putConfig(IWearableCallbacks callbacks, ConnectionConfiguration config) throws RemoteException { + wearable.createConnection(config); + callbacks.onStatus(Status.SUCCESS); + } + + @Override + public void deleteConfig(IWearableCallbacks callbacks, String name) throws RemoteException { + wearable.deleteConnection(name); + callbacks.onStatus(Status.SUCCESS); + } + + @Override + public void getConfigs(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "getConfigs"); + try { + callbacks.onGetConfigsResponse(new GetConfigsResponse(0, wearable.getConfigurations())); + } catch (Exception e) { + callbacks.onGetConfigsResponse(new GetConfigsResponse(8, new ConnectionConfiguration[0])); + } + } + + + @Override + public void enableConfig(IWearableCallbacks callbacks, String name) throws RemoteException { + Log.d(TAG, "enableConfig: " + name); + wearable.enableConnection(name); + callbacks.onStatus(Status.SUCCESS); + } + + @Override + public void disableConfig(IWearableCallbacks callbacks, String name) throws RemoteException { + Log.d(TAG, "disableConfig: " + name); + wearable.disableConnection(name); + callbacks.onStatus(Status.SUCCESS); + } + + /* + * DataItems + */ @Override public void putData(IWearableCallbacks callbacks, PutDataRequest request) throws RemoteException { @@ -74,32 +127,88 @@ public class WearableServiceImpl extends IWearableService.Stub { @Override public void getDataItems(IWearableCallbacks callbacks) throws RemoteException { Log.d(TAG, "getDataItems: " + callbacks); - callbacks.onDataHolder(wearable.getDataItemsAsHolder(packageName)); + callbacks.onDataItemChanged(wearable.getDataItemsAsHolder(packageName)); } @Override - public void sendMessage(IWearableCallbacks callbacks, String targetNodeId, String path, byte[] data) throws RemoteException { - Log.d(TAG, "sendMessage: " + targetNodeId + " / " + path); - wearable.sendMessage(targetNodeId, path, data); + public void getDataItemsByUri(IWearableCallbacks callbacks, Uri uri) throws RemoteException { + getDataItemsByUriWithFilter(callbacks, uri, 0); } @Override - public void getDataItemsByUri(IWearableCallbacks callbacks, Uri uri, int i) throws RemoteException { + public void getDataItemsByUriWithFilter(IWearableCallbacks callbacks, Uri uri, int typeFilter) throws RemoteException { Log.d(TAG, "getDataItemsByUri: " + uri); - callbacks.onDataHolder(wearable.getDataItemsByUriAsHolder(uri, packageName)); + callbacks.onDataItemChanged(wearable.getDataItemsByUriAsHolder(uri, packageName)); } @Override public void deleteDataItems(IWearableCallbacks callbacks, Uri uri) throws RemoteException { + deleteDataItemsWithFilter(callbacks, uri, 0); + } + + @Override + public void deleteDataItemsWithFilter(IWearableCallbacks callbacks, Uri uri, int typeFilter) throws RemoteException { Log.d(TAG, "deleteDataItems: " + uri); callbacks.onDeleteDataItemsResponse(new DeleteDataItemsResponse(0, wearable.deleteDataItems(uri, packageName))); } + @Override + public void sendMessage(IWearableCallbacks callbacks, String targetNodeId, String path, byte[] data) throws RemoteException { + Log.d(TAG, "sendMessage: " + targetNodeId + " / " + path); + SendMessageResponse sendMessageResponse = new SendMessageResponse(); + try { + sendMessageResponse.resultId = wearable.sendMessage(packageName, targetNodeId, path, data); + if (sendMessageResponse.resultId == -1) { + sendMessageResponse.statusCode = 4000; + } + } catch (Exception e) { + sendMessageResponse.statusCode = 8; + } + callbacks.onSendMessageResponse(sendMessageResponse); + } + + @Override + public void getFdForAsset(IWearableCallbacks callbacks, Asset asset) throws RemoteException { + Log.d(TAG, "getFdForAsset " + asset); + // TODO: Access control + try { + callbacks.onGetFdForAssetResponse(new GetFdForAssetResponse(0, ParcelFileDescriptor.open(wearable.createAssetFile(asset.getDigest()), ParcelFileDescriptor.MODE_READ_ONLY))); + } catch (FileNotFoundException e) { + callbacks.onGetFdForAssetResponse(new GetFdForAssetResponse(8, null)); + } + } + @Override public void optInCloudSync(IWearableCallbacks callbacks, boolean enable) throws RemoteException { callbacks.onStatus(Status.SUCCESS); } + @Override + @Deprecated + public void getCloudSyncOptInDone(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: getCloudSyncOptInDone"); + } + + @Override + public void setCloudSyncSetting(IWearableCallbacks callbacks, boolean enable) throws RemoteException { + Log.d(TAG, "unimplemented Method: setCloudSyncSetting"); + } + + @Override + public void getCloudSyncSetting(IWearableCallbacks callbacks) throws RemoteException { + callbacks.onGetCloudSyncSettingResponse(new GetCloudSyncSettingResponse(0, false)); + } + + @Override + public void getCloudSyncOptInStatus(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: getCloudSyncOptInStatus"); + } + + @Override + public void sendRemoteCommand(IWearableCallbacks callbacks, byte b) throws RemoteException { + Log.d(TAG, "unimplemented Method: sendRemoteCommand: " + b); + } + @Override public void getLocalNode(IWearableCallbacks callbacks) throws RemoteException { try { @@ -111,13 +220,35 @@ public class WearableServiceImpl extends IWearableService.Stub { @Override public void getConnectedNodes(IWearableCallbacks callbacks) throws RemoteException { - Log.d(TAG, "getConnectedNodes"); callbacks.onGetConnectedNodesResponse(new GetConnectedNodesResponse(0, wearable.getConnectedNodesParcelableList())); } + /* + * Capability + */ + + @Override + public void getConnectedCapability(IWearableCallbacks callbacks, String s1, int i) throws RemoteException { + Log.d(TAG, "unimplemented Method: getConnectedCapability " + s1 + ", " + i); + } + + @Override + public void getConnectedCapaibilties(IWearableCallbacks callbacks, int i) throws RemoteException { + Log.d(TAG, "unimplemented Method: getConnectedCapaibilties: " + i); + } + + @Override + public void addLocalCapability(IWearableCallbacks callbacks, String cap) throws RemoteException { + Log.d(TAG, "unimplemented Method: addLocalCapability: " + cap); + } + + @Override + public void removeLocalCapability(IWearableCallbacks callbacks, String cap) throws RemoteException { + Log.d(TAG, "unimplemented Method: removeLocalCapability: " + cap); + } + @Override public void addListener(IWearableCallbacks callbacks, AddListenerRequest request) throws RemoteException { - Log.d(TAG, "addListener[nyp]: " + request); if (request.listener != null) { wearable.addListener(packageName, request.listener); } @@ -126,25 +257,112 @@ public class WearableServiceImpl extends IWearableService.Stub { @Override public void removeListener(IWearableCallbacks callbacks, RemoveListenerRequest request) throws RemoteException { - Log.d(TAG, "removeListener[nyp]: " + request); wearable.removeListener(request.listener); callbacks.onStatus(Status.SUCCESS); } @Override - public void putConfig(IWearableCallbacks callbacks, ConnectionConfiguration config) throws RemoteException { - wearable.createConnection(config); - callbacks.onStatus(Status.SUCCESS); + public void getStrorageInformation(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: getStrorageInformation"); } @Override - public void deleteConfig(IWearableCallbacks callbacks, String name) throws RemoteException { - wearable.deleteConnection(name); - callbacks.onStatus(Status.SUCCESS); + public void clearStorage(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: clearStorage"); } @Override - public void getConfig(IWearableCallbacks callbacks) throws RemoteException { + public void endCall(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: endCall"); + } + + @Override + public void acceptRingingCall(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: acceptRingingCall"); + } + + @Override + public void silenceRinger(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: silenceRinger"); + } + + /* + * Apple Notification Center Service + */ + + @Override + public void injectAncsNotificationForTesting(IWearableCallbacks callbacks, AncsNotificationParcelable notification) throws RemoteException { + Log.d(TAG, "unimplemented Method: injectAncsNotificationForTesting: " + notification); + } + + @Override + public void doAncsPositiveAction(IWearableCallbacks callbacks, int i) throws RemoteException { + Log.d(TAG, "unimplemented Method: doAncsPositiveAction: " + i); + } + + @Override + public void doAncsNegativeAction(IWearableCallbacks callbacks, int i) throws RemoteException { + Log.d(TAG, "unimplemented Method: doAncsNegativeAction: " + i); + } + + @Override + public void openChannel(IWearableCallbacks callbacks, String s1, String s2) throws RemoteException { + Log.d(TAG, "unimplemented Method: openChannel; " + s1 + ", " + s2); + } + + /* + * Channels + */ + + @Override + public void closeChannel(IWearableCallbacks callbacks, String s) throws RemoteException { + Log.d(TAG, "unimplemented Method: closeChannel: " + s); + } + + @Override + public void closeChannelWithError(IWearableCallbacks callbacks, String s, int errorCode) throws RemoteException { + Log.d(TAG, "unimplemented Method: closeChannelWithError:" + s + ", " + errorCode); + + } + + @Override + public void getChannelInputStream(IWearableCallbacks callbacks, IChannelStreamCallbacks channelCallbacks, String s) throws RemoteException { + Log.d(TAG, "unimplemented Method: getChannelInputStream: " + s); + } + + @Override + public void getChannelOutputStream(IWearableCallbacks callbacks, IChannelStreamCallbacks channelCallbacks, String s) throws RemoteException { + Log.d(TAG, "unimplemented Method: getChannelOutputStream: " + s); + } + + @Override + public void writeChannelInputToFd(IWearableCallbacks callbacks, String s, ParcelFileDescriptor fd) throws RemoteException { + Log.d(TAG, "unimplemented Method: writeChannelInputToFd: " + s); + } + + @Override + public void readChannelOutputFromFd(IWearableCallbacks callbacks, String s, ParcelFileDescriptor fd, long l1, long l2) throws RemoteException { + Log.d(TAG, "unimplemented Method: readChannelOutputFromFd: " + s + ", " + l1 + ", " + l2); + } + + @Override + public void syncWifiCredentials(IWearableCallbacks callbacks) throws RemoteException { + Log.d(TAG, "unimplemented Method: syncWifiCredentials"); + } + + /* + * Connection deprecated + */ + + @Override + @Deprecated + public void putConnection(IWearableCallbacks callbacks, ConnectionConfiguration config) throws RemoteException { + Log.d(TAG, "unimplemented Method: putConnection"); + } + + @Override + @Deprecated + public void getConnection(IWearableCallbacks callbacks) throws RemoteException { Log.d(TAG, "getConfig"); ConnectionConfiguration[] configurations = wearable.getConfigurations(); if (configurations == null || configurations.length == 0) { @@ -155,28 +373,21 @@ public class WearableServiceImpl extends IWearableService.Stub { } @Override - public void getConfigs(IWearableCallbacks callbacks) throws RemoteException { - Log.d(TAG, "getConfigs"); - try { - callbacks.onGetConfigsResponse(new GetConfigsResponse(0, wearable.getConfigurations())); - } catch (Exception e) { - callbacks.onGetConfigsResponse(new GetConfigsResponse(8, new ConnectionConfiguration[0])); + @Deprecated + public void enableConnection(IWearableCallbacks callbacks) throws RemoteException { + ConnectionConfiguration[] configurations = wearable.getConfigurations(); + if (configurations.length > 0) { + enableConfig(callbacks, configurations[0].name); } } - @Override - public void enableConnection(IWearableCallbacks callbacks, String name) throws RemoteException { - Log.d(TAG, "enableConnection: " + name); - wearable.enableConnection(name); - callbacks.onStatus(Status.SUCCESS); - } - - @Override - public void disableConnection(IWearableCallbacks callbacks, String name) throws RemoteException { - Log.d(TAG, "disableConnection: " + name); - wearable.disableConnection(name); - callbacks.onStatus(Status.SUCCESS); + @Deprecated + public void disableConnection(IWearableCallbacks callbacks) throws RemoteException { + ConnectionConfiguration[] configurations = wearable.getConfigurations(); + if (configurations.length > 0) { + disableConfig(callbacks, configurations[0].name); + } } @Override