blob: 4cd703be82efd4e71601d1ca6404ebcfaed8490a [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chromoting;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import org.chromium.base.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Locale;
import java.util.Scanner;
/** Helper for fetching and modifying the host list. */
public class HostListManager {
public enum Error {
AUTH_FAILED,
NETWORK_ERROR,
SERVICE_UNAVAILABLE,
UNEXPECTED_RESPONSE,
UNKNOWN,
}
/** Callback for receiving the host list, or getting notified of an error. */
public interface Callback {
void onHostListReceived(HostInfo[] response);
void onHostUpdated();
void onHostDeleted();
void onError(Error error);
}
/**
* Represents a response from the directory server.
* If the request failed, |error| will not be null and |body| will be null.
* If the request succeeds, |error| will be null and |body| will not be null.
**/
private static class Response {
public final Error error;
public final String body;
public Response(Error error, String body) {
this.error = error;
this.body = body;
}
}
private static final String TAG = "Chromoting";
/** Path from which to download a user's host list JSON object. */
private static final String HOST_LIST_PATH =
"https://www.googleapis.com/chromoting/v1/@me/hosts";
/** Callback handler to be used for network operations. */
private Handler mNetworkThread;
/** Handler for main thread. */
private Handler mMainThread;
public HostListManager() {
// Thread responsible for downloading the host list.
mMainThread = new Handler(Looper.getMainLooper());
}
private void runOnNetworkThread(Runnable runnable) {
if (mNetworkThread == null) {
HandlerThread thread = new HandlerThread("network");
thread.start();
mNetworkThread = new Handler(thread.getLooper());
}
mNetworkThread.post(runnable);
}
/**
* Causes the host list to be fetched on a background thread. This should be called on the
* main thread, and callbacks will also be invoked on the main thread. On success,
* callback.onHostListReceived() will be called, otherwise callback.onError() will be called
* with an error-code describing the failure.
*/
public void retrieveHostList(final String authToken, final Callback callback) {
runOnNetworkThread(new Runnable() {
@Override
public void run() {
doRetrieveHostList(authToken, callback);
}
});
}
private void doRetrieveHostList(String authToken, Callback callback) {
Response response = sendRequest(authToken, HOST_LIST_PATH, "GET", null, null);
if (response.error != null) {
postError(callback, response.error);
return;
}
// Parse directory response.
ArrayList<HostInfo> hostList = new ArrayList<HostInfo>();
try {
JSONObject data = new JSONObject(response.body).getJSONObject("data");
if (data.has("items")) {
JSONArray hostsJson = data.getJSONArray("items");
int index = 0;
while (!hostsJson.isNull(index)) {
JSONObject hostJson = hostsJson.getJSONObject(index);
// If a host is only recently registered, it may be missing some of the keys
// below. It should still be visible in the list, even though a connection
// attempt will fail because of the missing keys. The failed attempt will
// trigger reloading of the host-list, by which time the keys will hopefully be
// present, and the retried connection can succeed.
HostInfo host = HostInfo.create(hostJson);
hostList.add(host);
++index;
}
}
} catch (JSONException ex) {
// Logging the exception stack trace may be too spammy.
Log.e(TAG, "Error parsing host list response: %s", ex.getMessage());
postError(callback, Error.UNEXPECTED_RESPONSE);
return;
}
sortHosts(hostList);
final Callback callbackFinal = callback;
final HostInfo[] hosts = hostList.toArray(new HostInfo[hostList.size()]);
mMainThread.post(new Runnable() {
@Override
public void run() {
callbackFinal.onHostListReceived(hosts);
}
});
}
/**
* Updates a host on the background thread. On success, callback.onHostUpdated() will be called,
* otherwise callback.onError() will be called with an error-code describing the failure.
*/
public void putHost(final String authToken, final String hostId, final String hostName,
final String publicKey, final Callback callback) {
runOnNetworkThread(new Runnable() {
@Override
public void run() {
doPutHost(authToken, hostId, hostName, publicKey, callback);
}
});
}
private void doPutHost(String authToken, String hostId, String hostName, String publicKey,
final Callback callback) {
String requestJson;
try {
JSONObject data = new JSONObject();
data.put("hostId", hostId);
data.put("hostName", hostName);
data.put("publicKey", publicKey);
JSONObject request = new JSONObject();
request.put("data", data);
requestJson = request.toString();
} catch (JSONException ex) {
Log.e(TAG, "Error creating put host JSON string: %s", ex.getMessage());
postError(callback, Error.UNKNOWN);
return;
}
Response response = sendRequest(authToken, HOST_LIST_PATH + '/' + hostId, "PUT",
"application/json", requestJson);
if (response.error != null) {
postError(callback, response.error);
} else {
mMainThread.post(new Runnable() {
@Override
public void run() {
callback.onHostUpdated();
}
});
}
}
/**
* Deletes a host on the background thread. On success, callback.onHostUpdated() will be called,
* otherwise callback.onError() will be called with an error-code describing the failure.
*/
public void deleteHost(final String authToken, final String hostId,
final Callback callback) {
runOnNetworkThread(new Runnable() {
@Override
public void run() {
doDeleteHost(authToken, hostId, callback);
}
});
}
private void doDeleteHost(String authToken, String hostId, final Callback callback) {
Response response = sendRequest(authToken, HOST_LIST_PATH + '/' + hostId, "DELETE",
null, null);
if (response.error != null) {
postError(callback, response.error);
} else {
mMainThread.post(new Runnable() {
@Override
public void run() {
callback.onHostDeleted();
}
});
}
}
/** Posts error to callback on main thread. */
private void postError(Callback callback, Error error) {
final Callback callbackFinal = callback;
final Error errorFinal = error;
mMainThread.post(new Runnable() {
@Override
public void run() {
callbackFinal.onError(errorFinal);
}
});
}
private static void sortHosts(ArrayList<HostInfo> hosts) {
Comparator<HostInfo> hostComparator = new Comparator<HostInfo>() {
@Override
public int compare(HostInfo a, HostInfo b) {
if (a.isOnline != b.isOnline) {
return a.isOnline ? -1 : 1;
}
String aName = a.name.toUpperCase(Locale.getDefault());
String bName = b.name.toUpperCase(Locale.getDefault());
return aName.compareTo(bName);
}
};
Collections.sort(hosts, hostComparator);
}
/**
* Sends request to the url and returns the response.
* @param authToken auth token
* @param url the URL to send the request
* @param method /GET/POST/PUT/DELETE/etc.
* @param requestContentType The content type of the request body. This can be null.
* @param requestBody This can be null.
* @return The response.
*/
private static Response sendRequest(String authToken, String url, String method,
String requestContentType, String requestBody) {
HttpURLConnection link = null;
Error error = null;
try {
link = (HttpURLConnection) new URL(url).openConnection();
link.setRequestMethod(method);
link.setRequestProperty("Authorization", "OAuth " + authToken);
if (requestContentType != null) {
link.setRequestProperty("Content-Type", requestContentType);
}
if (requestBody != null) {
byte[] requestBytes = requestBody.getBytes("UTF-8");
OutputStream outStream = link.getOutputStream();
outStream.write(requestBytes);
outStream.close();
}
// Listen for the server to respond.
int status = link.getResponseCode();
// TODO(yuweih): Turn this switch statement into range testing. e.g. 200-299 = OK.
switch (status) {
case HttpURLConnection.HTTP_OK: // 200
case HttpURLConnection.HTTP_NO_CONTENT: // 204
break;
case HttpURLConnection.HTTP_UNAUTHORIZED: // 401
error = Error.AUTH_FAILED;
break;
case HttpURLConnection.HTTP_BAD_GATEWAY: // 502
case HttpURLConnection.HTTP_UNAVAILABLE: // 503
error = Error.SERVICE_UNAVAILABLE;
break;
default:
error = Error.UNKNOWN;
}
if (error != null) {
return new Response(error, null);
}
StringBuilder responseBuilder = new StringBuilder();
Scanner incoming = new Scanner(link.getInputStream());
while (incoming.hasNext()) {
responseBuilder.append(incoming.nextLine());
}
incoming.close();
return new Response(null, responseBuilder.toString());
} catch (MalformedURLException ex) {
// This should never happen.
throw new RuntimeException("Unexpected error while fetching host list: ", ex);
} catch (IOException ex) {
return new Response(Error.NETWORK_ERROR, null);
} finally {
if (link != null) {
link.disconnect();
}
}
}
}