Updating trunk VERSION from 2139.0 to 2140.0
[chromium-blink-merge.git] / remoting / android / java / src / org / chromium / chromoting / Chromoting.java
blobf0901bc2d8e4655d9eadede2411628cee3035e7c
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
3 // found in the LICENSE file.
5 package org.chromium.chromoting;
7 import android.accounts.Account;
8 import android.accounts.AccountManager;
9 import android.accounts.AccountManagerCallback;
10 import android.accounts.AccountManagerFuture;
11 import android.accounts.AuthenticatorException;
12 import android.accounts.OperationCanceledException;
13 import android.annotation.SuppressLint;
14 import android.app.ActionBar;
15 import android.app.Activity;
16 import android.app.AlertDialog;
17 import android.app.ProgressDialog;
18 import android.content.DialogInterface;
19 import android.content.Intent;
20 import android.content.SharedPreferences;
21 import android.content.res.Configuration;
22 import android.os.Bundle;
23 import android.provider.Settings;
24 import android.util.Log;
25 import android.view.Menu;
26 import android.view.MenuItem;
27 import android.view.View;
28 import android.widget.ArrayAdapter;
29 import android.widget.ListView;
30 import android.widget.Toast;
32 import org.chromium.chromoting.jni.JniInterface;
34 import java.io.IOException;
35 import java.util.Arrays;
37 /**
38 * The user interface for querying and displaying a user's host list from the directory server. It
39 * also requests and renews authentication tokens using the system account manager.
41 public class Chromoting extends Activity implements JniInterface.ConnectionListener,
42 AccountManagerCallback<Bundle>, ActionBar.OnNavigationListener, HostListLoader.Callback,
43 View.OnClickListener {
44 /** Only accounts of this type will be selectable for authentication. */
45 private static final String ACCOUNT_TYPE = "com.google";
47 /** Scopes at which the authentication token we request will be valid. */
48 private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/chromoting " +
49 "https://www.googleapis.com/auth/googletalk";
51 /** Web page to be displayed in the Help screen when launched from this activity. */
52 private static final String HELP_URL =
53 "http://support.google.com/chrome/?p=mobile_crd_hostslist";
55 /** Web page to be displayed when user triggers the hyperlink for setting up hosts. */
56 private static final String HOST_SETUP_URL =
57 "https://support.google.com/chrome/answer/1649523";
59 /** User's account details. */
60 private Account mAccount;
62 /** List of accounts on the system. */
63 private Account[] mAccounts;
65 /** SpinnerAdapter used in the action bar for selecting accounts. */
66 private AccountsAdapter mAccountsAdapter;
68 /** Account auth token. */
69 private String mToken;
71 /** Helper for fetching the host list. */
72 private HostListLoader mHostListLoader;
74 /** List of hosts. */
75 private HostInfo[] mHosts = new HostInfo[0];
77 /** Refresh button. */
78 private MenuItem mRefreshButton;
80 /** Host list as it appears to the user. */
81 private ListView mHostListView;
83 /** Progress view shown instead of the host list when the host list is loading. */
84 private View mProgressView;
86 /** Dialog for reporting connection progress. */
87 private ProgressDialog mProgressIndicator;
89 /** Object for fetching OAuth2 access tokens from third party authorization servers. */
90 private ThirdPartyTokenFetcher mTokenFetcher;
92 /**
93 * This is set when receiving an authentication error from the HostListLoader. If that occurs,
94 * this flag is set and a fresh authentication token is fetched from the AccountsService, and
95 * used to request the host list a second time.
97 boolean mTriedNewAuthToken;
99 /** Shows a warning explaining that a Google account is required, then closes the activity. */
100 private void showNoAccountsDialog() {
101 AlertDialog.Builder builder = new AlertDialog.Builder(this);
102 builder.setMessage(R.string.noaccounts_message);
103 builder.setPositiveButton(R.string.noaccounts_add_account,
104 new DialogInterface.OnClickListener() {
105 @SuppressLint("InlinedApi")
106 @Override
107 public void onClick(DialogInterface dialog, int id) {
108 Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
109 intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES,
110 new String[] { ACCOUNT_TYPE });
111 if (intent.resolveActivity(getPackageManager()) != null) {
112 startActivity(intent);
114 finish();
117 builder.setNegativeButton(R.string.close, new DialogInterface.OnClickListener() {
118 @Override
119 public void onClick(DialogInterface dialog, int id) {
120 finish();
123 builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
124 @Override
125 public void onCancel(DialogInterface dialog) {
126 finish();
130 AlertDialog dialog = builder.create();
131 dialog.show();
134 /** Shows or hides the progress indicator for loading the host list. */
135 private void setHostListProgressVisible(boolean visible) {
136 mHostListView.setVisibility(visible ? View.GONE : View.VISIBLE);
137 mProgressView.setVisibility(visible ? View.VISIBLE : View.GONE);
139 // Hiding the host-list does not automatically hide the empty view, so do that here.
140 if (visible) {
141 mHostListView.getEmptyView().setVisibility(View.GONE);
146 * Called when the activity is first created. Loads the native library and requests an
147 * authentication token from the system.
149 @Override
150 public void onCreate(Bundle savedInstanceState) {
151 super.onCreate(savedInstanceState);
152 setContentView(R.layout.main);
154 mTriedNewAuthToken = false;
155 mHostListLoader = new HostListLoader();
157 // Get ahold of our view widgets.
158 mHostListView = (ListView)findViewById(R.id.hostList_chooser);
159 mHostListView.setEmptyView(findViewById(R.id.hostList_empty));
160 mProgressView = findViewById(R.id.hostList_progress);
162 findViewById(R.id.host_setup_link_android).setOnClickListener(this);
164 // Bring native components online.
165 JniInterface.loadLibrary(this);
168 @Override
169 protected void onNewIntent(Intent intent) {
170 super.onNewIntent(intent);
171 if (mTokenFetcher != null) {
172 if (mTokenFetcher.handleTokenFetched(intent)) {
173 mTokenFetcher = null;
178 * Called when the activity becomes visible. This happens on initial launch and whenever the
179 * user switches to the activity, for example, by using the window-switcher or when coming from
180 * the device's lock screen.
182 @Override
183 public void onStart() {
184 super.onStart();
186 mAccounts = AccountManager.get(this).getAccountsByType(ACCOUNT_TYPE);
187 if (mAccounts.length == 0) {
188 showNoAccountsDialog();
189 return;
192 SharedPreferences prefs = getPreferences(MODE_PRIVATE);
193 int index = -1;
194 if (prefs.contains("account_name") && prefs.contains("account_type")) {
195 mAccount = new Account(prefs.getString("account_name", null),
196 prefs.getString("account_type", null));
197 index = Arrays.asList(mAccounts).indexOf(mAccount);
199 if (index == -1) {
200 // Preference not loaded, or does not correspond to a valid account, so just pick the
201 // first account arbitrarily.
202 index = 0;
203 mAccount = mAccounts[0];
206 if (mAccounts.length == 1) {
207 getActionBar().setDisplayShowTitleEnabled(true);
208 getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
209 getActionBar().setTitle(R.string.mode_me2me);
210 getActionBar().setSubtitle(mAccount.name);
211 } else {
212 mAccountsAdapter = new AccountsAdapter(this, mAccounts);
213 getActionBar().setDisplayShowTitleEnabled(false);
214 getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
215 getActionBar().setListNavigationCallbacks(mAccountsAdapter, this);
216 getActionBar().setSelectedNavigationItem(index);
219 refreshHostList();
222 /** Called when the activity is finally finished. */
223 @Override
224 public void onDestroy() {
225 super.onDestroy();
226 JniInterface.disconnectFromHost();
229 /** Called when the display is rotated (as registered in the manifest). */
230 @Override
231 public void onConfigurationChanged(Configuration newConfig) {
232 super.onConfigurationChanged(newConfig);
234 // Reload the spinner resources, since the font sizes are dependent on the screen
235 // orientation.
236 if (mAccounts.length != 1) {
237 mAccountsAdapter.notifyDataSetChanged();
241 /** Called to initialize the action bar. */
242 @Override
243 public boolean onCreateOptionsMenu(Menu menu) {
244 getMenuInflater().inflate(R.menu.chromoting_actionbar, menu);
245 mRefreshButton = menu.findItem(R.id.actionbar_directoryrefresh);
247 if (mAccount == null) {
248 // If there is no account, don't allow the user to refresh the listing.
249 mRefreshButton.setEnabled(false);
252 return super.onCreateOptionsMenu(menu);
255 /** Called whenever an action bar button is pressed. */
256 @Override
257 public boolean onOptionsItemSelected(MenuItem item) {
258 int id = item.getItemId();
259 if (id == R.id.actionbar_directoryrefresh) {
260 refreshHostList();
261 return true;
263 if (id == R.id.actionbar_help) {
264 HelpActivity.launch(this, HELP_URL);
265 return true;
267 return super.onOptionsItemSelected(item);
270 /** Called when the user touches hyperlinked text. */
271 @Override
272 public void onClick(View view) {
273 HelpActivity.launch(this, HOST_SETUP_URL);
276 /** Called when the user taps on a host entry. */
277 public void connectToHost(HostInfo host) {
278 mProgressIndicator = ProgressDialog.show(this,
279 host.name, getString(R.string.footer_connecting), true, true,
280 new DialogInterface.OnCancelListener() {
281 @Override
282 public void onCancel(DialogInterface dialog) {
283 JniInterface.disconnectFromHost();
284 mTokenFetcher = null;
287 SessionConnector connector = new SessionConnector(this, this, mHostListLoader);
288 assert mTokenFetcher == null;
289 mTokenFetcher = createTokenFetcher(host);
290 connector.connectToHost(mAccount.name, mToken, host);
293 private void refreshHostList() {
294 mTriedNewAuthToken = false;
295 setHostListProgressVisible(true);
297 // The refresh button simply makes use of the currently-chosen account.
298 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null);
301 @Override
302 public void run(AccountManagerFuture<Bundle> future) {
303 Log.i("auth", "User finished with auth dialogs");
304 Bundle result = null;
305 String explanation = null;
306 try {
307 // Here comes our auth token from the Android system.
308 result = future.getResult();
309 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
310 Log.i("auth", "Received an auth token from system");
312 mToken = authToken;
314 mHostListLoader.retrieveHostList(authToken, this);
315 } catch (OperationCanceledException ex) {
316 // User canceled authentication. No need to report an error.
317 } catch (AuthenticatorException ex) {
318 explanation = getString(R.string.error_unexpected);
319 } catch (IOException ex) {
320 explanation = getString(R.string.error_network_error);
323 if (result == null) {
324 if (explanation != null) {
325 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
327 return;
330 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
331 Log.i("auth", "Received an auth token from system");
333 mToken = authToken;
335 mHostListLoader.retrieveHostList(authToken, this);
338 @Override
339 public boolean onNavigationItemSelected(int itemPosition, long itemId) {
340 mAccount = mAccounts[itemPosition];
342 getPreferences(MODE_PRIVATE).edit().putString("account_name", mAccount.name).
343 putString("account_type", mAccount.type).apply();
345 // The current host list is no longer valid for the new account, so clear the list.
346 mHosts = new HostInfo[0];
347 updateUi();
348 refreshHostList();
349 return true;
352 @Override
353 public void onHostListReceived(HostInfo[] hosts) {
354 // Store a copy of the array, so that it can't be mutated by the HostListLoader. HostInfo
355 // is an immutable type, so a shallow copy of the array is sufficient here.
356 mHosts = Arrays.copyOf(hosts, hosts.length);
357 setHostListProgressVisible(false);
358 updateUi();
361 @Override
362 public void onError(HostListLoader.Error error) {
363 String explanation = null;
364 switch (error) {
365 case AUTH_FAILED:
366 break;
367 case NETWORK_ERROR:
368 explanation = getString(R.string.error_network_error);
369 break;
370 case UNEXPECTED_RESPONSE:
371 case SERVICE_UNAVAILABLE:
372 case UNKNOWN:
373 explanation = getString(R.string.error_unexpected);
374 break;
375 default:
376 // Unreachable.
377 return;
380 if (explanation != null) {
381 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
382 setHostListProgressVisible(false);
383 return;
386 // This is the AUTH_FAILED case.
388 if (!mTriedNewAuthToken) {
389 // This was our first connection attempt.
391 AccountManager authenticator = AccountManager.get(this);
392 mTriedNewAuthToken = true;
394 Log.w("auth", "Requesting renewal of rejected auth token");
395 authenticator.invalidateAuthToken(mAccount.type, mToken);
396 mToken = null;
397 authenticator.getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null);
399 // We're not in an error state *yet*.
400 return;
401 } else {
402 // Authentication truly failed.
403 Log.e("auth", "Fresh auth token was also rejected");
404 explanation = getString(R.string.error_authentication_failed);
405 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
406 setHostListProgressVisible(false);
411 * Updates the infotext and host list display.
413 private void updateUi() {
414 if (mRefreshButton != null) {
415 mRefreshButton.setEnabled(mAccount != null);
417 ArrayAdapter<HostInfo> displayer = new HostListAdapter(this, R.layout.host, mHosts);
418 Log.i("hostlist", "About to populate host list display");
419 mHostListView.setAdapter(displayer);
422 @Override
423 public void onConnectionState(JniInterface.ConnectionListener.State state,
424 JniInterface.ConnectionListener.Error error) {
425 boolean dismissProgress = false;
426 switch (state) {
427 case INITIALIZING:
428 case CONNECTING:
429 case AUTHENTICATED:
430 // The connection is still being established.
431 break;
433 case CONNECTED:
434 dismissProgress = true;
435 // Display the remote desktop.
436 startActivityForResult(new Intent(this, Desktop.class), 0);
437 break;
439 case FAILED:
440 dismissProgress = true;
441 Toast.makeText(this, getString(error.message()), Toast.LENGTH_LONG).show();
442 // Close the Desktop view, if it is currently running.
443 finishActivity(0);
444 break;
446 case CLOSED:
447 // No need to show toast in this case. Either the connection will have failed
448 // because of an error, which will trigger toast already. Or the disconnection will
449 // have been initiated by the user.
450 dismissProgress = true;
451 finishActivity(0);
452 break;
454 default:
455 // Unreachable, but required by Google Java style and findbugs.
456 assert false : "Unreached";
459 if (dismissProgress && mProgressIndicator != null) {
460 mProgressIndicator.dismiss();
461 mProgressIndicator = null;
465 private ThirdPartyTokenFetcher createTokenFetcher(HostInfo host) {
466 ThirdPartyTokenFetcher.Callback callback = new ThirdPartyTokenFetcher.Callback() {
467 public void onTokenFetched(String code, String accessToken) {
468 // The native client sends the OAuth authorization code to the host as the token so
469 // that the host can obtain the shared secret from the third party authorization
470 // server.
471 String token = code;
473 // The native client uses the OAuth access token as the shared secret to
474 // authenticate itself with the host using spake.
475 String sharedSecret = accessToken;
477 JniInterface.onThirdPartyTokenFetched(token, sharedSecret);
480 return new ThirdPartyTokenFetcher(this, host.getTokenUrlPatterns(), callback);
483 public void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) {
484 assert mTokenFetcher != null;
485 mTokenFetcher.fetchToken(tokenUrl, clientId, scope);