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.app.Activity;
14 import android.content.Context;
15 import android.content.Intent;
16 import android.content.SharedPreferences;
17 import android.os.Bundle;
18 import android.os.Handler;
19 import android.os.HandlerThread;
20 import android.text.Html;
21 import android.util.Log;
22 import android.view.Menu;
23 import android.view.MenuItem;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.widget.ArrayAdapter;
27 import android.widget.TextView;
28 import android.widget.ListView;
29 import android.widget.Toast;
31 import org.chromium.chromoting.jni.JniInterface;
32 import org.json.JSONArray;
33 import org.json.JSONException;
34 import org.json.JSONObject;
36 import java.io.IOException;
38 import java.net.URLConnection;
39 import java.util.Scanner;
42 * The user interface for querying and displaying a user's host list from the directory server. It
43 * also requests and renews authentication tokens using the system account manager.
45 public class Chromoting extends Activity {
46 /** Only accounts of this type will be selectable for authentication. */
47 private static final String ACCOUNT_TYPE = "com.google";
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";
53 /** Path from which to download a user's host list JSON object. */
54 private static final String HOST_LIST_PATH =
55 "https://www.googleapis.com/chromoting/v1/@me/hosts?key=";
57 /** Color to use for hosts that are online. */
58 private static final String HOST_COLOR_ONLINE = "green";
60 /** Color to use for hosts that are offline. */
61 private static final String HOST_COLOR_OFFLINE = "red";
63 /** User's account details. */
64 private Account mAccount;
66 /** Account auth token. */
67 private String mToken;
70 private JSONArray mHosts;
72 /** Refresh button. */
73 private MenuItem mRefreshButton;
75 /** Account switcher. */
76 private MenuItem mAccountSwitcher;
78 /** Greeting at the top of the displayed list. */
79 private TextView mGreeting;
81 /** Host list as it appears to the user. */
82 private ListView mList;
84 /** Callback handler to be used for network operations. */
85 private Handler mNetwork;
88 * Called when the activity is first created. Loads the native library and requests an
89 * authentication token from the system.
92 public void onCreate(Bundle savedInstanceState) {
93 super.onCreate(savedInstanceState);
94 setContentView(R.layout.main);
96 // Get ahold of our view widgets.
97 mGreeting = (TextView)findViewById(R.id.hostList_greeting);
98 mList = (ListView)findViewById(R.id.hostList_chooser);
100 // Bring native components online.
101 JniInterface.loadLibrary(this);
103 // Thread responsible for downloading/displaying host list.
104 HandlerThread thread = new HandlerThread("auth_callback");
106 mNetwork = new Handler(thread.getLooper());
108 SharedPreferences prefs = getPreferences(MODE_PRIVATE);
109 if (prefs.contains("account_name") && prefs.contains("account_type")) {
110 // Perform authentication using saved account selection.
111 mAccount = new Account(prefs.getString("account_name", null),
112 prefs.getString("account_type", null));
113 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this,
114 new HostListDirectoryGrabber(this), mNetwork);
115 if (mAccountSwitcher != null) {
116 mAccountSwitcher.setTitle(mAccount.name);
119 // Request auth callback once user has chosen an account.
120 Log.i("auth", "Requesting auth token from system");
121 AccountManager.get(this).getAuthTokenByFeatures(
128 new HostListDirectoryGrabber(this),
134 /** Called when the activity is finally finished. */
136 public void onDestroy() {
138 JniInterface.disconnectFromHost();
141 /** Called to initialize the action bar. */
143 public boolean onCreateOptionsMenu(Menu menu) {
144 getMenuInflater().inflate(R.menu.chromoting_actionbar, menu);
145 mRefreshButton = menu.findItem(R.id.actionbar_directoryrefresh);
146 mAccountSwitcher = menu.findItem(R.id.actionbar_accountswitcher);
148 Account[] usableAccounts = AccountManager.get(this).getAccountsByType(ACCOUNT_TYPE);
149 if (usableAccounts.length == 1 && usableAccounts[0].equals(mAccount)) {
150 // If we're using the only available account, don't offer account switching.
151 // (If there are *no* accounts available, clicking this allows you to add a new one.)
152 mAccountSwitcher.setEnabled(false);
155 if (mAccount == null) {
156 // If no account has been chosen, don't allow the user to refresh the listing.
157 mRefreshButton.setEnabled(false);
159 // If the user has picked an account, show its name directly on the account switcher.
160 mAccountSwitcher.setTitle(mAccount.name);
163 return super.onCreateOptionsMenu(menu);
166 /** Called whenever an action bar button is pressed. */
168 public boolean onOptionsItemSelected(MenuItem item) {
169 if (item == mAccountSwitcher) {
170 // The account switcher triggers a listing of all available accounts.
171 AccountManager.get(this).getAuthTokenByFeatures(
178 new HostListDirectoryGrabber(this),
183 // The refresh button simply makes use of the currently-chosen account.
184 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this,
185 new HostListDirectoryGrabber(this), mNetwork);
192 * Processes the authentication token once the system provides it. Once in possession of such a
193 * token, attempts to request a host list from the directory server. In case of a bad response,
194 * this is retried once in case the system's cached auth token had expired.
196 private class HostListDirectoryGrabber implements AccountManagerCallback<Bundle> {
197 /** Whether authentication has already been attempted. */
198 private boolean mAlreadyTried;
200 /** Communication with the screen. */
201 private Activity mUi;
204 public HostListDirectoryGrabber(Activity ui) {
205 mAlreadyTried = false;
210 * Retrieves the host list from the directory server. This method performs
211 * network operations and must be run an a non-UI thread.
214 public void run(AccountManagerFuture<Bundle> future) {
215 Log.i("auth", "User finished with auth dialogs");
217 // Here comes our auth token from the Android system.
218 Bundle result = future.getResult();
219 String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME);
220 String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
221 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
222 Log.i("auth", "Received an auth token from system");
225 mAccount = new Account(accountName, accountType);
227 getPreferences(MODE_PRIVATE).edit().putString("account_name", accountName).
228 putString("account_type", accountType).apply();
231 // Send our HTTP request to the directory server.
233 new URL(HOST_LIST_PATH + JniInterface.nativeGetApiKey()).openConnection();
234 link.addRequestProperty("client_id", JniInterface.nativeGetClientId());
235 link.addRequestProperty("client_secret", JniInterface.nativeGetClientSecret());
236 link.setRequestProperty("Authorization", "OAuth " + authToken);
238 // Listen for the server to respond.
239 StringBuilder response = new StringBuilder();
240 Scanner incoming = new Scanner(link.getInputStream());
241 Log.i("auth", "Successfully authenticated to directory server");
242 while (incoming.hasNext()) {
243 response.append(incoming.nextLine());
247 // Interpret what the directory server told us.
248 JSONObject data = new JSONObject(String.valueOf(response)).getJSONObject("data");
249 mHosts = data.getJSONArray("items");
250 Log.i("hostlist", "Received host listing from directory server");
251 } catch (RuntimeException ex) {
252 // Make sure any other failure is reported to the user (as an unknown error).
254 } catch (Exception ex) {
255 // Assemble error message to display to the user.
256 String explanation = getString(R.string.error_unknown);
257 if (ex instanceof OperationCanceledException) {
258 explanation = getString(R.string.error_auth_canceled);
259 } else if (ex instanceof AuthenticatorException) {
260 explanation = getString(R.string.error_no_accounts);
261 } else if (ex instanceof IOException) {
262 if (!mAlreadyTried) {
263 // This was our first connection attempt.
266 if (mAccount != null) {
267 // We got an account, but couldn't log into it. We'll retry in case
268 // the system's cached authentication token had already expired.
269 AccountManager authenticator = AccountManager.get(mUi);
270 mAlreadyTried = true;
272 Log.w("auth", "Requesting renewal of rejected auth token");
273 authenticator.invalidateAuthToken(mAccount.type, mToken);
275 authenticator.getAuthToken(
276 mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork);
278 // We're not in an error state *yet*.
283 // We didn't even get an account, so the auth server is likely unreachable.
284 explanation = getString(R.string.error_bad_connection);
286 // Authentication truly failed.
287 Log.e("auth", "Fresh auth token was also rejected");
288 explanation = getString(R.string.error_auth_failed);
290 } else if (ex instanceof JSONException) {
291 explanation = getString(R.string.error_unexpected_response);
292 runOnUiThread(new HostListDisplayer(mUi));
297 Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show();
300 // Share our findings with the user.
301 runOnUiThread(new HostListDisplayer(mUi));
305 /** Formats the host list and offers it to the user. */
306 private class HostListDisplayer implements Runnable {
307 /** Communication with the screen. */
308 private Activity mUi;
311 public HostListDisplayer(Activity ui) {
316 * Updates the infotext and host list display.
317 * This method affects the UI and must be run on its same thread.
322 mRefreshButton.setEnabled(mAccount != null);
323 if (mAccount != null) {
324 mAccountSwitcher.setTitle(mAccount.name);
328 if (mHosts == null) {
329 mGreeting.setText(getString(R.string.inst_empty_list));
330 mList.setAdapter(null);
334 mGreeting.setText(getString(R.string.inst_host_list));
336 ArrayAdapter<JSONObject> displayer = new HostListAdapter(mUi, R.layout.host);
337 Log.i("hostlist", "About to populate host list display");
340 while (!mHosts.isNull(index)) {
341 displayer.add(mHosts.getJSONObject(index));
344 mList.setAdapter(displayer);
346 catch(JSONException ex) {
347 Log.w("hostlist", ex);
349 mUi, getString(R.string.error_cataloging_hosts), Toast.LENGTH_LONG).show();
351 // Close the application.
357 /** Describes the appearance and behavior of each host list entry. */
358 private class HostListAdapter extends ArrayAdapter<JSONObject> {
360 public HostListAdapter(Context context, int textViewResourceId) {
361 super(context, textViewResourceId);
364 /** Generates a View corresponding to this particular host. */
366 public View getView(int position, View convertView, ViewGroup parent) {
367 TextView target = (TextView)super.getView(position, convertView, parent);
370 final JSONObject host = getItem(position);
371 target.setText(Html.fromHtml(host.getString("hostName") + " (<font color = \"" +
372 (host.getString("status").equals("ONLINE") ? HOST_COLOR_ONLINE :
373 HOST_COLOR_OFFLINE) + "\">" + host.getString("status") + "</font>)"));
375 if (host.getString("status").equals("ONLINE")) { // Host is online.
376 target.setOnClickListener(new View.OnClickListener() {
378 public void onClick(View v) {
380 synchronized (getContext()) {
381 JniInterface.connectToHost(mAccount.name, mToken,
382 host.getString("jabberId"),
383 host.getString("hostId"),
384 host.getString("publicKey"),
389 new Intent(getContext(), Desktop.class));
394 catch(JSONException ex) {
396 Toast.makeText(getContext(),
397 getString(R.string.error_reading_host),
398 Toast.LENGTH_LONG).show();
400 // Close the application.
405 } else { // Host is offline.
406 // Disallow interaction with this entry.
407 target.setEnabled(false);
410 catch(JSONException ex) {
411 Log.w("hostlist", ex);
412 Toast.makeText(getContext(),
413 getString(R.string.error_displaying_host),
414 Toast.LENGTH_LONG).show();
416 // Close the application.