package com.crossbow.wear; import android.content.Context; import android.support.v4.util.ArrayMap; import android.util.Log; import com.android.volley.AuthFailureError; import com.android.volley.Network; import com.android.volley.NetworkResponse; import com.android.volley.NoConnectionError; import com.android.volley.Request; import com.android.volley.VolleyError; import com.crossbow.wear.core.RequestSerialUtil; import com.crossbow.wear.core.WearNetworkResponse; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.wearable.CapabilityApi; import com.google.android.gms.wearable.MessageApi; import com.google.android.gms.wearable.MessageEvent; import com.google.android.gms.wearable.Node; import com.google.android.gms.wearable.Wearable; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * Custom network that uses play services to send the requests to the handheld instead of the * http stack */ public class PlayNetwork implements Network, MessageApi.MessageListener { private GoogleApiClient googleApiClient; private final boolean serviceAvaiable; private final int playServicesAvaibleCode; private final Map<String, CountDownLatch> latchMap = Collections.synchronizedMap(new ArrayMap<String, CountDownLatch>(7)); private final Map<String, WearNetworkResponse> responsesMap = Collections.synchronizedMap(new ArrayMap<String, WearNetworkResponse>(7)); private final Set<String> inflightUuids = Collections.synchronizedSet(new HashSet<String>(7)); private Context context; public PlayNetwork(Context context) { this.context = context.getApplicationContext(); this.googleApiClient = new GoogleApiClient.Builder(context).addApi(Wearable.API).build(); playServicesAvaibleCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); serviceAvaiable = playServicesAvaibleCode == ConnectionResult.SUCCESS; } @Override public NetworkResponse performRequest(Request<?> request) throws VolleyError { if(!serviceAvaiable) { throwErrorForCode(playServicesAvaibleCode); } Log.d("Play Network", "performRequest start"); final long timeOut = request.getTimeoutMs(); ConnectionResult connectionResult = googleApiClient.blockingConnect(timeOut, TimeUnit.MILLISECONDS); if (!connectionResult.isSuccess()) { int code = connectionResult.getErrorCode(); throwErrorForCode(code); } //add the listeners for the data response Wearable.MessageApi.addListener(googleApiClient, this); //Request the nodes that can handle the request String nodeID = getBestNode(timeOut); //We are good to go //Serialize the request an send it to the phone String uuid = UUID.randomUUID().toString(); String wearMessageUrl = getDataRequestKey(uuid); Log.d("Play Network", "performRequest serialization"); byte[] data; try { data = RequestSerialUtil.serializeRequest(uuid, request); } catch (IOException e) { throw new VolleyError("cant serialize the request", e); } MessageApi.SendMessageResult result = Wearable.MessageApi.sendMessage(googleApiClient, nodeID, wearMessageUrl, data).await(timeOut, TimeUnit.MILLISECONDS); if(!result.getStatus().isSuccess()) { throw new NoConnectionError(new VolleyError("Failed to send message to device")); } inflightUuids.add(uuid); //block the thread until the data is received or times out CountDownLatch latch = new CountDownLatch(1); latchMap.put(uuid, latch); try { Log.d("Play Network", "performRequest latching"); latch.await(timeOut * 2, TimeUnit.MILLISECONDS); Log.d("Play Network", "latch released"); latchMap.remove(uuid); } catch (InterruptedException e) { Log.d("Play Network", "latch error"); throw new VolleyError("Request Timeout talking to handheld", e); } WearNetworkResponse networkResponse = responsesMap.get(uuid); responsesMap.remove(uuid); if(networkResponse == null || !networkResponse.success) { throw new VolleyError("No response in map"); } return networkResponse.getNetworkResponse(); } @Override public void onMessageReceived(MessageEvent messageEvent) { if(messageEvent.getPath().contains("crossbow_wear")) { byte[] data = messageEvent.getData(); Log.d("Play Network", "request response returned"); try { WearNetworkResponse wearNetworkResponse = WearNetworkResponse.fromByteArray(data); String uuid = wearNetworkResponse.uuid; responsesMap.put(uuid, wearNetworkResponse); if(latchMap.containsKey(uuid)) { //unlatch the correct thread Log.d("Play Network", "unlatching thread"); latchMap.get(uuid).countDown(); } } catch (Exception e) { Log.d("Play Network", "wearNetworkResponse error = " + e); } Log.d("Play Network", "request response returned"); } } private String getBestNode(long timeOut) throws VolleyError { final String nodeCompatibilty = context.getString(R.string.crossbow_compatibility); CapabilityApi.GetCapabilityResult nodes = Wearable.CapabilityApi.getCapability(googleApiClient, nodeCompatibilty, CapabilityApi.FILTER_REACHABLE).await(timeOut, TimeUnit.MILLISECONDS); String nodeID = null; Set<Node> nodeList = nodes.getCapability().getNodes(); if(nodeList.isEmpty()) { throw new NoConnectionError(new VolleyError("No nodes found to handle the request")); } //get the nearest node for(Node node : nodeList) { if(node.isNearby()) { return node.getId(); } nodeID = node.getId(); } return nodeID; } private static String getDataRequestKey(String uuid) throws AuthFailureError{ return "/crossbow_wear/" + uuid; } private static void throwErrorForCode(int code) throws VolleyError { final String message; switch (code) { case ConnectionResult.SERVICE_MISSING: message = "Play Services is missing"; break; case ConnectionResult.SERVICE_UPDATING: message = "Play Services is updating"; break; case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED: message = "Play Services is running the wrong version"; break; case ConnectionResult.SERVICE_DISABLED: message = "Play Services is disabled"; break; case ConnectionResult.SERVICE_INVALID: message = "Play Services is invalid"; break; default: message = "Connection to play services failed"; } throw new VolleyError(message); } }