diff options
author | lambroslambrou@chromium.org <lambroslambrou@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-02-18 21:02:47 +0000 |
---|---|---|
committer | lambroslambrou@chromium.org <lambroslambrou@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-02-18 21:02:47 +0000 |
commit | 37baafb37f9d44472de7bf0f9263b3a96064c0ed (patch) | |
tree | 40ee2c388ca00f41811aa9eddbb57f7bf5c9917d | |
parent | 52f4c505c831ba83ebd07a23f57c8e7ff2ae4661 (diff) | |
download | chromium_src-37baafb37f9d44472de7bf0f9263b3a96064c0ed.zip chromium_src-37baafb37f9d44472de7bf0f9263b3a96064c0ed.tar.gz chromium_src-37baafb37f9d44472de7bf0f9263b3a96064c0ed.tar.bz2 |
Pull HostListDirectoryGrabber out to a separate class.
This makes Chromoting.java completely single-threaded. Auth tokens are
still handled in Chromoting.java, but all the host-list fetching takes
place in HostListLoader.java.
The error-handling is improved to match more closely with what the
web-app does in host_list.js. Otherwise, I've tried to keep the
error-handling the same as before. Previously, errors from the
AccountService and host-list fetching were all handled in the same
place. For example, all IOExceptions were assumed to be caused by the
OAuth token being invalid. Now I've added an enumeration of possible
errors from host-list fetching, and we only refresh the auth
token if the error was explicitly an authentication failure.
Future CLs will improve the error-handling, reusing many of the same
localized strings as the web-app.
BUG=304719
NOTRY=true
TBR=sergeyu
Review URL: https://codereview.chromium.org/157013002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@251838 0039d316-1c4b-4281-b951-d872f2087c98
4 files changed, 355 insertions, 264 deletions
diff --git a/remoting/android/java/src/org/chromium/chromoting/Chromoting.java b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java index a4b3576..04741a4 100644 --- a/remoting/android/java/src/org/chromium/chromoting/Chromoting.java +++ b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java @@ -16,8 +16,6 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -27,24 +25,16 @@ import android.widget.TextView; import android.widget.Toast; import org.chromium.chromoting.jni.JniInterface; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import java.io.IOException; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Scanner; +import java.util.Arrays; /** * The user interface for querying and displaying a user's host list from the directory server. It * also requests and renews authentication tokens using the system account manager. */ -public class Chromoting extends Activity implements JniInterface.ConnectionListener { +public class Chromoting extends Activity implements JniInterface.ConnectionListener, + AccountManagerCallback<Bundle>, HostListLoader.Callback { /** Only accounts of this type will be selectable for authentication. */ private static final String ACCOUNT_TYPE = "com.google"; @@ -52,22 +42,17 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/chromoting " + "https://www.googleapis.com/auth/googletalk"; - /** 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?key="; - - /** Lock to protect |mAccount| and |mToken|. */ - // TODO(lambroslambrou): |mHosts| needs to be protected as well. - private Object mLock = new Object(); - /** User's account details. */ private Account mAccount; /** Account auth token. */ private String mToken; + /** Helper for fetching the host list. */ + private HostListLoader mHostListLoader; + /** List of hosts. */ - private JSONArray mHosts; + private HostInfo[] mHosts; /** Refresh button. */ private MenuItem mRefreshButton; @@ -81,13 +66,17 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe /** Host list as it appears to the user. */ private ListView mList; - /** Callback handler to be used for network operations. */ - private Handler mNetwork; - /** Dialog for reporting connection progress. */ private ProgressDialog mProgressIndicator; /** + * This is set when receiving an authentication error from the HostListLoader. If that occurs, + * this flag is set and a fresh authentication token is fetched from the AccountsService, and + * used to request the host list a second time. + */ + boolean mAlreadyTried; + + /** * Called when the activity is first created. Loads the native library and requests an * authentication token from the system. */ @@ -96,6 +85,9 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe super.onCreate(savedInstanceState); setContentView(R.layout.main); + mAlreadyTried = false; + mHostListLoader = new HostListLoader(); + // Get ahold of our view widgets. mGreeting = (TextView)findViewById(R.id.hostList_greeting); mList = (ListView)findViewById(R.id.hostList_chooser); @@ -103,34 +95,20 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe // Bring native components online. JniInterface.loadLibrary(this); - // Thread responsible for downloading/displaying host list. - HandlerThread thread = new HandlerThread("auth_callback"); - thread.start(); - mNetwork = new Handler(thread.getLooper()); - SharedPreferences prefs = getPreferences(MODE_PRIVATE); if (prefs.contains("account_name") && prefs.contains("account_type")) { // Perform authentication using saved account selection. mAccount = new Account(prefs.getString("account_name", null), prefs.getString("account_type", null)); - AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, - new HostListDirectoryGrabber(this), mNetwork); + AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null); if (mAccountSwitcher != null) { mAccountSwitcher.setTitle(mAccount.name); } } else { // Request auth callback once user has chosen an account. Log.i("auth", "Requesting auth token from system"); - AccountManager.get(this).getAuthTokenByFeatures( - ACCOUNT_TYPE, - TOKEN_SCOPE, - null, - this, - null, - null, - new HostListDirectoryGrabber(this), - mNetwork - ); + AccountManager.get(this).getAuthTokenByFeatures(ACCOUNT_TYPE, TOKEN_SCOPE, null, this, + null, null, this, null); } } @@ -169,210 +147,131 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe /** Called whenever an action bar button is pressed. */ @Override public boolean onOptionsItemSelected(MenuItem item) { + mAlreadyTried = false; if (item == mAccountSwitcher) { // The account switcher triggers a listing of all available accounts. - AccountManager.get(this).getAuthTokenByFeatures( - ACCOUNT_TYPE, - TOKEN_SCOPE, - null, - this, - null, - null, - new HostListDirectoryGrabber(this), - mNetwork - ); - } - else { + AccountManager.get(this).getAuthTokenByFeatures(ACCOUNT_TYPE, TOKEN_SCOPE, null, this, + null, null, this, null); + } else { // The refresh button simply makes use of the currently-chosen account. - AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, - new HostListDirectoryGrabber(this), mNetwork); + AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null); } return true; } /** Called when the user taps on a host entry. */ - public void connectToHost(JSONObject host) { - try { - synchronized (mLock) { - JniInterface.connectToHost(mAccount.name, mToken, host.getString("jabberId"), - host.getString("hostId"), host.getString("publicKey"), this); - } - } catch (JSONException ex) { - Log.w("host", ex); + public void connectToHost(HostInfo host) { + if (host.jabberId.isEmpty() || host.publicKey.isEmpty()) { + // TODO(lambroslambrou): If these keys are not present, treat this as a connection + // failure and reload the host list (see crbug.com/304719). Toast.makeText(this, getString(R.string.error_reading_host), Toast.LENGTH_LONG).show(); - // Close the application. - finish(); + return; } + + JniInterface.connectToHost(mAccount.name, mToken, host.jabberId, host.id, host.publicKey, + this); } - /** - * Processes the authentication token once the system provides it. Once in possession of such a - * token, attempts to request a host list from the directory server. In case of a bad response, - * this is retried once in case the system's cached auth token had expired. - */ - private class HostListDirectoryGrabber implements AccountManagerCallback<Bundle> { - // TODO(lambroslambrou): Refactor this class to provide async interface usable on the UI - // thread. + @Override + public void run(AccountManagerFuture<Bundle> future) { + Log.i("auth", "User finished with auth dialogs"); + Bundle result = null; + String explanation = null; + try { + // Here comes our auth token from the Android system. + result = future.getResult(); + } catch (OperationCanceledException ex) { + explanation = getString(R.string.error_auth_canceled); + } catch (AuthenticatorException ex) { + explanation = getString(R.string.error_no_accounts); + } catch (IOException ex) { + explanation = getString(R.string.error_bad_connection); + } - /** Whether authentication has already been attempted. */ - private boolean mAlreadyTried; + if (result == null) { + Toast.makeText(this, explanation, Toast.LENGTH_LONG).show(); + return; + } - /** Communication with the screen. */ - private Activity mUi; + String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME); + String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE); + String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); + Log.i("auth", "Received an auth token from system"); - /** Constructor. */ - public HostListDirectoryGrabber(Activity ui) { - mAlreadyTried = false; - mUi = ui; - } + mAccount = new Account(accountName, accountType); + mToken = authToken; + getPreferences(MODE_PRIVATE).edit().putString("account_name", accountName). + putString("account_type", accountType).apply(); - /** - * Retrieves the host list from the directory server. This method performs - * network operations and must be run an a non-UI thread. - */ - @Override - public void run(AccountManagerFuture<Bundle> future) { - Log.i("auth", "User finished with auth dialogs"); - try { - // Here comes our auth token from the Android system. - Bundle result = future.getResult(); - String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME); - String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE); - String authToken = result.getString(AccountManager.KEY_AUTHTOKEN); - Log.i("auth", "Received an auth token from system"); - - synchronized (mLock) { - mAccount = new Account(accountName, accountType); - mToken = authToken; - getPreferences(MODE_PRIVATE).edit().putString("account_name", accountName). - putString("account_type", accountType).apply(); - } + mHostListLoader.retrieveHostList(authToken, this); + } - // Send our HTTP request to the directory server. - URLConnection link = - new URL(HOST_LIST_PATH + JniInterface.nativeGetApiKey()).openConnection(); - link.addRequestProperty("client_id", JniInterface.nativeGetClientId()); - link.addRequestProperty("client_secret", JniInterface.nativeGetClientSecret()); - link.setRequestProperty("Authorization", "OAuth " + authToken); - - // Listen for the server to respond. - StringBuilder response = new StringBuilder(); - Scanner incoming = new Scanner(link.getInputStream()); - Log.i("auth", "Successfully authenticated to directory server"); - while (incoming.hasNext()) { - response.append(incoming.nextLine()); - } - incoming.close(); - - // Interpret what the directory server told us. - JSONObject data = new JSONObject(String.valueOf(response)).getJSONObject("data"); - mHosts = sortHosts(data.getJSONArray("items")); - Log.i("hostlist", "Received host listing from directory server"); - } catch (RuntimeException ex) { - // Make sure any other failure is reported to the user (as an unknown error). - throw ex; - } catch (Exception ex) { - // Assemble error message to display to the user. - String explanation = getString(R.string.error_unknown); - if (ex instanceof OperationCanceledException) { - explanation = getString(R.string.error_auth_canceled); - } else if (ex instanceof AuthenticatorException) { - explanation = getString(R.string.error_no_accounts); - } else if (ex instanceof IOException) { - if (!mAlreadyTried) { - // This was our first connection attempt. - - synchronized (mLock) { - if (mAccount != null) { - // We got an account, but couldn't log into it. We'll retry in case - // the system's cached authentication token had already expired. - AccountManager authenticator = AccountManager.get(mUi); - mAlreadyTried = true; - - Log.w("auth", "Requesting renewal of rejected auth token"); - authenticator.invalidateAuthToken(mAccount.type, mToken); - mToken = null; - authenticator.getAuthToken( - mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork); - - // We're not in an error state *yet*. - return; - } - } - - // We didn't even get an account, so the auth server is likely unreachable. - explanation = getString(R.string.error_bad_connection); - } else { - // Authentication truly failed. - Log.e("auth", "Fresh auth token was also rejected"); - explanation = getString(R.string.error_auth_failed); - } - } else if (ex instanceof JSONException) { - explanation = getString(R.string.error_unexpected_response); - } + @Override + public void onHostListReceived(HostInfo[] hosts) { + // Store a copy of the array, so that it can't be mutated by the HostListLoader. HostInfo + // is an immutable type, so a shallow copy of the array is sufficient here. + mHosts = Arrays.copyOf(hosts, hosts.length); + updateUi(); + } - mHosts = null; - Log.w("auth", ex); - Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show(); - } + @Override + public void onError(HostListLoader.Error error) { + String explanation = null; + switch (error) { + case AUTH_FAILED: + break; + case NETWORK_ERROR: + explanation = getString(R.string.error_bad_connection); + break; + case SERVICE_UNAVAILABLE: + case UNEXPECTED_RESPONSE: + explanation = getString(R.string.error_unexpected_response); + break; + case UNKNOWN: + explanation = getString(R.string.error_unknown); + break; + default: + // Unreachable. + return; + } - // Share our findings with the user. - runOnUiThread(new Runnable() { - @Override - public void run() { - updateUi(); - } - }); + if (explanation != null) { + Toast.makeText(this, explanation, Toast.LENGTH_LONG).show(); + return; } - private JSONArray sortHosts(JSONArray hosts) { - List<JSONObject> hostList = new ArrayList<JSONObject>(); - for (int i = 0; i < hosts.length(); i++) { - try { - hostList.add(hosts.getJSONObject(i)); - } catch (JSONException ex) { - // Ignore non-object entries. - } - } + // This is the AUTH_FAILED case. - Comparator<JSONObject> compareHosts = new Comparator<JSONObject>() { - public int compare(JSONObject a, JSONObject b) { - try { - boolean aOnline = a.getString("status").equals("ONLINE"); - boolean bOnline = b.getString("status").equals("ONLINE"); - if (aOnline && !bOnline) { - return -1; - } - if (bOnline && !aOnline) { - return 1; - } - String aName = a.getString("hostName").toUpperCase(); - String bName = b.getString("hostName").toUpperCase(); - return aName.compareTo(bName); - } catch (JSONException ex) { - return 0; - } - } - }; - Collections.sort(hostList, compareHosts); + if (!mAlreadyTried) { + // This was our first connection attempt. + + AccountManager authenticator = AccountManager.get(this); + mAlreadyTried = true; - JSONArray result = new JSONArray(hostList); - return result; + Log.w("auth", "Requesting renewal of rejected auth token"); + authenticator.invalidateAuthToken(mAccount.type, mToken); + mToken = null; + authenticator.getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null); + + // We're not in an error state *yet*. + return; + } else { + // Authentication truly failed. + Log.e("auth", "Fresh auth token was also rejected"); + explanation = getString(R.string.error_auth_failed); + Toast.makeText(this, explanation, Toast.LENGTH_LONG).show(); } } /** * Updates the infotext and host list display. - * This method affects the UI and must be run on the main thread. */ private void updateUi() { - synchronized (mLock) { - mRefreshButton.setEnabled(mAccount != null); - if (mAccount != null) { - mAccountSwitcher.setTitle(mAccount.name); - } + mRefreshButton.setEnabled(mAccount != null); + if (mAccount != null) { + mAccountSwitcher.setTitle(mAccount.name); } if (mHosts == null) { @@ -383,23 +282,9 @@ public class Chromoting extends Activity implements JniInterface.ConnectionListe mGreeting.setText(getString(R.string.inst_host_list)); - ArrayAdapter<JSONObject> displayer = new HostListAdapter(this, R.layout.host); + ArrayAdapter<HostInfo> displayer = new HostListAdapter(this, R.layout.host, mHosts); Log.i("hostlist", "About to populate host list display"); - try { - int index = 0; - while (!mHosts.isNull(index)) { - displayer.add(mHosts.getJSONObject(index)); - ++index; - } - mList.setAdapter(displayer); - } catch (JSONException ex) { - Log.w("hostlist", ex); - Toast.makeText(this, getString(R.string.error_cataloging_hosts), - Toast.LENGTH_LONG).show(); - - // Close the application. - finish(); - } + mList.setAdapter(displayer); } @Override diff --git a/remoting/android/java/src/org/chromium/chromoting/HostInfo.java b/remoting/android/java/src/org/chromium/chromoting/HostInfo.java new file mode 100644 index 0000000..229764f --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/HostInfo.java @@ -0,0 +1,22 @@ +// 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; + +/** Class to represent a Host returned by {@link HostListLoader}. */ +public class HostInfo { + public final String name; + public final String id; + public final String jabberId; + public final String publicKey; + public final boolean isOnline; + + public HostInfo(String name, String id, String jabberId, String publicKey, boolean isOnline) { + this.name = name; + this.id = id; + this.jabberId = jabberId; + this.publicKey = publicKey; + this.isOnline = isOnline; + } +} diff --git a/remoting/android/java/src/org/chromium/chromoting/HostListAdapter.java b/remoting/android/java/src/org/chromium/chromoting/HostListAdapter.java index c4d09e7..28c6e43 100644 --- a/remoting/android/java/src/org/chromium/chromoting/HostListAdapter.java +++ b/remoting/android/java/src/org/chromium/chromoting/HostListAdapter.java @@ -5,18 +5,13 @@ package org.chromium.chromoting; import android.text.Html; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; -import android.widget.Toast; - -import org.json.JSONException; -import org.json.JSONObject; /** Describes the appearance and behavior of each host list entry. */ -class HostListAdapter extends ArrayAdapter<JSONObject> { +class HostListAdapter extends ArrayAdapter<HostInfo> { /** Color to use for hosts that are online. */ private static final String HOST_COLOR_ONLINE = "green"; @@ -26,8 +21,8 @@ class HostListAdapter extends ArrayAdapter<JSONObject> { private Chromoting mChromoting; /** Constructor. */ - public HostListAdapter(Chromoting chromoting, int textViewResourceId) { - super(chromoting, textViewResourceId); + public HostListAdapter(Chromoting chromoting, int textViewResourceId, HostInfo[] hosts) { + super(chromoting, textViewResourceId, hosts); mChromoting = chromoting; } @@ -36,32 +31,24 @@ class HostListAdapter extends ArrayAdapter<JSONObject> { public View getView(int position, View convertView, ViewGroup parent) { TextView target = (TextView)super.getView(position, convertView, parent); - try { - final JSONObject host = getItem(position); - String status = host.getString("status"); - boolean online = status.equals("ONLINE"); - target.setText(Html.fromHtml(host.getString("hostName") + " (<font color=\"" + - (online ? HOST_COLOR_ONLINE : HOST_COLOR_OFFLINE) + "\">" + status + - "</font>)")); - - if (online) { - target.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mChromoting.connectToHost(host); - } - }); - } else { - // Disallow interaction with this entry. - target.setEnabled(false); - } - } catch (JSONException ex) { - Log.w("hostlist", ex); - Toast.makeText(mChromoting, mChromoting.getString(R.string.error_displaying_host), - Toast.LENGTH_LONG).show(); - - // Close the application. - mChromoting.finish(); + final HostInfo host = getItem(position); + + // TODO(lambroslambrou): Don't use hardcoded ONLINE/OFFLINE strings here. + // See http://crbug.com/331103 + target.setText(Html.fromHtml(host.name + " (<font color=\"" + + (host.isOnline ? HOST_COLOR_ONLINE : HOST_COLOR_OFFLINE) + "\">" + + (host.isOnline ? "ONLINE" : "OFFLINE") + "</font>)")); + + if (host.isOnline) { + target.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mChromoting.connectToHost(host); + } + }); + } else { + // Disallow interaction with this entry. + target.setEnabled(false); } return target; diff --git a/remoting/android/java/src/org/chromium/chromoting/HostListLoader.java b/remoting/android/java/src/org/chromium/chromoting/HostListLoader.java new file mode 100644 index 0000000..b9d1c54 --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/HostListLoader.java @@ -0,0 +1,197 @@ +// 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.chromium.chromoting.jni.JniInterface; +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.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?key="; + + /** 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 + JniInterface.nativeGetApiKey()).openConnection(); + link.addRequestProperty("client_id", JniInterface.nativeGetClientId()); + link.addRequestProperty("client_secret", JniInterface.nativeGetClientSecret()); + 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"); + 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 (once crbug.com/304719 is fixed), by which time the keys will + // hopefully be present, and the retried connection can succeed. + HostInfo host = new HostInfo( + hostJson.getString("hostName"), + hostJson.getString("hostId"), + hostJson.optString("jabberId"), + hostJson.optString("publicKey"), + hostJson.optString("status").equals("ONLINE")); + hostList.add(host); + ++index; + } + } catch (JSONException 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>() { + public int compare(HostInfo a, HostInfo b) { + if (a.isOnline != b.isOnline) { + return a.isOnline ? -1 : 1; + } + String aName = a.name.toUpperCase(); + String bName = b.name.toUpperCase(); + return aName.compareTo(bName); + } + }; + Collections.sort(hosts, hostComparator); + } +} |