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