Chromium Code Reviews| OLD | NEW |
|---|---|
| 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.content.Context; | |
| 15 import android.content.Intent; | 14 import android.content.Intent; |
| 16 import android.content.SharedPreferences; | 15 import android.content.SharedPreferences; |
| 17 import android.os.Bundle; | 16 import android.os.Bundle; |
| 18 import android.os.Handler; | 17 import android.os.Handler; |
| 19 import android.os.HandlerThread; | 18 import android.os.HandlerThread; |
| 20 import android.text.Html; | |
| 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.view.View; | |
| 25 import android.view.ViewGroup; | |
| 26 import android.widget.ArrayAdapter; | 22 import android.widget.ArrayAdapter; |
| 27 import android.widget.ListView; | 23 import android.widget.ListView; |
| 28 import android.widget.TextView; | 24 import android.widget.TextView; |
| 29 import android.widget.Toast; | 25 import android.widget.Toast; |
| 30 | 26 |
| 31 import org.chromium.chromoting.jni.JniInterface; | 27 import org.chromium.chromoting.jni.JniInterface; |
| 32 import org.json.JSONArray; | 28 import org.json.JSONArray; |
| 33 import org.json.JSONException; | 29 import org.json.JSONException; |
| 34 import org.json.JSONObject; | 30 import org.json.JSONObject; |
| 35 | 31 |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 47 private static final String ACCOUNT_TYPE = "com.google"; | 43 private static final String ACCOUNT_TYPE = "com.google"; |
| 48 | 44 |
| 49 /** Scopes at which the authentication token we request will be valid. */ | 45 /** Scopes at which the authentication token we request will be valid. */ |
| 50 private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com /auth/chromoting " + | 46 private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com /auth/chromoting " + |
| 51 "https://www.googleapis.com/auth/googletalk"; | 47 "https://www.googleapis.com/auth/googletalk"; |
| 52 | 48 |
| 53 /** Path from which to download a user's host list JSON object. */ | 49 /** Path from which to download a user's host list JSON object. */ |
| 54 private static final String HOST_LIST_PATH = | 50 private static final String HOST_LIST_PATH = |
| 55 "https://www.googleapis.com/chromoting/v1/@me/hosts?key="; | 51 "https://www.googleapis.com/chromoting/v1/@me/hosts?key="; |
| 56 | 52 |
| 57 /** Color to use for hosts that are online. */ | 53 /** Lock to protect |mAccount| and |mToken|. */ |
| 58 private static final String HOST_COLOR_ONLINE = "green"; | 54 // TODO(lambroslambrou): |mHosts| needs to be protected as well. |
| 59 | 55 private Object mLock = new Object(); |
|
Sergey Ulanov
2013/12/28 02:17:30
Do we really need this lock? It won't be necessary
Lambros
2013/12/30 21:46:35
We still need the network thread. I don't know if
| |
| 60 /** Color to use for hosts that are offline. */ | |
| 61 private static final String HOST_COLOR_OFFLINE = "red"; | |
| 62 | 56 |
| 63 /** User's account details. */ | 57 /** User's account details. */ |
| 64 private Account mAccount; | 58 private Account mAccount; |
| 65 | 59 |
| 66 /** Account auth token. */ | 60 /** Account auth token. */ |
| 67 private String mToken; | 61 private String mToken; |
| 68 | 62 |
| 69 /** List of hosts. */ | 63 /** List of hosts. */ |
| 70 private JSONArray mHosts; | 64 private JSONArray mHosts; |
| 71 | 65 |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 104 HandlerThread thread = new HandlerThread("auth_callback"); | 98 HandlerThread thread = new HandlerThread("auth_callback"); |
| 105 thread.start(); | 99 thread.start(); |
| 106 mNetwork = new Handler(thread.getLooper()); | 100 mNetwork = new Handler(thread.getLooper()); |
| 107 | 101 |
| 108 SharedPreferences prefs = getPreferences(MODE_PRIVATE); | 102 SharedPreferences prefs = getPreferences(MODE_PRIVATE); |
| 109 if (prefs.contains("account_name") && prefs.contains("account_type")) { | 103 if (prefs.contains("account_name") && prefs.contains("account_type")) { |
| 110 // Perform authentication using saved account selection. | 104 // Perform authentication using saved account selection. |
| 111 mAccount = new Account(prefs.getString("account_name", null), | 105 mAccount = new Account(prefs.getString("account_name", null), |
| 112 prefs.getString("account_type", null)); | 106 prefs.getString("account_type", null)); |
| 113 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, | 107 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, |
| 114 new HostListDirectoryGrabber(this), mNetwork); | 108 new HostListDirectoryGrabber(this), mNetwork); |
|
Sergey Ulanov
2013/12/28 02:17:30
The last parameter here specifies the thread on wh
Lambros
2013/12/30 21:46:35
We still need the lock, regardless of what we pass
| |
| 115 if (mAccountSwitcher != null) { | 109 if (mAccountSwitcher != null) { |
| 116 mAccountSwitcher.setTitle(mAccount.name); | 110 mAccountSwitcher.setTitle(mAccount.name); |
| 117 } | 111 } |
| 118 } else { | 112 } else { |
| 119 // Request auth callback once user has chosen an account. | 113 // Request auth callback once user has chosen an account. |
| 120 Log.i("auth", "Requesting auth token from system"); | 114 Log.i("auth", "Requesting auth token from system"); |
| 121 AccountManager.get(this).getAuthTokenByFeatures( | 115 AccountManager.get(this).getAuthTokenByFeatures( |
| 122 ACCOUNT_TYPE, | 116 ACCOUNT_TYPE, |
| 123 TOKEN_SCOPE, | 117 TOKEN_SCOPE, |
| 124 null, | 118 null, |
| (...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 181 } | 175 } |
| 182 else { | 176 else { |
| 183 // The refresh button simply makes use of the currently-chosen accou nt. | 177 // The refresh button simply makes use of the currently-chosen accou nt. |
| 184 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, | 178 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, t his, |
| 185 new HostListDirectoryGrabber(this), mNetwork); | 179 new HostListDirectoryGrabber(this), mNetwork); |
| 186 } | 180 } |
| 187 | 181 |
| 188 return true; | 182 return true; |
| 189 } | 183 } |
| 190 | 184 |
| 185 /** Called when the user taps on a host entry. */ | |
| 186 public void connectToHost(JSONObject host) { | |
| 187 try { | |
| 188 synchronized (mLock) { | |
| 189 JniInterface.connectToHost(mAccount.name, mToken, host.getString ("jabberId"), | |
| 190 host.getString("hostId"), host.getString("publicKey"), | |
| 191 new Runnable() { | |
| 192 @Override | |
| 193 public void run() { | |
| 194 startActivity(new Intent(Chromoting.this, De sktop.class)); | |
| 195 } | |
| 196 }); | |
| 197 } | |
| 198 } catch (JSONException ex) { | |
| 199 Log.w("host", ex); | |
| 200 Toast.makeText(this, getString(R.string.error_reading_host), | |
| 201 Toast.LENGTH_LONG).show(); | |
| 202 // Close the application. | |
| 203 finish(); | |
| 204 } | |
| 205 } | |
| 206 | |
| 191 /** | 207 /** |
| 192 * Processes the authentication token once the system provides it. Once in p ossession of such a | 208 * Processes the authentication token once the system provides it. Once in p ossession of such a |
| 193 * token, attempts to request a host list from the directory server. In case of a bad response, | 209 * token, attempts to request a host list from the directory server. In case of a bad response, |
| 194 * this is retried once in case the system's cached auth token had expired. | 210 * this is retried once in case the system's cached auth token had expired. |
| 195 */ | 211 */ |
| 196 private class HostListDirectoryGrabber implements AccountManagerCallback<Bun dle> { | 212 private class HostListDirectoryGrabber implements AccountManagerCallback<Bun dle> { |
|
Sergey Ulanov
2013/12/28 02:17:30
While you are removing nested classes, you can get
Lambros
2013/12/30 21:46:35
The problem is that the nested class has some inte
| |
| 197 /** Whether authentication has already been attempted. */ | 213 /** Whether authentication has already been attempted. */ |
| 198 private boolean mAlreadyTried; | 214 private boolean mAlreadyTried; |
| 199 | 215 |
| 200 /** Communication with the screen. */ | 216 /** Communication with the screen. */ |
| 201 private Activity mUi; | 217 private Activity mUi; |
| 202 | 218 |
| 203 /** Constructor. */ | 219 /** Constructor. */ |
| 204 public HostListDirectoryGrabber(Activity ui) { | 220 public HostListDirectoryGrabber(Activity ui) { |
| 205 mAlreadyTried = false; | 221 mAlreadyTried = false; |
| 206 mUi = ui; | 222 mUi = ui; |
| 207 } | 223 } |
| 208 | 224 |
| 209 /** | 225 /** |
| 210 * Retrieves the host list from the directory server. This method perfor ms | 226 * Retrieves the host list from the directory server. This method perfor ms |
| 211 * network operations and must be run an a non-UI thread. | 227 * network operations and must be run an a non-UI thread. |
| 212 */ | 228 */ |
| 213 @Override | 229 @Override |
| 214 public void run(AccountManagerFuture<Bundle> future) { | 230 public void run(AccountManagerFuture<Bundle> future) { |
| 215 Log.i("auth", "User finished with auth dialogs"); | 231 Log.i("auth", "User finished with auth dialogs"); |
| 216 try { | 232 try { |
| 217 // Here comes our auth token from the Android system. | 233 // Here comes our auth token from the Android system. |
| 218 Bundle result = future.getResult(); | 234 Bundle result = future.getResult(); |
| 219 String accountName = result.getString(AccountManager.KEY_ACCOUNT _NAME); | 235 String accountName = result.getString(AccountManager.KEY_ACCOUNT _NAME); |
| 220 String accountType = result.getString(AccountManager.KEY_ACCOUNT _TYPE); | 236 String accountType = result.getString(AccountManager.KEY_ACCOUNT _TYPE); |
| 221 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN ); | 237 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN ); |
| 222 Log.i("auth", "Received an auth token from system"); | 238 Log.i("auth", "Received an auth token from system"); |
| 223 | 239 |
| 224 synchronized (mUi) { | 240 synchronized (mLock) { |
| 225 mAccount = new Account(accountName, accountType); | 241 mAccount = new Account(accountName, accountType); |
| 226 mToken = authToken; | 242 mToken = authToken; |
| 227 getPreferences(MODE_PRIVATE).edit().putString("account_name" , accountName). | 243 getPreferences(MODE_PRIVATE).edit().putString("account_name" , accountName). |
| 228 putString("account_type", accountType).apply(); | 244 putString("account_type", accountType).apply(); |
| 229 } | 245 } |
| 230 | 246 |
| 231 // Send our HTTP request to the directory server. | 247 // Send our HTTP request to the directory server. |
| 232 URLConnection link = | 248 URLConnection link = |
| 233 new URL(HOST_LIST_PATH + JniInterface.nativeGetApiKey()) .openConnection(); | 249 new URL(HOST_LIST_PATH + JniInterface.nativeGetApiKey()) .openConnection(); |
| 234 link.addRequestProperty("client_id", JniInterface.nativeGetClien tId()); | 250 link.addRequestProperty("client_id", JniInterface.nativeGetClien tId()); |
| (...skipping 20 matching lines...) Expand all Loading... | |
| 255 // Assemble error message to display to the user. | 271 // Assemble error message to display to the user. |
| 256 String explanation = getString(R.string.error_unknown); | 272 String explanation = getString(R.string.error_unknown); |
| 257 if (ex instanceof OperationCanceledException) { | 273 if (ex instanceof OperationCanceledException) { |
| 258 explanation = getString(R.string.error_auth_canceled); | 274 explanation = getString(R.string.error_auth_canceled); |
| 259 } else if (ex instanceof AuthenticatorException) { | 275 } else if (ex instanceof AuthenticatorException) { |
| 260 explanation = getString(R.string.error_no_accounts); | 276 explanation = getString(R.string.error_no_accounts); |
| 261 } else if (ex instanceof IOException) { | 277 } else if (ex instanceof IOException) { |
| 262 if (!mAlreadyTried) { | 278 if (!mAlreadyTried) { |
| 263 // This was our first connection attempt. | 279 // This was our first connection attempt. |
| 264 | 280 |
| 265 synchronized (mUi) { | 281 synchronized (mLock) { |
| 266 if (mAccount != null) { | 282 if (mAccount != null) { |
| 267 // We got an account, but couldn't log into it. We'll retry in case | 283 // We got an account, but couldn't log into it. We'll retry in case |
| 268 // the system's cached authentication token had already expired. | 284 // the system's cached authentication token had already expired. |
| 269 AccountManager authenticator = AccountManager.ge t(mUi); | 285 AccountManager authenticator = AccountManager.ge t(mUi); |
| 270 mAlreadyTried = true; | 286 mAlreadyTried = true; |
| 271 | 287 |
| 272 Log.w("auth", "Requesting renewal of rejected au th token"); | 288 Log.w("auth", "Requesting renewal of rejected au th token"); |
| 273 authenticator.invalidateAuthToken(mAccount.type, mToken); | 289 authenticator.invalidateAuthToken(mAccount.type, mToken); |
| 274 mToken = null; | 290 mToken = null; |
| 275 authenticator.getAuthToken( | 291 authenticator.getAuthToken( |
| 276 mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork); | 292 mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork); |
| 277 | 293 |
| 278 // We're not in an error state *yet*. | 294 // We're not in an error state *yet*. |
| 279 return; | 295 return; |
| 280 } | 296 } |
| 281 } | 297 } |
| 282 | 298 |
| 283 // We didn't even get an account, so the auth server is likely unreachable. | 299 // We didn't even get an account, so the auth server is likely unreachable. |
| 284 explanation = getString(R.string.error_bad_connection); | 300 explanation = getString(R.string.error_bad_connection); |
| 285 } else { | 301 } else { |
| 286 // Authentication truly failed. | 302 // Authentication truly failed. |
| 287 Log.e("auth", "Fresh auth token was also rejected"); | 303 Log.e("auth", "Fresh auth token was also rejected"); |
| 288 explanation = getString(R.string.error_auth_failed); | 304 explanation = getString(R.string.error_auth_failed); |
| 289 } | 305 } |
| 290 } else if (ex instanceof JSONException) { | 306 } else if (ex instanceof JSONException) { |
| 291 explanation = getString(R.string.error_unexpected_response); | 307 explanation = getString(R.string.error_unexpected_response); |
| 292 runOnUiThread(new HostListDisplayer(mUi)); | |
| 293 } | 308 } |
| 294 | 309 |
| 295 mHosts = null; | 310 mHosts = null; |
| 296 Log.w("auth", ex); | 311 Log.w("auth", ex); |
| 297 Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show(); | 312 Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show(); |
| 298 } | 313 } |
| 299 | 314 |
| 300 // Share our findings with the user. | 315 // Share our findings with the user. |
| 301 runOnUiThread(new HostListDisplayer(mUi)); | 316 runOnUiThread(new Runnable() { |
| 317 @Override | |
| 318 public void run() { | |
| 319 updateUi(); | |
| 320 } | |
| 321 }); | |
| 302 } | 322 } |
| 303 } | 323 } |
| 304 | 324 |
| 305 /** Formats the host list and offers it to the user. */ | 325 /** |
| 306 private class HostListDisplayer implements Runnable { | 326 * Updates the infotext and host list display. |
| 307 /** Communication with the screen. */ | 327 * This method affects the UI and must be run on the main thread. |
| 308 private Activity mUi; | 328 */ |
| 309 | 329 private void updateUi() { |
| 310 /** Constructor. */ | 330 synchronized (mLock) { |
| 311 public HostListDisplayer(Activity ui) { | 331 mRefreshButton.setEnabled(mAccount != null); |
| 312 mUi = ui; | 332 if (mAccount != null) { |
| 333 mAccountSwitcher.setTitle(mAccount.name); | |
| 334 } | |
| 313 } | 335 } |
| 314 | 336 |
| 315 /** | 337 if (mHosts == null) { |
| 316 * Updates the infotext and host list display. | 338 mGreeting.setText(getString(R.string.inst_empty_list)); |
| 317 * This method affects the UI and must be run on its same thread. | 339 mList.setAdapter(null); |
| 318 */ | 340 return; |
| 319 @Override | |
| 320 public void run() { | |
| 321 synchronized (mUi) { | |
| 322 mRefreshButton.setEnabled(mAccount != null); | |
| 323 if (mAccount != null) { | |
| 324 mAccountSwitcher.setTitle(mAccount.name); | |
| 325 } | |
| 326 } | |
| 327 | |
| 328 if (mHosts == null) { | |
| 329 mGreeting.setText(getString(R.string.inst_empty_list)); | |
| 330 mList.setAdapter(null); | |
| 331 return; | |
| 332 } | |
| 333 | |
| 334 mGreeting.setText(getString(R.string.inst_host_list)); | |
| 335 | |
| 336 ArrayAdapter<JSONObject> displayer = new HostListAdapter(mUi, R.layo ut.host); | |
| 337 Log.i("hostlist", "About to populate host list display"); | |
| 338 try { | |
| 339 int index = 0; | |
| 340 while (!mHosts.isNull(index)) { | |
| 341 displayer.add(mHosts.getJSONObject(index)); | |
| 342 ++index; | |
| 343 } | |
| 344 mList.setAdapter(displayer); | |
| 345 } | |
| 346 catch(JSONException ex) { | |
| 347 Log.w("hostlist", ex); | |
| 348 Toast.makeText( | |
| 349 mUi, getString(R.string.error_cataloging_hosts), Toast.L ENGTH_LONG).show(); | |
| 350 | |
| 351 // Close the application. | |
| 352 finish(); | |
| 353 } | |
| 354 } | |
| 355 } | |
| 356 | |
| 357 /** Describes the appearance and behavior of each host list entry. */ | |
| 358 private class HostListAdapter extends ArrayAdapter<JSONObject> { | |
| 359 /** Constructor. */ | |
| 360 public HostListAdapter(Context context, int textViewResourceId) { | |
| 361 super(context, textViewResourceId); | |
| 362 } | 341 } |
| 363 | 342 |
| 364 /** Generates a View corresponding to this particular host. */ | 343 mGreeting.setText(getString(R.string.inst_host_list)); |
| 365 @Override | |
| 366 public View getView(int position, View convertView, ViewGroup parent) { | |
| 367 TextView target = (TextView)super.getView(position, convertView, par ent); | |
| 368 | 344 |
| 369 try { | 345 ArrayAdapter<JSONObject> displayer = new HostListAdapter(this, R.layout. host); |
| 370 final JSONObject host = getItem(position); | 346 Log.i("hostlist", "About to populate host list display"); |
| 371 target.setText(Html.fromHtml(host.getString("hostName") + " (<fo nt color = \"" + | 347 try { |
| 372 (host.getString("status").equals("ONLINE") ? HOST_COLOR_ ONLINE : | 348 int index = 0; |
| 373 HOST_COLOR_OFFLINE) + "\">" + host.getString("status") + "</font>)")); | 349 while (!mHosts.isNull(index)) { |
| 350 displayer.add(mHosts.getJSONObject(index)); | |
| 351 ++index; | |
| 352 } | |
| 353 mList.setAdapter(displayer); | |
| 354 } catch (JSONException ex) { | |
| 355 Log.w("hostlist", ex); | |
| 356 Toast.makeText(this, getString(R.string.error_cataloging_hosts), | |
| 357 Toast.LENGTH_LONG).show(); | |
| 374 | 358 |
| 375 if (host.getString("status").equals("ONLINE")) { // Host is onl ine. | 359 // Close the application. |
| 376 target.setOnClickListener(new View.OnClickListener() { | 360 finish(); |
| 377 @Override | |
| 378 public void onClick(View v) { | |
| 379 try { | |
| 380 synchronized (getContext()) { | |
| 381 JniInterface.connectToHost(mAccount.name , mToken, | |
| 382 host.getString("jabberId"), | |
| 383 host.getString("hostId"), | |
| 384 host.getString("publicKey"), | |
| 385 new Runnable() { | |
| 386 @Override | |
| 387 public void run() { | |
| 388 startActivity( | |
| 389 new Intent(getContext(), Desktop.class)); | |
| 390 } | |
| 391 }); | |
| 392 } | |
| 393 } | |
| 394 catch(JSONException ex) { | |
| 395 Log.w("host", ex); | |
| 396 Toast.makeText(getContext(), | |
| 397 getString(R.string.error_reading_hos t), | |
| 398 Toast.LENGTH_LONG).show(); | |
| 399 | |
| 400 // Close the application. | |
| 401 finish(); | |
| 402 } | |
| 403 } | |
| 404 }); | |
| 405 } else { // Host is offline. | |
| 406 // Disallow interaction with this entry. | |
| 407 target.setEnabled(false); | |
| 408 } | |
| 409 } | |
| 410 catch(JSONException ex) { | |
| 411 Log.w("hostlist", ex); | |
| 412 Toast.makeText(getContext(), | |
| 413 getString(R.string.error_displaying_host), | |
| 414 Toast.LENGTH_LONG).show(); | |
| 415 | |
| 416 // Close the application. | |
| 417 finish(); | |
| 418 } | |
| 419 | |
| 420 return target; | |
| 421 } | 361 } |
| 422 } | 362 } |
| 423 } | 363 } |
| OLD | NEW |