Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(15)

Side by Side Diff: remoting/android/java/src/org/chromium/chromoting/Chromoting.java

Issue 157013002: Pull HostListDirectoryGrabber out to a separate class. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Add Host class and address comments Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 package org.chromium.chromoting; 5 package org.chromium.chromoting;
6 6
7 import android.accounts.Account; 7 import android.accounts.Account;
8 import android.accounts.AccountManager; 8 import android.accounts.AccountManager;
9 import android.accounts.AccountManagerCallback; 9 import android.accounts.AccountManagerCallback;
10 import android.accounts.AccountManagerFuture; 10 import android.accounts.AccountManagerFuture;
11 import android.accounts.AuthenticatorException; 11 import android.accounts.AuthenticatorException;
12 import android.accounts.OperationCanceledException; 12 import android.accounts.OperationCanceledException;
13 import android.app.Activity; 13 import android.app.Activity;
14 import android.app.ProgressDialog; 14 import android.app.ProgressDialog;
15 import android.content.DialogInterface; 15 import android.content.DialogInterface;
16 import android.content.Intent; 16 import android.content.Intent;
17 import android.content.SharedPreferences; 17 import android.content.SharedPreferences;
18 import android.os.Bundle; 18 import android.os.Bundle;
19 import android.os.Handler;
20 import android.os.HandlerThread;
21 import android.util.Log; 19 import android.util.Log;
22 import android.view.Menu; 20 import android.view.Menu;
23 import android.view.MenuItem; 21 import android.view.MenuItem;
24 import android.widget.ArrayAdapter; 22 import android.widget.ArrayAdapter;
25 import android.widget.ListView; 23 import android.widget.ListView;
26 import android.widget.TextView; 24 import android.widget.TextView;
27 import android.widget.Toast; 25 import android.widget.Toast;
28 26
29 import org.chromium.chromoting.jni.JniInterface; 27 import org.chromium.chromoting.jni.JniInterface;
30 import org.json.JSONArray;
31 import org.json.JSONException;
32 import org.json.JSONObject;
33 28
34 import java.io.IOException; 29 import java.io.IOException;
35 import java.net.URL;
36 import java.net.URLConnection;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.Comparator;
40 import java.util.List;
41 import java.util.Scanner;
42 30
43 /** 31 /**
44 * The user interface for querying and displaying a user's host list from the di rectory server. It 32 * The user interface for querying and displaying a user's host list from the di rectory server. It
45 * also requests and renews authentication tokens using the system account manag er. 33 * also requests and renews authentication tokens using the system account manag er.
46 */ 34 */
47 public class Chromoting extends Activity implements JniInterface.ConnectionListe ner { 35 public class Chromoting extends Activity implements JniInterface.ConnectionListe ner,
36 AccountManagerCallback<Bundle>, HostListLoader.Callback {
48 /** Only accounts of this type will be selectable for authentication. */ 37 /** Only accounts of this type will be selectable for authentication. */
49 private static final String ACCOUNT_TYPE = "com.google"; 38 private static final String ACCOUNT_TYPE = "com.google";
50 39
51 /** Scopes at which the authentication token we request will be valid. */ 40 /** Scopes at which the authentication token we request will be valid. */
52 private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com /auth/chromoting " + 41 private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com /auth/chromoting " +
53 "https://www.googleapis.com/auth/googletalk"; 42 "https://www.googleapis.com/auth/googletalk";
54 43
55 /** Path from which to download a user's host list JSON object. */
56 private static final String HOST_LIST_PATH =
57 "https://www.googleapis.com/chromoting/v1/@me/hosts?key=";
58
59 /** Lock to protect |mAccount| and |mToken|. */
60 // TODO(lambroslambrou): |mHosts| needs to be protected as well.
61 private Object mLock = new Object();
62
63 /** User's account details. */ 44 /** User's account details. */
64 private Account mAccount; 45 private Account mAccount;
65 46
66 /** Account auth token. */ 47 /** Account auth token. */
67 private String mToken; 48 private String mToken;
68 49
50 /** Helper for fetching the host list. */
51 private HostListLoader mHostListLoader;
52
69 /** List of hosts. */ 53 /** List of hosts. */
70 private JSONArray mHosts; 54 private Host[] mHosts;
71 55
72 /** Refresh button. */ 56 /** Refresh button. */
73 private MenuItem mRefreshButton; 57 private MenuItem mRefreshButton;
74 58
75 /** Account switcher. */ 59 /** Account switcher. */
76 private MenuItem mAccountSwitcher; 60 private MenuItem mAccountSwitcher;
77 61
78 /** Greeting at the top of the displayed list. */ 62 /** Greeting at the top of the displayed list. */
79 private TextView mGreeting; 63 private TextView mGreeting;
80 64
81 /** Host list as it appears to the user. */ 65 /** Host list as it appears to the user. */
82 private ListView mList; 66 private ListView mList;
83 67
84 /** Callback handler to be used for network operations. */
85 private Handler mNetwork;
86
87 /** Dialog for reporting connection progress. */ 68 /** Dialog for reporting connection progress. */
88 private ProgressDialog mProgressIndicator; 69 private ProgressDialog mProgressIndicator;
89 70
90 /** 71 /**
72 * This is set when receiving an authentication error from the HostListLoade r. If that occurs,
73 * this flag is set and a fresh authentication token is fetched from the Acc ountsService, and
74 * used to request the host list a second time.
75 */
76 boolean mAlreadyTried;
77
78 /**
91 * Called when the activity is first created. Loads the native library and r equests an 79 * Called when the activity is first created. Loads the native library and r equests an
92 * authentication token from the system. 80 * authentication token from the system.
93 */ 81 */
94 @Override 82 @Override
95 public void onCreate(Bundle savedInstanceState) { 83 public void onCreate(Bundle savedInstanceState) {
96 super.onCreate(savedInstanceState); 84 super.onCreate(savedInstanceState);
97 setContentView(R.layout.main); 85 setContentView(R.layout.main);
98 86
87 mAlreadyTried = false;
88 mHostListLoader = new HostListLoader();
89
99 // Get ahold of our view widgets. 90 // Get ahold of our view widgets.
100 mGreeting = (TextView)findViewById(R.id.hostList_greeting); 91 mGreeting = (TextView)findViewById(R.id.hostList_greeting);
101 mList = (ListView)findViewById(R.id.hostList_chooser); 92 mList = (ListView)findViewById(R.id.hostList_chooser);
102 93
103 // Bring native components online. 94 // Bring native components online.
104 JniInterface.loadLibrary(this); 95 JniInterface.loadLibrary(this);
105 96
106 // Thread responsible for downloading/displaying host list.
107 HandlerThread thread = new HandlerThread("auth_callback");
108 thread.start();
109 mNetwork = new Handler(thread.getLooper());
110
111 SharedPreferences prefs = getPreferences(MODE_PRIVATE); 97 SharedPreferences prefs = getPreferences(MODE_PRIVATE);
112 if (prefs.contains("account_name") && prefs.contains("account_type")) { 98 if (prefs.contains("account_name") && prefs.contains("account_type")) {
113 // Perform authentication using saved account selection. 99 // Perform authentication using saved account selection.
114 mAccount = new Account(prefs.getString("account_name", null), 100 mAccount = new Account(prefs.getString("account_name", null),
115 prefs.getString("account_type", null)); 101 prefs.getString("account_type", null));
116 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, 102 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, this, null);
117 new HostListDirectoryGrabber(this), mNetwork);
118 if (mAccountSwitcher != null) { 103 if (mAccountSwitcher != null) {
119 mAccountSwitcher.setTitle(mAccount.name); 104 mAccountSwitcher.setTitle(mAccount.name);
120 } 105 }
121 } else { 106 } else {
122 // Request auth callback once user has chosen an account. 107 // Request auth callback once user has chosen an account.
123 Log.i("auth", "Requesting auth token from system"); 108 Log.i("auth", "Requesting auth token from system");
124 AccountManager.get(this).getAuthTokenByFeatures( 109 AccountManager.get(this).getAuthTokenByFeatures(ACCOUNT_TYPE, TOKEN_ SCOPE, null, this,
125 ACCOUNT_TYPE, 110 null, null, this, null);
126 TOKEN_SCOPE,
127 null,
128 this,
129 null,
130 null,
131 new HostListDirectoryGrabber(this),
132 mNetwork
133 );
134 } 111 }
135 } 112 }
136 113
137 /** Called when the activity is finally finished. */ 114 /** Called when the activity is finally finished. */
138 @Override 115 @Override
139 public void onDestroy() { 116 public void onDestroy() {
140 super.onDestroy(); 117 super.onDestroy();
141 JniInterface.disconnectFromHost(); 118 JniInterface.disconnectFromHost();
142 } 119 }
143 120
(...skipping 18 matching lines...) Expand all
162 // If the user has picked an account, show its name directly on the account switcher. 139 // If the user has picked an account, show its name directly on the account switcher.
163 mAccountSwitcher.setTitle(mAccount.name); 140 mAccountSwitcher.setTitle(mAccount.name);
164 } 141 }
165 142
166 return super.onCreateOptionsMenu(menu); 143 return super.onCreateOptionsMenu(menu);
167 } 144 }
168 145
169 /** Called whenever an action bar button is pressed. */ 146 /** Called whenever an action bar button is pressed. */
170 @Override 147 @Override
171 public boolean onOptionsItemSelected(MenuItem item) { 148 public boolean onOptionsItemSelected(MenuItem item) {
149 mAlreadyTried = false;
172 if (item == mAccountSwitcher) { 150 if (item == mAccountSwitcher) {
173 // The account switcher triggers a listing of all available accounts . 151 // The account switcher triggers a listing of all available accounts .
174 AccountManager.get(this).getAuthTokenByFeatures( 152 AccountManager.get(this).getAuthTokenByFeatures(ACCOUNT_TYPE, TOKEN_ SCOPE, null, this,
175 ACCOUNT_TYPE, 153 null, null, this, null);
176 TOKEN_SCOPE, 154 } else {
177 null,
178 this,
179 null,
180 null,
181 new HostListDirectoryGrabber(this),
182 mNetwork
183 );
184 }
185 else {
186 // The refresh button simply makes use of the currently-chosen accou nt. 155 // The refresh button simply makes use of the currently-chosen accou nt.
187 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, 156 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, this, null);
188 new HostListDirectoryGrabber(this), mNetwork);
189 } 157 }
190 158
191 return true; 159 return true;
192 } 160 }
193 161
194 /** Called when the user taps on a host entry. */ 162 /** Called when the user taps on a host entry. */
195 public void connectToHost(JSONObject host) { 163 public void connectToHost(Host host) {
164 JniInterface.connectToHost(mAccount.name, mToken, host.jabberId, host.id , host.publicKey,
165 this);
166 }
167
168 @Override
169 public void run(AccountManagerFuture<Bundle> future) {
170 Log.i("auth", "User finished with auth dialogs");
196 try { 171 try {
197 synchronized (mLock) { 172 // Here comes our auth token from the Android system.
198 JniInterface.connectToHost(mAccount.name, mToken, host.getString ("jabberId"), 173 Bundle result = future.getResult();
199 host.getString("hostId"), host.getString("publicKey"), t his); 174 String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAM E);
200 } 175 String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYP E);
201 } catch (JSONException ex) { 176 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
202 Log.w("host", ex); 177 Log.i("auth", "Received an auth token from system");
203 Toast.makeText(this, getString(R.string.error_reading_host), 178
204 Toast.LENGTH_LONG).show(); 179 mAccount = new Account(accountName, accountType);
205 // Close the application. 180 mToken = authToken;
206 finish(); 181 getPreferences(MODE_PRIVATE).edit().putString("account_name", accoun tName).
182 putString("account_type", accountType).apply();
183
184 mHostListLoader.retrieveHostList(authToken, this);
185 } catch (OperationCanceledException ex) {
Sergey Ulanov 2014/02/14 08:55:20 I think you can wrap only the first line (future.g
Lambros 2014/02/15 01:40:21 Done.
186 String explanation = getString(R.string.error_auth_canceled);
187 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
188 } catch (AuthenticatorException ex) {
189 String explanation = getString(R.string.error_no_accounts);
190 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
191 } catch (IOException ex) {
192 String explanation = getString(R.string.error_bad_connection);
193 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
207 } 194 }
208 } 195 }
209 196
210 /** 197 @Override
211 * Processes the authentication token once the system provides it. Once in p ossession of such a 198 public void onHostListReceived(Host[] hosts) {
212 * token, attempts to request a host list from the directory server. In case of a bad response, 199 mHosts = hosts;
213 * this is retried once in case the system's cached auth token had expired. 200 updateUi();
214 */ 201 }
215 private class HostListDirectoryGrabber implements AccountManagerCallback<Bun dle> {
216 // TODO(lambroslambrou): Refactor this class to provide async interface usable on the UI
217 // thread.
218 202
219 /** Whether authentication has already been attempted. */ 203 @Override
220 private boolean mAlreadyTried; 204 public void onError(HostListLoader.Error error) {
221 205 String explanation = null;
222 /** Communication with the screen. */ 206 switch (error) {
223 private Activity mUi; 207 case AUTH_FAILED:
224 208 break;
225 /** Constructor. */ 209 case NETWORK_ERROR:
226 public HostListDirectoryGrabber(Activity ui) { 210 explanation = getString(R.string.error_bad_connection);
227 mAlreadyTried = false; 211 break;
228 mUi = ui; 212 case SERVICE_UNAVAILABLE:
213 case UNEXPECTED_RESPONSE:
214 explanation = getString(R.string.error_unexpected_response);
215 break;
216 case UNKNOWN:
217 explanation = getString(R.string.error_unknown);
218 break;
219 default:
220 // Unreachable.
221 return;
229 } 222 }
230 223
231 /** 224 if (explanation != null) {
232 * Retrieves the host list from the directory server. This method perfor ms 225 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
233 * network operations and must be run an a non-UI thread. 226 return;
234 */
235 @Override
236 public void run(AccountManagerFuture<Bundle> future) {
237 Log.i("auth", "User finished with auth dialogs");
238 try {
239 // Here comes our auth token from the Android system.
240 Bundle result = future.getResult();
241 String accountName = result.getString(AccountManager.KEY_ACCOUNT _NAME);
242 String accountType = result.getString(AccountManager.KEY_ACCOUNT _TYPE);
243 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN );
244 Log.i("auth", "Received an auth token from system");
245
246 synchronized (mLock) {
247 mAccount = new Account(accountName, accountType);
248 mToken = authToken;
249 getPreferences(MODE_PRIVATE).edit().putString("account_name" , accountName).
250 putString("account_type", accountType).apply();
251 }
252
253 // Send our HTTP request to the directory server.
254 URLConnection link =
255 new URL(HOST_LIST_PATH + JniInterface.nativeGetApiKey()) .openConnection();
256 link.addRequestProperty("client_id", JniInterface.nativeGetClien tId());
257 link.addRequestProperty("client_secret", JniInterface.nativeGetC lientSecret());
258 link.setRequestProperty("Authorization", "OAuth " + authToken);
259
260 // Listen for the server to respond.
261 StringBuilder response = new StringBuilder();
262 Scanner incoming = new Scanner(link.getInputStream());
263 Log.i("auth", "Successfully authenticated to directory server");
264 while (incoming.hasNext()) {
265 response.append(incoming.nextLine());
266 }
267 incoming.close();
268
269 // Interpret what the directory server told us.
270 JSONObject data = new JSONObject(String.valueOf(response)).getJS ONObject("data");
271 mHosts = sortHosts(data.getJSONArray("items"));
272 Log.i("hostlist", "Received host listing from directory server") ;
273 } catch (RuntimeException ex) {
274 // Make sure any other failure is reported to the user (as an un known error).
275 throw ex;
276 } catch (Exception ex) {
277 // Assemble error message to display to the user.
278 String explanation = getString(R.string.error_unknown);
279 if (ex instanceof OperationCanceledException) {
280 explanation = getString(R.string.error_auth_canceled);
281 } else if (ex instanceof AuthenticatorException) {
282 explanation = getString(R.string.error_no_accounts);
283 } else if (ex instanceof IOException) {
284 if (!mAlreadyTried) {
285 // This was our first connection attempt.
286
287 synchronized (mLock) {
288 if (mAccount != null) {
289 // We got an account, but couldn't log into it. We'll retry in case
290 // the system's cached authentication token had already expired.
291 AccountManager authenticator = AccountManager.ge t(mUi);
292 mAlreadyTried = true;
293
294 Log.w("auth", "Requesting renewal of rejected au th token");
295 authenticator.invalidateAuthToken(mAccount.type, mToken);
296 mToken = null;
297 authenticator.getAuthToken(
298 mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork);
299
300 // We're not in an error state *yet*.
301 return;
302 }
303 }
304
305 // We didn't even get an account, so the auth server is likely unreachable.
306 explanation = getString(R.string.error_bad_connection);
307 } else {
308 // Authentication truly failed.
309 Log.e("auth", "Fresh auth token was also rejected");
310 explanation = getString(R.string.error_auth_failed);
311 }
312 } else if (ex instanceof JSONException) {
313 explanation = getString(R.string.error_unexpected_response);
314 }
315
316 mHosts = null;
317 Log.w("auth", ex);
318 Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show();
319 }
320
321 // Share our findings with the user.
322 runOnUiThread(new Runnable() {
323 @Override
324 public void run() {
325 updateUi();
326 }
327 });
328 } 227 }
329 228
330 private JSONArray sortHosts(JSONArray hosts) { 229 // This is the AUTH_FAILED case.
331 List<JSONObject> hostList = new ArrayList<JSONObject>();
332 for (int i = 0; i < hosts.length(); i++) {
333 try {
334 hostList.add(hosts.getJSONObject(i));
335 } catch (JSONException ex) {
336 // Ignore non-object entries.
337 }
338 }
339 230
340 Comparator<JSONObject> compareHosts = new Comparator<JSONObject>() { 231 if (!mAlreadyTried) {
341 public int compare(JSONObject a, JSONObject b) { 232 // This was our first connection attempt.
342 try {
343 boolean aOnline = a.getString("status").equals("ONLINE") ;
344 boolean bOnline = b.getString("status").equals("ONLINE") ;
345 if (aOnline && !bOnline) {
346 return -1;
347 }
348 if (bOnline && !aOnline) {
349 return 1;
350 }
351 String aName = a.getString("hostName").toUpperCase();
352 String bName = b.getString("hostName").toUpperCase();
353 return aName.compareTo(bName);
354 } catch (JSONException ex) {
355 return 0;
356 }
357 }
358 };
359 Collections.sort(hostList, compareHosts);
360 233
361 JSONArray result = new JSONArray(hostList); 234 AccountManager authenticator = AccountManager.get(this);
362 return result; 235 mAlreadyTried = true;
236
237 Log.w("auth", "Requesting renewal of rejected auth token");
238 authenticator.invalidateAuthToken(mAccount.type, mToken);
239 mToken = null;
240 authenticator.getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null);
241
242 // We're not in an error state *yet*.
243 return;
244 } else {
245 // Authentication truly failed.
246 Log.e("auth", "Fresh auth token was also rejected");
247 explanation = getString(R.string.error_auth_failed);
248 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
363 } 249 }
364 } 250 }
365 251
366 /** 252 /**
367 * Updates the infotext and host list display. 253 * Updates the infotext and host list display.
368 * This method affects the UI and must be run on the main thread.
369 */ 254 */
370 private void updateUi() { 255 private void updateUi() {
371 synchronized (mLock) { 256 mRefreshButton.setEnabled(mAccount != null);
372 mRefreshButton.setEnabled(mAccount != null); 257 if (mAccount != null) {
373 if (mAccount != null) { 258 mAccountSwitcher.setTitle(mAccount.name);
374 mAccountSwitcher.setTitle(mAccount.name);
375 }
376 } 259 }
377 260
378 if (mHosts == null) { 261 if (mHosts == null) {
379 mGreeting.setText(getString(R.string.inst_empty_list)); 262 mGreeting.setText(getString(R.string.inst_empty_list));
380 mList.setAdapter(null); 263 mList.setAdapter(null);
381 return; 264 return;
382 } 265 }
383 266
384 mGreeting.setText(getString(R.string.inst_host_list)); 267 mGreeting.setText(getString(R.string.inst_host_list));
385 268
386 ArrayAdapter<JSONObject> displayer = new HostListAdapter(this, R.layout. host); 269 ArrayAdapter<Host> displayer = new HostListAdapter(this, R.layout.host, mHosts);
387 Log.i("hostlist", "About to populate host list display"); 270 Log.i("hostlist", "About to populate host list display");
388 try { 271 mList.setAdapter(displayer);
389 int index = 0;
390 while (!mHosts.isNull(index)) {
391 displayer.add(mHosts.getJSONObject(index));
392 ++index;
393 }
394 mList.setAdapter(displayer);
395 } catch (JSONException ex) {
396 Log.w("hostlist", ex);
397 Toast.makeText(this, getString(R.string.error_cataloging_hosts),
398 Toast.LENGTH_LONG).show();
399
400 // Close the application.
401 finish();
402 }
403 } 272 }
404 273
405 @Override 274 @Override
406 public void onConnectionState(JniInterface.ConnectionListener.State state, 275 public void onConnectionState(JniInterface.ConnectionListener.State state,
407 JniInterface.ConnectionListener.Error error) { 276 JniInterface.ConnectionListener.Error error) {
408 String stateText = getResources().getStringArray(R.array.protoc_states)[ state.value()]; 277 String stateText = getResources().getStringArray(R.array.protoc_states)[ state.value()];
409 boolean dismissProgress = false; 278 boolean dismissProgress = false;
410 switch (state) { 279 switch (state) {
411 case INITIALIZING: 280 case INITIALIZING:
412 case CONNECTING: 281 case CONNECTING:
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
456 // Unreachable, but required by Google Java style and findbugs. 325 // Unreachable, but required by Google Java style and findbugs.
457 assert false : "Unreached"; 326 assert false : "Unreached";
458 } 327 }
459 328
460 if (dismissProgress && mProgressIndicator != null) { 329 if (dismissProgress && mProgressIndicator != null) {
461 mProgressIndicator.dismiss(); 330 mProgressIndicator.dismiss();
462 mProgressIndicator = null; 331 mProgressIndicator = null;
463 } 332 }
464 } 333 }
465 } 334 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698