1eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch// Copyright 2014 The Chromium Authors. All rights reserved. 2eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch// Use of this source code is governed by a BSD-style license that can be 3eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch// found in the LICENSE file. 4eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 5eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochpackage org.chromium.chromoting; 6eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 7eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport android.os.Handler; 8eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport android.os.HandlerThread; 9eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport android.os.Looper; 10eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport android.util.Log; 11eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 12eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport org.chromium.chromoting.jni.JniInterface; 13eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport org.json.JSONArray; 14eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport org.json.JSONException; 15eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport org.json.JSONObject; 16eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 17eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.io.IOException; 18eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.net.HttpURLConnection; 19eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.net.MalformedURLException; 20eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.net.URL; 21eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.ArrayList; 22eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.Collections; 23eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.Comparator; 24eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.Locale; 25eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.Scanner; 26eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 27eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch/** Helper for fetching the host list. */ 28eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochpublic class HostListLoader { 29eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch public enum Error { 30eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch AUTH_FAILED, 31eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch NETWORK_ERROR, 32eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch SERVICE_UNAVAILABLE, 33eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch UNEXPECTED_RESPONSE, 34eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch UNKNOWN, 35eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 36eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 37eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch /** Callback for receiving the host list, or getting notified of an error. */ 38eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch public interface Callback { 39eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch void onHostListReceived(HostInfo[] hosts); 40eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch void onError(Error error); 41eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 42eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 43eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch /** Path from which to download a user's host list JSON object. */ 44eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch private static final String HOST_LIST_PATH = 45eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch "https://www.googleapis.com/chromoting/v1/@me/hosts?key="; 46eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 47eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch /** Callback handler to be used for network operations. */ 48eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch private Handler mNetworkThread; 49eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 50eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch /** Handler for main thread. */ 51eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch private Handler mMainThread; 52eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 53eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch public HostListLoader() { 54eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // Thread responsible for downloading the host list. 55eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 56eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch mMainThread = new Handler(Looper.getMainLooper()); 57eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 58eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 59eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch private void initNetworkThread() { 60eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch if (mNetworkThread == null) { 61eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch HandlerThread thread = new HandlerThread("network"); 62eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch thread.start(); 63eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch mNetworkThread = new Handler(thread.getLooper()); 64eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 65eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 66eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 67eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch /** 68eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch * Causes the host list to be fetched on a background thread. This should be called on the 69eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch * main thread, and callbacks will also be invoked on the main thread. On success, 70eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch * callback.onHostListReceived() will be called, otherwise callback.onError() will be called 71eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch * with an error-code describing the failure. 72eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch */ 73eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch public void retrieveHostList(String authToken, Callback callback) { 74eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch initNetworkThread(); 75eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch final String authTokenFinal = authToken; 76eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch final Callback callbackFinal = callback; 77eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch mNetworkThread.post(new Runnable() { 78eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch @Override 79eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch public void run() { 80eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch doRetrieveHostList(authTokenFinal, callbackFinal); 81eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 82eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch }); 83eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 84eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 85eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch private void doRetrieveHostList(String authToken, Callback callback) { 86eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch HttpURLConnection link = null; 87eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch String response = null; 88eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch try { 89eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch link = (HttpURLConnection) 90eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch new URL(HOST_LIST_PATH + JniInterface.nativeGetApiKey()).openConnection(); 91eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch link.addRequestProperty("client_id", JniInterface.nativeGetClientId()); 92eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch link.addRequestProperty("client_secret", JniInterface.nativeGetClientSecret()); 93eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch link.setRequestProperty("Authorization", "OAuth " + authToken); 94eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 95eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // Listen for the server to respond. 96eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch int status = link.getResponseCode(); 97eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch switch (status) { 98eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch case HttpURLConnection.HTTP_OK: // 200 99eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch break; 100eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch case HttpURLConnection.HTTP_UNAUTHORIZED: // 401 101eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch postError(callback, Error.AUTH_FAILED); 102eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch return; 103eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch case HttpURLConnection.HTTP_BAD_GATEWAY: // 502 104eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch case HttpURLConnection.HTTP_UNAVAILABLE: // 503 105eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch postError(callback, Error.SERVICE_UNAVAILABLE); 106eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch return; 107eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch default: 108eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch postError(callback, Error.UNKNOWN); 109eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch return; 110eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 111eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 112eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch StringBuilder responseBuilder = new StringBuilder(); 113eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch Scanner incoming = new Scanner(link.getInputStream()); 114eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch Log.i("auth", "Successfully authenticated to directory server"); 115eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch while (incoming.hasNext()) { 116eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch responseBuilder.append(incoming.nextLine()); 117eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 118eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch response = String.valueOf(responseBuilder); 119eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch incoming.close(); 120eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } catch (MalformedURLException ex) { 121eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // This should never happen. 122eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch throw new RuntimeException("Unexpected error while fetching host list: " + ex); 123eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } catch (IOException ex) { 124eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch postError(callback, Error.NETWORK_ERROR); 125eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch return; 126eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } finally { 127eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch if (link != null) { 128eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch link.disconnect(); 129eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 130eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 131eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 132eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // Parse directory response. 133eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch ArrayList<HostInfo> hostList = new ArrayList<HostInfo>(); 134eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch try { 135eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch JSONObject data = new JSONObject(response).getJSONObject("data"); 136eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch if (data.has("items")) { 137eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch JSONArray hostsJson = data.getJSONArray("items"); 138eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch Log.i("hostlist", "Received host listing from directory server"); 139eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 140eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch int index = 0; 141eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch while (!hostsJson.isNull(index)) { 142eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch JSONObject hostJson = hostsJson.getJSONObject(index); 143eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // If a host is only recently registered, it may be missing some of the keys 144eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // below. It should still be visible in the list, even though a connection 145eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // attempt will fail because of the missing keys. The failed attempt will 146eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // trigger reloading of the host-list, by which time the keys will hopefully be 147eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch // present, and the retried connection can succeed. 148eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch HostInfo host = HostInfo.create(hostJson); 149eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch hostList.add(host); 150eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch ++index; 151eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 152eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 153eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } catch (JSONException ex) { 154eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch Log.e("hostlist", "Error parsing host list response: ", ex); 155eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch postError(callback, Error.UNEXPECTED_RESPONSE); 156eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch return; 157eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch } 158eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 159eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch sortHosts(hostList); 160eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch 161 final Callback callbackFinal = callback; 162 final HostInfo[] hosts = hostList.toArray(new HostInfo[hostList.size()]); 163 mMainThread.post(new Runnable() { 164 @Override 165 public void run() { 166 callbackFinal.onHostListReceived(hosts); 167 } 168 }); 169 } 170 171 /** Posts error to callback on main thread. */ 172 private void postError(Callback callback, Error error) { 173 final Callback callbackFinal = callback; 174 final Error errorFinal = error; 175 mMainThread.post(new Runnable() { 176 @Override 177 public void run() { 178 callbackFinal.onError(errorFinal); 179 } 180 }); 181 } 182 183 private static void sortHosts(ArrayList<HostInfo> hosts) { 184 Comparator<HostInfo> hostComparator = new Comparator<HostInfo>() { 185 @Override 186 public int compare(HostInfo a, HostInfo b) { 187 if (a.isOnline != b.isOnline) { 188 return a.isOnline ? -1 : 1; 189 } 190 String aName = a.name.toUpperCase(Locale.getDefault()); 191 String bName = b.name.toUpperCase(Locale.getDefault()); 192 return aName.compareTo(bName); 193 } 194 }; 195 Collections.sort(hosts, hostComparator); 196 } 197} 198