Upstream version 11.40.277.0
[platform/framework/web/crosswalk.git] / src / remoting / android / java / src / org / chromium / chromoting / Chromoting.java
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.
4
5 package org.chromium.chromoting;
6
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.AdapterView;
29 import android.widget.ArrayAdapter;
30 import android.widget.ListView;
31 import android.widget.Spinner;
32 import android.widget.Toast;
33
34 import org.chromium.chromoting.jni.JniInterface;
35
36 import java.io.IOException;
37 import java.util.Arrays;
38
39 /**
40  * The user interface for querying and displaying a user's host list from the directory server. It
41  * also requests and renews authentication tokens using the system account manager.
42  */
43 public class Chromoting extends Activity implements JniInterface.ConnectionListener,
44         AccountManagerCallback<Bundle>, AdapterView.OnItemSelectedListener, HostListLoader.Callback,
45         View.OnClickListener {
46     /** Only accounts of this type will be selectable for authentication. */
47     private static final String ACCOUNT_TYPE = "com.google";
48
49     /** 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 "
51             + "https://www.googleapis.com/auth/googletalk";
52
53     /** Web page to be displayed in the Help screen when launched from this activity. */
54     private static final String HELP_URL =
55             "http://support.google.com/chrome/?p=mobile_crd_hostslist";
56
57     /** Web page to be displayed when user triggers the hyperlink for setting up hosts. */
58     private static final String HOST_SETUP_URL =
59             "https://support.google.com/chrome/answer/1649523";
60
61     /** User's account details. */
62     private Account mAccount;
63
64     /** List of accounts on the system. */
65     private Account[] mAccounts;
66
67     /** SpinnerAdapter used in the action bar for selecting accounts. */
68     private AccountsAdapter mAccountsAdapter;
69
70     /** Account auth token. */
71     private String mToken;
72
73     /** Helper for fetching the host list. */
74     private HostListLoader mHostListLoader;
75
76     /** List of hosts. */
77     private HostInfo[] mHosts = new HostInfo[0];
78
79     /** Refresh button. */
80     private MenuItem mRefreshButton;
81
82     /** Host list as it appears to the user. */
83     private ListView mHostListView;
84
85     /** Progress view shown instead of the host list when the host list is loading. */
86     private View mProgressView;
87
88     /** Dialog for reporting connection progress. */
89     private ProgressDialog mProgressIndicator;
90
91     /** Object for fetching OAuth2 access tokens from third party authorization servers. */
92     private ThirdPartyTokenFetcher mTokenFetcher;
93
94     /**
95      * This is set when receiving an authentication error from the HostListLoader. If that occurs,
96      * this flag is set and a fresh authentication token is fetched from the AccountsService, and
97      * used to request the host list a second time.
98      */
99     boolean mTriedNewAuthToken;
100
101     /**
102      * Flag to track whether a call to AccountManager.getAuthToken() is currently pending.
103      * This avoids infinitely-nested calls in case onStart() gets triggered a second time
104      * while a token is being fetched.
105      */
106     private boolean mWaitingForAuthToken = false;
107
108     /** Shows a warning explaining that a Google account is required, then closes the activity. */
109     private void showNoAccountsDialog() {
110         AlertDialog.Builder builder = new AlertDialog.Builder(this);
111         builder.setMessage(R.string.noaccounts_message);
112         builder.setPositiveButton(R.string.noaccounts_add_account,
113                 new DialogInterface.OnClickListener() {
114                     @SuppressLint("InlinedApi")
115                     @Override
116                     public void onClick(DialogInterface dialog, int id) {
117                         Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
118                         intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES,
119                                 new String[] { ACCOUNT_TYPE });
120                         if (intent.resolveActivity(getPackageManager()) != null) {
121                             startActivity(intent);
122                         }
123                         finish();
124                     }
125                 });
126         builder.setNegativeButton(R.string.close, new DialogInterface.OnClickListener() {
127                 @Override
128                 public void onClick(DialogInterface dialog, int id) {
129                     finish();
130                 }
131             });
132         builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
133                 @Override
134                 public void onCancel(DialogInterface dialog) {
135                     finish();
136                 }
137             });
138
139         AlertDialog dialog = builder.create();
140         dialog.show();
141     }
142
143     /** Shows or hides the progress indicator for loading the host list. */
144     private void setHostListProgressVisible(boolean visible) {
145         mHostListView.setVisibility(visible ? View.GONE : View.VISIBLE);
146         mProgressView.setVisibility(visible ? View.VISIBLE : View.GONE);
147
148         // Hiding the host-list does not automatically hide the empty view, so do that here.
149         if (visible) {
150             mHostListView.getEmptyView().setVisibility(View.GONE);
151         }
152     }
153
154     /**
155      * Called when the activity is first created. Loads the native library and requests an
156      * authentication token from the system.
157      */
158     @Override
159     public void onCreate(Bundle savedInstanceState) {
160         super.onCreate(savedInstanceState);
161         setContentView(R.layout.main);
162
163         mTriedNewAuthToken = false;
164         mHostListLoader = new HostListLoader();
165
166         // Get ahold of our view widgets.
167         mHostListView = (ListView) findViewById(R.id.hostList_chooser);
168         mHostListView.setEmptyView(findViewById(R.id.hostList_empty));
169         mProgressView = findViewById(R.id.hostList_progress);
170
171         findViewById(R.id.host_setup_link_android).setOnClickListener(this);
172
173         // Bring native components online.
174         JniInterface.loadLibrary(this);
175     }
176
177     @Override
178     protected void onNewIntent(Intent intent) {
179         super.onNewIntent(intent);
180         if (mTokenFetcher != null) {
181             if (mTokenFetcher.handleTokenFetched(intent)) {
182                 mTokenFetcher = null;
183             }
184         }
185     }
186
187     /**
188      * Called when the activity becomes visible. This happens on initial launch and whenever the
189      * user switches to the activity, for example, by using the window-switcher or when coming from
190      * the device's lock screen.
191      */
192     @Override
193     public void onStart() {
194         super.onStart();
195
196         mAccounts = AccountManager.get(this).getAccountsByType(ACCOUNT_TYPE);
197         if (mAccounts.length == 0) {
198             showNoAccountsDialog();
199             return;
200         }
201
202         SharedPreferences prefs = getPreferences(MODE_PRIVATE);
203         int index = -1;
204         if (prefs.contains("account_name") && prefs.contains("account_type")) {
205             mAccount = new Account(prefs.getString("account_name", null),
206                     prefs.getString("account_type", null));
207             index = Arrays.asList(mAccounts).indexOf(mAccount);
208         }
209         if (index == -1) {
210             // Preference not loaded, or does not correspond to a valid account, so just pick the
211             // first account arbitrarily.
212             index = 0;
213             mAccount = mAccounts[0];
214         }
215
216         if (mAccounts.length == 1) {
217             getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE,
218                     ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
219             getActionBar().setTitle(R.string.mode_me2me);
220             getActionBar().setSubtitle(mAccount.name);
221         } else {
222             mAccountsAdapter = new AccountsAdapter(this, mAccounts);
223             Spinner accountsSpinner = new Spinner(getActionBar().getThemedContext(), null,
224                     android.R.attr.actionDropDownStyle);
225             accountsSpinner.setAdapter(mAccountsAdapter);
226             getActionBar().setCustomView(accountsSpinner);
227             getActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM,
228                     ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM);
229             accountsSpinner.setOnItemSelectedListener(this);
230             accountsSpinner.setSelection(index);
231         }
232
233         refreshHostList();
234     }
235
236     /** Called when the activity is finally finished. */
237     @Override
238     public void onDestroy() {
239         super.onDestroy();
240         JniInterface.disconnectFromHost();
241     }
242
243     /** Called when the display is rotated (as registered in the manifest). */
244     @Override
245     public void onConfigurationChanged(Configuration newConfig) {
246         super.onConfigurationChanged(newConfig);
247
248         // Reload the spinner resources, since the font sizes are dependent on the screen
249         // orientation.
250         if (mAccounts.length != 1) {
251             mAccountsAdapter.notifyDataSetChanged();
252         }
253     }
254
255     /** Called to initialize the action bar. */
256     @Override
257     public boolean onCreateOptionsMenu(Menu menu) {
258         getMenuInflater().inflate(R.menu.chromoting_actionbar, menu);
259         mRefreshButton = menu.findItem(R.id.actionbar_directoryrefresh);
260
261         if (mAccount == null) {
262             // If there is no account, don't allow the user to refresh the listing.
263             mRefreshButton.setEnabled(false);
264         }
265
266         return super.onCreateOptionsMenu(menu);
267     }
268
269     /** Called whenever an action bar button is pressed. */
270     @Override
271     public boolean onOptionsItemSelected(MenuItem item) {
272         int id = item.getItemId();
273         if (id == R.id.actionbar_directoryrefresh) {
274             refreshHostList();
275             return true;
276         }
277         if (id == R.id.actionbar_help) {
278             HelpActivity.launch(this, HELP_URL);
279             return true;
280         }
281         return super.onOptionsItemSelected(item);
282     }
283
284     /** Called when the user touches hyperlinked text. */
285     @Override
286     public void onClick(View view) {
287         HelpActivity.launch(this, HOST_SETUP_URL);
288     }
289
290     /** Called when the user taps on a host entry. */
291     public void connectToHost(HostInfo host) {
292         mProgressIndicator = ProgressDialog.show(
293                 this,
294                 host.name,
295                 getString(R.string.footer_connecting),
296                 true,
297                 true,
298                 new DialogInterface.OnCancelListener() {
299                     @Override
300                     public void onCancel(DialogInterface dialog) {
301                         JniInterface.disconnectFromHost();
302                         mTokenFetcher = null;
303                     }
304                 });
305         SessionConnector connector = new SessionConnector(this, this, mHostListLoader);
306         assert mTokenFetcher == null;
307         mTokenFetcher = createTokenFetcher(host);
308         connector.connectToHost(mAccount.name, mToken, host);
309     }
310
311     private void refreshHostList() {
312         if (mWaitingForAuthToken) {
313             return;
314         }
315
316         mTriedNewAuthToken = false;
317         setHostListProgressVisible(true);
318
319         // The refresh button simply makes use of the currently-chosen account.
320         requestAuthToken();
321     }
322
323     private void requestAuthToken() {
324         AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null);
325         mWaitingForAuthToken = true;
326     }
327
328     @Override
329     public void run(AccountManagerFuture<Bundle> future) {
330         Log.i("auth", "User finished with auth dialogs");
331         mWaitingForAuthToken = false;
332
333         Bundle result = null;
334         String explanation = null;
335         try {
336             // Here comes our auth token from the Android system.
337             result = future.getResult();
338         } catch (OperationCanceledException ex) {
339             // User canceled authentication. No need to report an error.
340         } catch (AuthenticatorException ex) {
341             explanation = getString(R.string.error_unexpected);
342         } catch (IOException ex) {
343             explanation = getString(R.string.error_network_error);
344         }
345
346         if (result == null) {
347             setHostListProgressVisible(false);
348             if (explanation != null) {
349                 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
350             }
351             return;
352         }
353
354         mToken = result.getString(AccountManager.KEY_AUTHTOKEN);
355         Log.i("auth", "Received an auth token from system");
356
357         mHostListLoader.retrieveHostList(mToken, this);
358     }
359
360     @Override
361     public void onItemSelected(AdapterView parent, View view, int itemPosition, long itemId) {
362         mAccount = mAccounts[itemPosition];
363
364         getPreferences(MODE_PRIVATE).edit().putString("account_name", mAccount.name)
365                 .putString("account_type", mAccount.type).apply();
366
367         // The current host list is no longer valid for the new account, so clear the list.
368         mHosts = new HostInfo[0];
369         updateUi();
370         refreshHostList();
371     }
372
373     @Override
374     public void onNothingSelected(AdapterView parent) {
375     }
376
377     @Override
378     public void onHostListReceived(HostInfo[] hosts) {
379         // Store a copy of the array, so that it can't be mutated by the HostListLoader. HostInfo
380         // is an immutable type, so a shallow copy of the array is sufficient here.
381         mHosts = Arrays.copyOf(hosts, hosts.length);
382         setHostListProgressVisible(false);
383         updateUi();
384     }
385
386     @Override
387     public void onError(HostListLoader.Error error) {
388         String explanation = null;
389         switch (error) {
390             case AUTH_FAILED:
391                 break;
392             case NETWORK_ERROR:
393                 explanation = getString(R.string.error_network_error);
394                 break;
395             case UNEXPECTED_RESPONSE:
396             case SERVICE_UNAVAILABLE:
397             case UNKNOWN:
398                 explanation = getString(R.string.error_unexpected);
399                 break;
400             default:
401                 // Unreachable.
402                 return;
403         }
404
405         if (explanation != null) {
406             Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
407             setHostListProgressVisible(false);
408             return;
409         }
410
411         // This is the AUTH_FAILED case.
412
413         if (!mTriedNewAuthToken) {
414             // This was our first connection attempt.
415
416             AccountManager authenticator = AccountManager.get(this);
417             mTriedNewAuthToken = true;
418
419             Log.w("auth", "Requesting renewal of rejected auth token");
420             authenticator.invalidateAuthToken(mAccount.type, mToken);
421             mToken = null;
422             requestAuthToken();
423
424             // We're not in an error state *yet*.
425             return;
426         } else {
427             // Authentication truly failed.
428             Log.e("auth", "Fresh auth token was also rejected");
429             explanation = getString(R.string.error_authentication_failed);
430             Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
431             setHostListProgressVisible(false);
432         }
433     }
434
435     /**
436      * Updates the infotext and host list display.
437      */
438     private void updateUi() {
439         if (mRefreshButton != null) {
440             mRefreshButton.setEnabled(mAccount != null);
441         }
442         ArrayAdapter<HostInfo> displayer = new HostListAdapter(this, R.layout.host, mHosts);
443         Log.i("hostlist", "About to populate host list display");
444         mHostListView.setAdapter(displayer);
445     }
446
447     @Override
448     public void onConnectionState(JniInterface.ConnectionListener.State state,
449             JniInterface.ConnectionListener.Error error) {
450         boolean dismissProgress = false;
451         switch (state) {
452             case INITIALIZING:
453             case CONNECTING:
454             case AUTHENTICATED:
455                 // The connection is still being established.
456                 break;
457
458             case CONNECTED:
459                 dismissProgress = true;
460                 // Display the remote desktop.
461                 startActivityForResult(new Intent(this, Desktop.class), 0);
462                 break;
463
464             case FAILED:
465                 dismissProgress = true;
466                 Toast.makeText(this, getString(error.message()), Toast.LENGTH_LONG).show();
467                 // Close the Desktop view, if it is currently running.
468                 finishActivity(0);
469                 break;
470
471             case CLOSED:
472                 // No need to show toast in this case. Either the connection will have failed
473                 // because of an error, which will trigger toast already. Or the disconnection will
474                 // have been initiated by the user.
475                 dismissProgress = true;
476                 finishActivity(0);
477                 break;
478
479             default:
480                 // Unreachable, but required by Google Java style and findbugs.
481                 assert false : "Unreached";
482         }
483
484         if (dismissProgress && mProgressIndicator != null) {
485             mProgressIndicator.dismiss();
486             mProgressIndicator = null;
487         }
488     }
489
490     private ThirdPartyTokenFetcher createTokenFetcher(HostInfo host) {
491         ThirdPartyTokenFetcher.Callback callback = new ThirdPartyTokenFetcher.Callback() {
492             @Override
493             public void onTokenFetched(String code, String accessToken) {
494                 // The native client sends the OAuth authorization code to the host as the token so
495                 // that the host can obtain the shared secret from the third party authorization
496                 // server.
497                 String token = code;
498
499                 // The native client uses the OAuth access token as the shared secret to
500                 // authenticate itself with the host using spake.
501                 String sharedSecret = accessToken;
502
503                 JniInterface.onThirdPartyTokenFetched(token, sharedSecret);
504             }
505         };
506         return new ThirdPartyTokenFetcher(this, host.getTokenUrlPatterns(), callback);
507     }
508
509     public void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) {
510         assert mTokenFetcher != null;
511         mTokenFetcher.fetchToken(tokenUrl, clientId, scope);
512     }
513 }