blob: 05a631e0c019311f19f00451ee711efe419d18b8 [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 android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
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 the host list. */
public class HostListLoader {
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[] hosts);
void onError(Error error);
}
/** 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 HostListLoader() {
// Thread responsible for downloading the host list.
mMainThread = new Handler(Looper.getMainLooper());
}
private void initNetworkThread() {
if (mNetworkThread == null) {
HandlerThread thread = new HandlerThread("network");
thread.start();
mNetworkThread = new Handler(thread.getLooper());
}
}
/**
* 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(String authToken, Callback callback) {
initNetworkThread();
final String authTokenFinal = authToken;
final Callback callbackFinal = callback;
mNetworkThread.post(new Runnable() {
@Override
public void run() {
doRetrieveHostList(authTokenFinal, callbackFinal);
}
});
}
private void doRetrieveHostList(String authToken, Callback callback) {
HttpURLConnection link = null;
String response = null;
try {
link = (HttpURLConnection) new URL(HOST_LIST_PATH).openConnection();
link.setRequestProperty("Authorization", "OAuth " + authToken);
// Listen for the server to respond.
int status = link.getResponseCode();
switch (status) {
case HttpURLConnection.HTTP_OK: // 200
break;
case HttpURLConnection.HTTP_UNAUTHORIZED: // 401
postError(callback, Error.AUTH_FAILED);
return;
case HttpURLConnection.HTTP_BAD_GATEWAY: // 502
case HttpURLConnection.HTTP_UNAVAILABLE: // 503
postError(callback, Error.SERVICE_UNAVAILABLE);
return;
default:
postError(callback, Error.UNKNOWN);
return;
}
StringBuilder responseBuilder = new StringBuilder();
Scanner incoming = new Scanner(link.getInputStream());
Log.i("auth", "Successfully authenticated to directory server");
while (incoming.hasNext()) {
responseBuilder.append(incoming.nextLine());
}
response = String.valueOf(responseBuilder);
incoming.close();
} catch (MalformedURLException ex) {
// This should never happen.
throw new RuntimeException("Unexpected error while fetching host list: " + ex);
} catch (IOException ex) {
postError(callback, Error.NETWORK_ERROR);
return;
} finally {
if (link != null) {
link.disconnect();
}
}
// Parse directory response.
ArrayList<HostInfo> hostList = new ArrayList<HostInfo>();
try {
JSONObject data = new JSONObject(response).getJSONObject("data");
if (data.has("items")) {
JSONArray hostsJson = data.getJSONArray("items");
Log.i("hostlist", "Received host listing from directory server");
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) {
Log.e("hostlist", "Error parsing host list response: ", ex);
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);
}
});
}
/** 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);
}
}