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
.AlertDialog
;
15 import android
.app
.ProgressDialog
;
16 import android
.content
.DialogInterface
;
17 import android
.content
.Intent
;
18 import android
.content
.SharedPreferences
;
19 import android
.content
.res
.Configuration
;
20 import android
.content
.res
.Resources
;
21 import android
.os
.Bundle
;
22 import android
.provider
.Settings
;
23 import android
.support
.v4
.widget
.DrawerLayout
;
24 import android
.support
.v7
.app
.ActionBarActivity
;
25 import android
.support
.v7
.app
.ActionBarDrawerToggle
;
26 import android
.support
.v7
.widget
.Toolbar
;
27 import android
.text
.TextUtils
;
28 import android
.util
.Log
;
29 import android
.view
.Menu
;
30 import android
.view
.MenuItem
;
31 import android
.view
.View
;
32 import android
.widget
.AdapterView
;
33 import android
.widget
.ArrayAdapter
;
34 import android
.widget
.ListView
;
35 import android
.widget
.Spinner
;
36 import android
.widget
.Toast
;
38 import org
.chromium
.chromoting
.jni
.JniInterface
;
40 import java
.io
.IOException
;
41 import java
.util
.Arrays
;
42 import java
.util
.Locale
;
45 * The user interface for querying and displaying a user's host list from the directory server. It
46 * also requests and renews authentication tokens using the system account manager.
48 public class Chromoting
extends ActionBarActivity
implements JniInterface
.ConnectionListener
,
49 AccountManagerCallback
<Bundle
>, AdapterView
.OnItemSelectedListener
, HostListLoader
.Callback
,
50 View
.OnClickListener
{
51 /** Only accounts of this type will be selectable for authentication. */
52 private static final String ACCOUNT_TYPE
= "com.google";
54 /** Scopes at which the authentication token we request will be valid. */
55 private static final String TOKEN_SCOPE
= "oauth2:https://www.googleapis.com/auth/chromoting "
56 + "https://www.googleapis.com/auth/googletalk";
58 /** Web page to be displayed in the Help screen when launched from this activity. */
59 private static final String HELP_URL
=
60 "https://support.google.com/chrome/?p=mobile_crd_hostslist";
62 /** Web page to be displayed when user triggers the hyperlink for setting up hosts. */
63 private static final String HOST_SETUP_URL
=
64 "https://support.google.com/chrome/answer/1649523";
66 /** User's account details. */
67 private Account mAccount
;
69 /** List of accounts on the system. */
70 private Account
[] mAccounts
;
72 /** SpinnerAdapter used in the action bar for selecting accounts. */
73 private AccountsAdapter mAccountsAdapter
;
75 /** Account auth token. */
76 private String mToken
;
78 /** Helper for fetching the host list. */
79 private HostListLoader mHostListLoader
;
82 private HostInfo
[] mHosts
= new HostInfo
[0];
84 /** Refresh button. */
85 private MenuItem mRefreshButton
;
87 /** Host list as it appears to the user. */
88 private ListView mHostListView
;
90 /** Progress view shown instead of the host list when the host list is loading. */
91 private View mProgressView
;
93 /** Dialog for reporting connection progress. */
94 private ProgressDialog mProgressIndicator
;
97 * Helper used by SessionConnection for session authentication. Receives onNewIntent()
98 * notifications to handle third-party authentication.
100 private SessionAuthenticator mAuthenticator
;
103 * This is set when receiving an authentication error from the HostListLoader. If that occurs,
104 * this flag is set and a fresh authentication token is fetched from the AccountsService, and
105 * used to request the host list a second time.
107 boolean mTriedNewAuthToken
;
110 * Flag to track whether a call to AccountManager.getAuthToken() is currently pending.
111 * This avoids infinitely-nested calls in case onStart() gets triggered a second time
112 * while a token is being fetched.
114 private boolean mWaitingForAuthToken
= false;
116 private ActionBarDrawerToggle mDrawerToggle
;
118 /** Shows a warning explaining that a Google account is required, then closes the activity. */
119 private void showNoAccountsDialog() {
120 AlertDialog
.Builder builder
= new AlertDialog
.Builder(this);
121 builder
.setMessage(R
.string
.noaccounts_message
);
122 builder
.setPositiveButton(R
.string
.noaccounts_add_account
,
123 new DialogInterface
.OnClickListener() {
124 @SuppressLint("InlinedApi")
126 public void onClick(DialogInterface dialog
, int id
) {
127 Intent intent
= new Intent(Settings
.ACTION_ADD_ACCOUNT
);
128 intent
.putExtra(Settings
.EXTRA_ACCOUNT_TYPES
,
129 new String
[] { ACCOUNT_TYPE
});
130 if (intent
.resolveActivity(getPackageManager()) != null) {
131 startActivity(intent
);
136 builder
.setNegativeButton(R
.string
.close
, new DialogInterface
.OnClickListener() {
138 public void onClick(DialogInterface dialog
, int id
) {
142 builder
.setOnCancelListener(new DialogInterface
.OnCancelListener() {
144 public void onCancel(DialogInterface dialog
) {
149 AlertDialog dialog
= builder
.create();
153 /** Shows or hides the progress indicator for loading the host list. */
154 private void setHostListProgressVisible(boolean visible
) {
155 mHostListView
.setVisibility(visible ? View
.GONE
: View
.VISIBLE
);
156 mProgressView
.setVisibility(visible ? View
.VISIBLE
: View
.GONE
);
158 // Hiding the host-list does not automatically hide the empty view, so do that here.
160 mHostListView
.getEmptyView().setVisibility(View
.GONE
);
165 * Called when the activity is first created. Loads the native library and requests an
166 * authentication token from the system.
169 public void onCreate(Bundle savedInstanceState
) {
170 super.onCreate(savedInstanceState
);
171 setContentView(R
.layout
.main
);
173 Toolbar toolbar
= (Toolbar
) findViewById(R
.id
.toolbar
);
174 setSupportActionBar(toolbar
);
176 mTriedNewAuthToken
= false;
177 mHostListLoader
= new HostListLoader();
179 // Get ahold of our view widgets.
180 mHostListView
= (ListView
) findViewById(R
.id
.hostList_chooser
);
181 mHostListView
.setEmptyView(findViewById(R
.id
.hostList_empty
));
182 mHostListView
.setOnItemClickListener(
183 new AdapterView
.OnItemClickListener() {
185 public void onItemClick(AdapterView
<?
> parent
, View view
, int position
,
187 onHostClicked(position
);
191 mProgressView
= findViewById(R
.id
.hostList_progress
);
193 findViewById(R
.id
.host_setup_link_android
).setOnClickListener(this);
195 DrawerLayout drawerLayout
= (DrawerLayout
) findViewById(R
.id
.drawer_layout
);
196 mDrawerToggle
= new ActionBarDrawerToggle(this, drawerLayout
,
197 R
.string
.open_navigation_drawer
, R
.string
.close_navigation_drawer
);
198 drawerLayout
.setDrawerListener(mDrawerToggle
);
200 ListView navigationMenu
= (ListView
) findViewById(R
.id
.navigation_menu
);
201 String
[] navigationMenuItems
= new String
[] {
202 getString(R
.string
.actionbar_help
)
204 ArrayAdapter
<String
> adapter
= new ArrayAdapter
<String
>(this, R
.layout
.navigation_list_item
,
205 navigationMenuItems
);
206 navigationMenu
.setAdapter(adapter
);
207 navigationMenu
.setOnItemClickListener(
208 new AdapterView
.OnItemClickListener() {
210 public void onItemClick(AdapterView
<?
> parent
, View view
, int position
,
212 HelpActivity
.launch(Chromoting
.this, HELP_URL
);
216 // Make the navigation drawer icon visible in the ActionBar.
217 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
219 // Bring native components online.
220 JniInterface
.loadLibrary(this);
224 protected void onPostCreate(Bundle savedInstanceState
) {
225 super.onPostCreate(savedInstanceState
);
226 mDrawerToggle
.syncState();
230 protected void onNewIntent(Intent intent
) {
231 super.onNewIntent(intent
);
233 if (mAuthenticator
!= null) {
234 mAuthenticator
.onNewIntent(intent
);
239 * Called when the activity becomes visible. This happens on initial launch and whenever the
240 * user switches to the activity, for example, by using the window-switcher or when coming from
241 * the device's lock screen.
244 public void onStart() {
247 mAccounts
= AccountManager
.get(this).getAccountsByType(ACCOUNT_TYPE
);
248 if (mAccounts
.length
== 0) {
249 showNoAccountsDialog();
253 SharedPreferences prefs
= getPreferences(MODE_PRIVATE
);
255 if (prefs
.contains("account_name") && prefs
.contains("account_type")) {
256 mAccount
= new Account(prefs
.getString("account_name", null),
257 prefs
.getString("account_type", null));
258 index
= Arrays
.asList(mAccounts
).indexOf(mAccount
);
261 // Preference not loaded, or does not correspond to a valid account, so just pick the
262 // first account arbitrarily.
264 mAccount
= mAccounts
[0];
267 getSupportActionBar().setTitle(R
.string
.mode_me2me
);
269 mAccountsAdapter
= new AccountsAdapter(this, mAccounts
);
270 Spinner accountsSpinner
= (Spinner
) findViewById(R
.id
.accounts_spinner
);
271 accountsSpinner
.setAdapter(mAccountsAdapter
);
272 accountsSpinner
.setOnItemSelectedListener(this);
273 accountsSpinner
.setSelection(index
);
278 /** Called when the activity is finally finished. */
280 public void onDestroy() {
282 JniInterface
.disconnectFromHost();
285 /** Called when the display is rotated (as registered in the manifest). */
287 public void onConfigurationChanged(Configuration newConfig
) {
288 super.onConfigurationChanged(newConfig
);
290 mDrawerToggle
.onConfigurationChanged(newConfig
);
293 /** Called to initialize the action bar. */
295 public boolean onCreateOptionsMenu(Menu menu
) {
296 getMenuInflater().inflate(R
.menu
.chromoting_actionbar
, menu
);
297 mRefreshButton
= menu
.findItem(R
.id
.actionbar_directoryrefresh
);
299 if (mAccount
== null) {
300 // If there is no account, don't allow the user to refresh the listing.
301 mRefreshButton
.setEnabled(false);
304 return super.onCreateOptionsMenu(menu
);
307 /** Called whenever an action bar button is pressed. */
309 public boolean onOptionsItemSelected(MenuItem item
) {
310 if (mDrawerToggle
.onOptionsItemSelected(item
)) {
314 int id
= item
.getItemId();
315 if (id
== R
.id
.actionbar_directoryrefresh
) {
319 return super.onOptionsItemSelected(item
);
322 /** Called when the user touches hyperlinked text. */
324 public void onClick(View view
) {
325 HelpActivity
.launch(this, HOST_SETUP_URL
);
328 /** Called when the user taps on a host entry. */
329 private void onHostClicked(int index
) {
330 HostInfo host
= mHosts
[index
];
334 String tooltip
= getHostOfflineTooltip(host
.hostOfflineReason
);
335 Toast
.makeText(this, tooltip
, Toast
.LENGTH_SHORT
).show();
339 private String
getHostOfflineTooltip(String hostOfflineReason
) {
340 if (TextUtils
.isEmpty(hostOfflineReason
)) {
341 return getString(R
.string
.host_offline_tooltip
);
344 String resourceName
= "offline_reason_" + hostOfflineReason
.toLowerCase(Locale
.ENGLISH
);
345 int resourceId
= getResources().getIdentifier(resourceName
, "string",
347 return getString(resourceId
);
348 } catch (Resources
.NotFoundException ignored
) {
349 return getString(R
.string
.offline_reason_unknown
, hostOfflineReason
);
353 private void connectToHost(HostInfo host
) {
354 mProgressIndicator
= ProgressDialog
.show(
357 getString(R
.string
.footer_connecting
),
360 new DialogInterface
.OnCancelListener() {
362 public void onCancel(DialogInterface dialog
) {
363 JniInterface
.disconnectFromHost();
366 SessionConnector connector
= new SessionConnector(this, this, mHostListLoader
);
367 mAuthenticator
= new SessionAuthenticator(this, host
);
368 connector
.connectToHost(mAccount
.name
, mToken
, host
, mAuthenticator
);
371 private void refreshHostList() {
372 if (mWaitingForAuthToken
) {
376 mTriedNewAuthToken
= false;
377 setHostListProgressVisible(true);
379 // The refresh button simply makes use of the currently-chosen account.
383 private void requestAuthToken() {
384 AccountManager
.get(this).getAuthToken(mAccount
, TOKEN_SCOPE
, null, this, this, null);
385 mWaitingForAuthToken
= true;
389 public void run(AccountManagerFuture
<Bundle
> future
) {
390 Log
.i("auth", "User finished with auth dialogs");
391 mWaitingForAuthToken
= false;
393 Bundle result
= null;
394 String explanation
= null;
396 // Here comes our auth token from the Android system.
397 result
= future
.getResult();
398 } catch (OperationCanceledException ex
) {
399 // User canceled authentication. No need to report an error.
400 } catch (AuthenticatorException ex
) {
401 explanation
= getString(R
.string
.error_unexpected
);
402 } catch (IOException ex
) {
403 explanation
= getString(R
.string
.error_network_error
);
406 if (result
== null) {
407 setHostListProgressVisible(false);
408 if (explanation
!= null) {
409 Toast
.makeText(this, explanation
, Toast
.LENGTH_LONG
).show();
414 mToken
= result
.getString(AccountManager
.KEY_AUTHTOKEN
);
415 Log
.i("auth", "Received an auth token from system");
417 mHostListLoader
.retrieveHostList(mToken
, this);
421 public void onItemSelected(AdapterView
<?
> parent
, View view
, int itemPosition
, long itemId
) {
422 mAccount
= mAccounts
[itemPosition
];
424 getPreferences(MODE_PRIVATE
).edit().putString("account_name", mAccount
.name
)
425 .putString("account_type", mAccount
.type
).apply();
427 // The current host list is no longer valid for the new account, so clear the list.
428 mHosts
= new HostInfo
[0];
434 public void onNothingSelected(AdapterView
<?
> parent
) {
438 public void onHostListReceived(HostInfo
[] hosts
) {
439 // Store a copy of the array, so that it can't be mutated by the HostListLoader. HostInfo
440 // is an immutable type, so a shallow copy of the array is sufficient here.
441 mHosts
= Arrays
.copyOf(hosts
, hosts
.length
);
442 setHostListProgressVisible(false);
447 public void onError(HostListLoader
.Error error
) {
448 String explanation
= null;
453 explanation
= getString(R
.string
.error_network_error
);
455 case UNEXPECTED_RESPONSE
:
456 case SERVICE_UNAVAILABLE
:
458 explanation
= getString(R
.string
.error_unexpected
);
465 if (explanation
!= null) {
466 Toast
.makeText(this, explanation
, Toast
.LENGTH_LONG
).show();
467 setHostListProgressVisible(false);
471 // This is the AUTH_FAILED case.
473 if (!mTriedNewAuthToken
) {
474 // This was our first connection attempt.
476 AccountManager authenticator
= AccountManager
.get(this);
477 mTriedNewAuthToken
= true;
479 Log
.w("auth", "Requesting renewal of rejected auth token");
480 authenticator
.invalidateAuthToken(mAccount
.type
, mToken
);
484 // We're not in an error state *yet*.
487 // Authentication truly failed.
488 Log
.e("auth", "Fresh auth token was also rejected");
489 explanation
= getString(R
.string
.error_authentication_failed
);
490 Toast
.makeText(this, explanation
, Toast
.LENGTH_LONG
).show();
491 setHostListProgressVisible(false);
496 * Updates the infotext and host list display.
498 private void updateUi() {
499 if (mRefreshButton
!= null) {
500 mRefreshButton
.setEnabled(mAccount
!= null);
502 ArrayAdapter
<HostInfo
> displayer
= new HostListAdapter(this, R
.layout
.host
, mHosts
);
503 Log
.i("hostlist", "About to populate host list display");
504 mHostListView
.setAdapter(displayer
);
508 public void onConnectionState(JniInterface
.ConnectionListener
.State state
,
509 JniInterface
.ConnectionListener
.Error error
) {
510 boolean dismissProgress
= false;
515 // The connection is still being established.
519 dismissProgress
= true;
520 // Display the remote desktop.
521 startActivityForResult(new Intent(this, Desktop
.class), 0);
525 dismissProgress
= true;
526 Toast
.makeText(this, getString(error
.message()), Toast
.LENGTH_LONG
).show();
527 // Close the Desktop view, if it is currently running.
532 // No need to show toast in this case. Either the connection will have failed
533 // because of an error, which will trigger toast already. Or the disconnection will
534 // have been initiated by the user.
535 dismissProgress
= true;
540 // Unreachable, but required by Google Java style and findbugs.
541 assert false : "Unreached";
544 if (dismissProgress
&& mProgressIndicator
!= null) {
545 mProgressIndicator
.dismiss();
546 mProgressIndicator
= null;