Upstream version 9.38.198.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.ArrayAdapter;
29 import android.widget.ListView;
30 import android.widget.Toast;
31
32 import org.chromium.chromoting.jni.JniInterface;
33
34 import java.io.IOException;
35 import java.util.Arrays;
36
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.
40  */
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";
46
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";
50
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";
54
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";
58
59     /** User's account details. */
60     private Account mAccount;
61
62     /** List of accounts on the system. */
63     private Account[] mAccounts;
64
65     /** SpinnerAdapter used in the action bar for selecting accounts. */
66     private AccountsAdapter mAccountsAdapter;
67
68     /** Account auth token. */
69     private String mToken;
70
71     /** Helper for fetching the host list. */
72     private HostListLoader mHostListLoader;
73
74     /** List of hosts. */
75     private HostInfo[] mHosts = new HostInfo[0];
76
77     /** Refresh button. */
78     private MenuItem mRefreshButton;
79
80     /** Host list as it appears to the user. */
81     private ListView mHostListView;
82
83     /** Progress view shown instead of the host list when the host list is loading. */
84     private View mProgressView;
85
86     /** Dialog for reporting connection progress. */
87     private ProgressDialog mProgressIndicator;
88
89     /** Object for fetching OAuth2 access tokens from third party authorization servers. */
90     private ThirdPartyTokenFetcher mTokenFetcher;
91
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.
96      */
97     boolean mTriedNewAuthToken;
98
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);
113                         }
114                         finish();
115                     }
116                 });
117         builder.setNegativeButton(R.string.close, new DialogInterface.OnClickListener() {
118                 @Override
119                 public void onClick(DialogInterface dialog, int id) {
120                     finish();
121                 }
122             });
123         builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
124                 @Override
125                 public void onCancel(DialogInterface dialog) {
126                     finish();
127                 }
128             });
129
130         AlertDialog dialog = builder.create();
131         dialog.show();
132     }
133
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);
138
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);
142         }
143     }
144
145     /**
146      * Called when the activity is first created. Loads the native library and requests an
147      * authentication token from the system.
148      */
149     @Override
150     public void onCreate(Bundle savedInstanceState) {
151         super.onCreate(savedInstanceState);
152         setContentView(R.layout.main);
153
154         mTriedNewAuthToken = false;
155         mHostListLoader = new HostListLoader();
156
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);
161
162         findViewById(R.id.host_setup_link_android).setOnClickListener(this);
163
164         // Bring native components online.
165         JniInterface.loadLibrary(this);
166     }
167
168     @Override
169     protected void onNewIntent(Intent intent) {
170         super.onNewIntent(intent);
171         if (mTokenFetcher != null) {
172             if (mTokenFetcher.handleTokenFetched(intent)) {
173                 mTokenFetcher = null;
174             }
175         }
176     }
177     /**
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.
181      */
182     @Override
183     public void onStart() {
184         super.onStart();
185
186         mAccounts = AccountManager.get(this).getAccountsByType(ACCOUNT_TYPE);
187         if (mAccounts.length == 0) {
188             showNoAccountsDialog();
189             return;
190         }
191
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);
198         }
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];
204         }
205
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);
217         }
218
219         refreshHostList();
220     }
221
222     /** Called when the activity is finally finished. */
223     @Override
224     public void onDestroy() {
225         super.onDestroy();
226         JniInterface.disconnectFromHost();
227     }
228
229     /** Called when the display is rotated (as registered in the manifest). */
230     @Override
231     public void onConfigurationChanged(Configuration newConfig) {
232         super.onConfigurationChanged(newConfig);
233
234         // Reload the spinner resources, since the font sizes are dependent on the screen
235         // orientation.
236         if (mAccounts.length != 1) {
237             mAccountsAdapter.notifyDataSetChanged();
238         }
239     }
240
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);
246
247         if (mAccount == null) {
248             // If there is no account, don't allow the user to refresh the listing.
249             mRefreshButton.setEnabled(false);
250         }
251
252         return super.onCreateOptionsMenu(menu);
253     }
254
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;
262         }
263         if (id == R.id.actionbar_help) {
264             HelpActivity.launch(this, HELP_URL);
265             return true;
266         }
267         return super.onOptionsItemSelected(item);
268     }
269
270     /** Called when the user touches hyperlinked text. */
271     @Override
272     public void onClick(View view) {
273         HelpActivity.launch(this, HOST_SETUP_URL);
274     }
275
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;
285                   }
286               });
287         SessionConnector connector = new SessionConnector(this, this, mHostListLoader);
288         assert mTokenFetcher == null;
289         mTokenFetcher = createTokenFetcher(host);
290         connector.connectToHost(mAccount.name, mToken, host);
291     }
292
293     private void refreshHostList() {
294         mTriedNewAuthToken = false;
295         setHostListProgressVisible(true);
296
297         // The refresh button simply makes use of the currently-chosen account.
298         AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null);
299     }
300
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");
311
312             mToken = authToken;
313
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);
321         }
322
323         if (result == null) {
324             if (explanation != null) {
325                 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
326             }
327             return;
328         }
329
330         String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
331         Log.i("auth", "Received an auth token from system");
332
333         mToken = authToken;
334
335         mHostListLoader.retrieveHostList(authToken, this);
336     }
337
338     @Override
339     public boolean onNavigationItemSelected(int itemPosition, long itemId) {
340         mAccount = mAccounts[itemPosition];
341
342         getPreferences(MODE_PRIVATE).edit().putString("account_name", mAccount.name).
343                     putString("account_type", mAccount.type).apply();
344
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;
350     }
351
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();
359     }
360
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;
378         }
379
380         if (explanation != null) {
381             Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
382             setHostListProgressVisible(false);
383             return;
384         }
385
386         // This is the AUTH_FAILED case.
387
388         if (!mTriedNewAuthToken) {
389             // This was our first connection attempt.
390
391             AccountManager authenticator = AccountManager.get(this);
392             mTriedNewAuthToken = true;
393
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);
398
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);
407         }
408     }
409
410     /**
411      * Updates the infotext and host list display.
412      */
413     private void updateUi() {
414         if (mRefreshButton != null) {
415             mRefreshButton.setEnabled(mAccount != null);
416         }
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);
420     }
421
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;
432
433             case CONNECTED:
434                 dismissProgress = true;
435                 // Display the remote desktop.
436                 startActivityForResult(new Intent(this, Desktop.class), 0);
437                 break;
438
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;
445
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;
453
454             default:
455                 // Unreachable, but required by Google Java style and findbugs.
456                 assert false : "Unreached";
457         }
458
459         if (dismissProgress && mProgressIndicator != null) {
460             mProgressIndicator.dismiss();
461             mProgressIndicator = null;
462         }
463     }
464
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;
472
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;
476
477                 JniInterface.onThirdPartyTokenFetched(token, sharedSecret);
478             }
479         };
480         return new ThirdPartyTokenFetcher(this, host.getTokenUrlPatterns(), callback);
481     }
482
483     public void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) {
484         assert mTokenFetcher != null;
485         mTokenFetcher.fetchToken(tokenUrl, clientId, scope);
486     }
487 }