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