@@ -6,8 +6,11 @@ import com.google.android.gms.wearable.CapabilityClient
66import com.google.android.gms.wearable.Node
77import com.google.android.gms.wearable.Wearable
88import kotlinx.coroutines.Dispatchers
9+ import kotlinx.coroutines.TimeoutCancellationException
910import kotlinx.coroutines.tasks.await
1011import kotlinx.coroutines.withContext
12+ import kotlinx.coroutines.withTimeout
13+ import kotlinx.coroutines.withTimeoutOrNull
1114import java.io.InputStream
1215import java.net.URLEncoder
1316
@@ -38,26 +41,26 @@ class WatchRepository(context: Context) {
3841 suspend fun fetchLibrary (): Result <LibrarySnapshot > = withContext(Dispatchers .IO ) {
3942 val node = bestNode() ? : return @withContext Result .NoWatch
4043 runCatching {
41- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_LIST , ByteArray (0 )).await( )
44+ val bytes = sendRequest(node, WearProtocol .PATH_LIST , ByteArray (0 ))
4245 Result .Ok (LibraryListJson .decode(bytes))
43- }.getOrElse { Result . Error ( it.message ? : " Failed to fetch library" ) }
46+ }.getOrElse { it.toFetchResult( " Failed to fetch library" ) }
4447 }
4548
4649 suspend fun fetchStats (): Result <StatsSummary > = withContext(Dispatchers .IO ) {
4750 val node = bestNode() ? : return @withContext Result .NoWatch
4851 runCatching {
49- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_STATS , ByteArray (0 )).await( )
52+ val bytes = sendRequest(node, WearProtocol .PATH_STATS , ByteArray (0 ))
5053 Result .Ok (StatsJson .decode(bytes))
51- }.getOrElse { Result . Error ( it.message ? : " Failed to fetch stats" ) }
54+ }.getOrElse { it.toFetchResult( " Failed to fetch stats" ) }
5255 }
5356
5457 suspend fun fetchSettings (): Result <SettingsSnapshot > = withContext(Dispatchers .IO ) {
5558 val node = bestNode() ? : return @withContext Result .NoWatch
5659 runCatching {
57- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_SETTINGS_GET , ByteArray (0 )).await( )
60+ val bytes = sendRequest(node, WearProtocol .PATH_SETTINGS_GET , ByteArray (0 ))
5861 val snap = SettingsJson .decode(bytes) ? : return @runCatching Result .Error (" Empty settings response" )
5962 Result .Ok (snap)
60- }.getOrElse { Result . Error ( it.message ? : " Failed to fetch settings" ) }
63+ }.getOrElse { it.toFetchResult( " Failed to fetch settings" ) }
6164 }
6265
6366 /* *
@@ -69,19 +72,19 @@ class WatchRepository(context: Context) {
6972 val node = bestNode() ? : return @withContext Result .NoWatch
7073 runCatching {
7174 val payload = SettingsJson .encodeSetRequest(key, value)
72- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_SETTINGS_SET , payload).await( )
75+ val bytes = sendRequest(node, WearProtocol .PATH_SETTINGS_SET , payload)
7376 val snap = SettingsJson .decode(bytes) ? : return @runCatching Result .Error (" Empty settings response" )
7477 Result .Ok (snap)
75- }.getOrElse { Result . Error ( it.message ? : " Failed to update setting" ) }
78+ }.getOrElse { it.toActionError( " Failed to update setting" ) }
7679 }
7780
7881 suspend fun deleteBook (id : String ): Result <LibrarySnapshot > = withContext(Dispatchers .IO ) {
7982 val node = bestNode() ? : return @withContext Result .NoWatch
8083 runCatching {
8184 val payload = org.json.JSONObject ().put(" id" , id).toString().toByteArray(Charsets .UTF_8 )
82- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_DELETE , payload).await( )
85+ val bytes = sendRequest(node, WearProtocol .PATH_DELETE , payload)
8386 Result .Ok (LibraryListJson .decode(bytes))
84- }.getOrElse { Result . Error ( it.message ? : " Delete failed" ) }
87+ }.getOrElse { it.toActionError( " Delete failed" ) }
8588 }
8689
8790 suspend fun uploadBook (
@@ -170,19 +173,19 @@ class WatchRepository(context: Context) {
170173 val node = bestNode() ? : return @withContext Result .NoWatch
171174 runCatching {
172175 val payload = """ {"name":${jsonString(name)} }""" .toByteArray(Charsets .UTF_8 )
173- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_MKDIR , payload).await( )
176+ val bytes = sendRequest(node, WearProtocol .PATH_MKDIR , payload)
174177 Result .Ok (LibraryListJson .decode(bytes))
175- }.getOrElse { Result . Error ( it.message ? : " mkdir failed" ) }
178+ }.getOrElse { it.toActionError( " mkdir failed" ) }
176179 }
177180
178181 suspend fun renameFolder (oldName : String , newName : String ): Result <LibrarySnapshot > = withContext(Dispatchers .IO ) {
179182 val node = bestNode() ? : return @withContext Result .NoWatch
180183 runCatching {
181184 val payload = """ {"from":${jsonString(oldName)} ,"to":${jsonString(newName)} }"""
182185 .toByteArray(Charsets .UTF_8 )
183- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_RENAME , payload).await( )
186+ val bytes = sendRequest(node, WearProtocol .PATH_RENAME , payload)
184187 Result .Ok (LibraryListJson .decode(bytes))
185- }.getOrElse { Result . Error ( it.message ? : " rename failed" ) }
188+ }.getOrElse { it.toActionError( " rename failed" ) }
186189 }
187190
188191 suspend fun moveBook (bookId : String , targetFolder : String ): Result <LibrarySnapshot > =
@@ -191,9 +194,9 @@ class WatchRepository(context: Context) {
191194 runCatching {
192195 val payload = """ {"id":${jsonString(bookId)} ,"folder":${jsonString(targetFolder)} }"""
193196 .toByteArray(Charsets .UTF_8 )
194- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_MOVE , payload).await( )
197+ val bytes = sendRequest(node, WearProtocol .PATH_MOVE , payload)
195198 Result .Ok (LibraryListJson .decode(bytes))
196- }.getOrElse { Result . Error ( it.message ? : " move failed" ) }
199+ }.getOrElse { it.toActionError( " move failed" ) }
197200 }
198201
199202 suspend fun reorderBooks (folder : String , orderedIds : List <String >): Result <LibrarySnapshot > =
@@ -203,9 +206,9 @@ class WatchRepository(context: Context) {
203206 val order = orderedIds.joinToString(" ," , prefix = " [" , postfix = " ]" ) { jsonString(it) }
204207 val payload = """ {"folder":${jsonString(folder)} ,"order":$order }"""
205208 .toByteArray(Charsets .UTF_8 )
206- val bytes = messageClient. sendRequest(node.id , WearProtocol .PATH_REORDER , payload).await( )
209+ val bytes = sendRequest(node, WearProtocol .PATH_REORDER , payload)
207210 Result .Ok (LibraryListJson .decode(bytes))
208- }.getOrElse { Result . Error ( it.message ? : " reorder failed" ) }
211+ }.getOrElse { it.toActionError( " reorder failed" ) }
209212 }
210213
211214 suspend fun hasReachableWatch (): Boolean = withContext(Dispatchers .IO ) {
@@ -214,20 +217,36 @@ class WatchRepository(context: Context) {
214217
215218 /* * Find a connected node that has the wBooks watch app installed. */
216219 private suspend fun bestNode (): Node ? {
217- val info = runCatching {
218- capabilityClient
219- .getCapability(WBOOKS_CAPABILITY , CapabilityClient .FILTER_REACHABLE )
220- .await()
221- }.getOrNull()
220+ val info = withTimeoutOrNull(NODE_LOOKUP_TIMEOUT_MS ) {
221+ runCatching {
222+ capabilityClient
223+ .getCapability(WBOOKS_CAPABILITY , CapabilityClient .FILTER_REACHABLE )
224+ .await()
225+ }.getOrNull()
226+ }
222227 val capabilityNode = info
223228 ?.nodes
224229 ?.let { nodes -> nodes.firstOrNull { it.isNearby } ? : nodes.firstOrNull() }
225230 if (capabilityNode != null ) return capabilityNode
226231
227- val connectedNodes = runCatching { nodeClient.connectedNodes.await() }.getOrNull().orEmpty()
232+ val connectedNodes = withTimeoutOrNull(NODE_LOOKUP_TIMEOUT_MS ) {
233+ runCatching { nodeClient.connectedNodes.await() }.getOrNull().orEmpty()
234+ }.orEmpty()
228235 return connectedNodes.firstOrNull { it.isNearby } ? : connectedNodes.firstOrNull()
229236 }
230237
238+ private suspend fun sendRequest (node : Node , path : String , payload : ByteArray ): ByteArray =
239+ withTimeout(REQUEST_TIMEOUT_MS ) {
240+ messageClient.sendRequest(node.id, path, payload).await()
241+ }
242+
243+ private fun <T > Throwable.toFetchResult (fallback : String ): Result <T > =
244+ if (this is TimeoutCancellationException ) Result .NoWatch
245+ else Result .Error (message ? : fallback)
246+
247+ private fun <T > Throwable.toActionError (fallback : String ): Result <T > =
248+ Result .Error (if (this is TimeoutCancellationException ) " Watch did not respond." else message ? : fallback)
249+
231250 companion object {
232251 /* * Must match the capability the watch app advertises in res/values/wear.xml. */
233252 const val WBOOKS_CAPABILITY = " wbooks_receiver"
@@ -260,5 +279,7 @@ class WatchRepository(context: Context) {
260279 " docx" ,
261280 " odt" ,
262281 )
282+ private const val NODE_LOOKUP_TIMEOUT_MS = 5_000L
283+ private const val REQUEST_TIMEOUT_MS = 12_000L
263284 }
264285}
0 commit comments