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