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