Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / remoting / android / java / src / org / chromium / chromoting / Chromoting.java
blobac8e9e288d8351ccb74ec64daa7f058666ea469a
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;
44 /**
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;
81 /** List of hosts. */
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;
96 /**
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")
125 @Override
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);
133 finish();
136 builder.setNegativeButton(R.string.close, new DialogInterface.OnClickListener() {
137 @Override
138 public void onClick(DialogInterface dialog, int id) {
139 finish();
142 builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
143 @Override
144 public void onCancel(DialogInterface dialog) {
145 finish();
149 AlertDialog dialog = builder.create();
150 dialog.show();
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.
159 if (visible) {
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.
168 @Override
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() {
184 @Override
185 public void onItemClick(AdapterView<?> parent, View view, int position,
186 long id) {
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() {
209 @Override
210 public void onItemClick(AdapterView<?> parent, View view, int position,
211 long id) {
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);
223 @Override
224 protected void onPostCreate(Bundle savedInstanceState) {
225 super.onPostCreate(savedInstanceState);
226 mDrawerToggle.syncState();
229 @Override
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.
243 @Override
244 public void onStart() {
245 super.onStart();
247 mAccounts = AccountManager.get(this).getAccountsByType(ACCOUNT_TYPE);
248 if (mAccounts.length == 0) {
249 showNoAccountsDialog();
250 return;
253 SharedPreferences prefs = getPreferences(MODE_PRIVATE);
254 int index = -1;
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);
260 if (index == -1) {
261 // Preference not loaded, or does not correspond to a valid account, so just pick the
262 // first account arbitrarily.
263 index = 0;
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);
275 refreshHostList();
278 /** Called when the activity is finally finished. */
279 @Override
280 public void onDestroy() {
281 super.onDestroy();
282 JniInterface.disconnectFromHost();
285 /** Called when the display is rotated (as registered in the manifest). */
286 @Override
287 public void onConfigurationChanged(Configuration newConfig) {
288 super.onConfigurationChanged(newConfig);
290 mDrawerToggle.onConfigurationChanged(newConfig);
293 /** Called to initialize the action bar. */
294 @Override
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. */
308 @Override
309 public boolean onOptionsItemSelected(MenuItem item) {
310 if (mDrawerToggle.onOptionsItemSelected(item)) {
311 return true;
314 int id = item.getItemId();
315 if (id == R.id.actionbar_directoryrefresh) {
316 refreshHostList();
317 return true;
319 return super.onOptionsItemSelected(item);
322 /** Called when the user touches hyperlinked text. */
323 @Override
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];
331 if (host.isOnline) {
332 connectToHost(host);
333 } else {
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);
343 try {
344 String resourceName = "offline_reason_" + hostOfflineReason.toLowerCase(Locale.ENGLISH);
345 int resourceId = getResources().getIdentifier(resourceName, "string",
346 getPackageName());
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(
355 this,
356 host.name,
357 getString(R.string.footer_connecting),
358 true,
359 true,
360 new DialogInterface.OnCancelListener() {
361 @Override
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) {
373 return;
376 mTriedNewAuthToken = false;
377 setHostListProgressVisible(true);
379 // The refresh button simply makes use of the currently-chosen account.
380 requestAuthToken();
383 private void requestAuthToken() {
384 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this, this, null);
385 mWaitingForAuthToken = true;
388 @Override
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;
395 try {
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();
411 return;
414 mToken = result.getString(AccountManager.KEY_AUTHTOKEN);
415 Log.i("auth", "Received an auth token from system");
417 mHostListLoader.retrieveHostList(mToken, this);
420 @Override
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];
429 updateUi();
430 refreshHostList();
433 @Override
434 public void onNothingSelected(AdapterView<?> parent) {
437 @Override
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);
443 updateUi();
446 @Override
447 public void onError(HostListLoader.Error error) {
448 String explanation = null;
449 switch (error) {
450 case AUTH_FAILED:
451 break;
452 case NETWORK_ERROR:
453 explanation = getString(R.string.error_network_error);
454 break;
455 case UNEXPECTED_RESPONSE:
456 case SERVICE_UNAVAILABLE:
457 case UNKNOWN:
458 explanation = getString(R.string.error_unexpected);
459 break;
460 default:
461 // Unreachable.
462 return;
465 if (explanation != null) {
466 Toast.makeText(this, explanation, Toast.LENGTH_LONG).show();
467 setHostListProgressVisible(false);
468 return;
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);
481 mToken = null;
482 requestAuthToken();
484 // We're not in an error state *yet*.
485 return;
486 } else {
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);
507 @Override
508 public void onConnectionState(JniInterface.ConnectionListener.State state,
509 JniInterface.ConnectionListener.Error error) {
510 boolean dismissProgress = false;
511 switch (state) {
512 case INITIALIZING:
513 case CONNECTING:
514 case AUTHENTICATED:
515 // The connection is still being established.
516 break;
518 case CONNECTED:
519 dismissProgress = true;
520 // Display the remote desktop.
521 startActivityForResult(new Intent(this, Desktop.class), 0);
522 break;
524 case FAILED:
525 dismissProgress = true;
526 Toast.makeText(this, getString(error.message()), Toast.LENGTH_LONG).show();
527 // Close the Desktop view, if it is currently running.
528 finishActivity(0);
529 break;
531 case CLOSED:
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;
536 finishActivity(0);
537 break;
539 default:
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;