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);
}
}