blob: cfcbf49a6d7a1faef831df47b090004cd8d6d219 [file] [log] [blame]
Ben Murdochca12bfa2013-07-23 11:17:05 +01001// 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
5package org.chromium.chromoting;
6
7import android.accounts.Account;
8import android.accounts.AccountManager;
9import android.accounts.AccountManagerCallback;
10import android.accounts.AccountManagerFuture;
11import android.accounts.AuthenticatorException;
12import android.accounts.OperationCanceledException;
13import android.app.Activity;
14import android.content.Context;
15import android.content.Intent;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010016import android.content.SharedPreferences;
Ben Murdochca12bfa2013-07-23 11:17:05 +010017import android.os.Bundle;
18import android.os.Handler;
19import android.os.HandlerThread;
20import android.text.Html;
21import android.util.Log;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010022import android.view.Menu;
23import android.view.MenuItem;
Ben Murdochca12bfa2013-07-23 11:17:05 +010024import android.view.View;
25import android.view.ViewGroup;
26import android.widget.ArrayAdapter;
27import android.widget.TextView;
28import android.widget.ListView;
29import android.widget.Toast;
30
31import org.chromium.chromoting.jni.JniInterface;
32import org.json.JSONArray;
33import org.json.JSONException;
34import org.json.JSONObject;
35
36import java.io.IOException;
37import java.net.URL;
38import java.net.URLConnection;
39import java.util.Scanner;
40
41/**
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.
44 */
45public class Chromoting extends Activity {
46 /** Only accounts of this type will be selectable for authentication. */
47 private static final String ACCOUNT_TYPE = "com.google";
48
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";
52
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=";
56
57 /** Color to use for hosts that are online. */
58 private static final String HOST_COLOR_ONLINE = "green";
59
60 /** Color to use for hosts that are offline. */
61 private static final String HOST_COLOR_OFFLINE = "red";
62
63 /** User's account details. */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010064 private Account mAccount;
Ben Murdochca12bfa2013-07-23 11:17:05 +010065
66 /** Account auth token. */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010067 private String mToken;
Ben Murdochca12bfa2013-07-23 11:17:05 +010068
69 /** List of hosts. */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010070 private JSONArray mHosts;
71
72 /** Account switcher. */
73 private MenuItem mAccountSwitcher;
Ben Murdochca12bfa2013-07-23 11:17:05 +010074
75 /** Greeting at the top of the displayed list. */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010076 private TextView mGreeting;
Ben Murdochca12bfa2013-07-23 11:17:05 +010077
78 /** Host list as it appears to the user. */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010079 private ListView mList;
Ben Murdochca12bfa2013-07-23 11:17:05 +010080
81 /** Callback handler to be used for network operations. */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010082 private Handler mNetwork;
Ben Murdochca12bfa2013-07-23 11:17:05 +010083
84 /**
85 * Called when the activity is first created. Loads the native library and requests an
86 * authentication token from the system.
87 */
88 @Override
89 public void onCreate(Bundle savedInstanceState) {
90 super.onCreate(savedInstanceState);
91 setContentView(R.layout.main);
92
93 // Get ahold of our view widgets.
94 mGreeting = (TextView)findViewById(R.id.hostList_greeting);
95 mList = (ListView)findViewById(R.id.hostList_chooser);
96
97 // Bring native components online.
98 JniInterface.loadLibrary(this);
99
100 // Thread responsible for downloading/displaying host list.
101 HandlerThread thread = new HandlerThread("auth_callback");
102 thread.start();
103 mNetwork = new Handler(thread.getLooper());
104
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100105 SharedPreferences prefs = getPreferences(MODE_PRIVATE);
106 if (prefs.contains("account_name") && prefs.contains("account_type")) {
107 // Perform authentication using saved account selection.
108 mAccount = new Account(prefs.getString("account_name", null),
109 prefs.getString("account_type", null));
110 AccountManager.get(this).getAuthToken(mAccount, TOKEN_SCOPE, null, this,
111 new HostListDirectoryGrabber(this), mNetwork);
112 if (mAccountSwitcher != null) {
113 mAccountSwitcher.setTitle(mAccount.name);
114 }
115 } else {
116 // Request auth callback once user has chosen an account.
117 Log.i("auth", "Requesting auth token from system");
118 AccountManager.get(this).getAuthTokenByFeatures(
119 ACCOUNT_TYPE,
120 TOKEN_SCOPE,
121 null,
122 this,
123 null,
124 null,
125 new HostListDirectoryGrabber(this),
126 mNetwork
127 );
128 }
129 }
130
131 /** Called when the activity is finally finished. */
132 @Override
133 public void onDestroy() {
134 super.onDestroy();
135 JniInterface.disconnectFromHost();
136 }
137
138 /** Called to initialize the action bar. */
139 @Override
140 public boolean onCreateOptionsMenu(Menu menu) {
141 getMenuInflater().inflate(R.menu.chromoting_actionbar, menu);
142 mAccountSwitcher = menu.findItem(R.id.actionbar_accountswitcher);
143 if (mAccount != null) {
144 mAccountSwitcher.setTitle(mAccount.name);
145 }
146
147 return super.onCreateOptionsMenu(menu);
148 }
149
150 /** Called whenever an action bar button is pressed. */
151 @Override
152 public boolean onOptionsItemSelected(MenuItem item) {
153 // The only button is the account switcher, so defer to the android accounts system now.
Ben Murdochca12bfa2013-07-23 11:17:05 +0100154 AccountManager.get(this).getAuthTokenByFeatures(
155 ACCOUNT_TYPE,
156 TOKEN_SCOPE,
157 null,
158 this,
159 null,
160 null,
161 new HostListDirectoryGrabber(this),
162 mNetwork
163 );
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100164 return true;
Ben Murdochca12bfa2013-07-23 11:17:05 +0100165 }
166
167 /**
168 * Processes the authentication token once the system provides it. Once in possession of such a
169 * token, attempts to request a host list from the directory server. In case of a bad response,
170 * this is retried once in case the system's cached auth token had expired.
171 */
172 private class HostListDirectoryGrabber implements AccountManagerCallback<Bundle> {
173 /** Whether authentication has already been attempted. */
174 private boolean mAlreadyTried;
175
176 /** Communication with the screen. */
177 private Activity mUi;
178
179 /** Constructor. */
180 public HostListDirectoryGrabber(Activity ui) {
181 mAlreadyTried = false;
182 mUi = ui;
183 }
184
185 /**
186 * Retrieves the host list from the directory server. This method performs
187 * network operations and must be run an a non-UI thread.
188 */
189 @Override
190 public void run(AccountManagerFuture<Bundle> future) {
191 Log.i("auth", "User finished with auth dialogs");
Ben Murdochca12bfa2013-07-23 11:17:05 +0100192 try {
193 // Here comes our auth token from the Android system.
194 Bundle result = future.getResult();
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100195 String accountName = result.getString(AccountManager.KEY_ACCOUNT_NAME);
196 String accountType = result.getString(AccountManager.KEY_ACCOUNT_TYPE);
197 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100198 Log.i("auth", "Received an auth token from system");
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100199
200 synchronized (mUi) {
201 mAccount = new Account(accountName, accountType);
202 mToken = authToken;
203 getPreferences(MODE_PRIVATE).edit().putString("account_name", accountName).
Ben Murdochbb1529c2013-08-08 10:24:53 +0100204 putString("account_type", accountType).apply();
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100205 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100206
207 // Send our HTTP request to the directory server.
208 URLConnection link =
209 new URL(HOST_LIST_PATH + JniInterface.getApiKey()).openConnection();
210 link.addRequestProperty("client_id", JniInterface.getClientId());
211 link.addRequestProperty("client_secret", JniInterface.getClientSecret());
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100212 link.setRequestProperty("Authorization", "OAuth " + authToken);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100213
214 // Listen for the server to respond.
215 StringBuilder response = new StringBuilder();
216 Scanner incoming = new Scanner(link.getInputStream());
217 Log.i("auth", "Successfully authenticated to directory server");
218 while (incoming.hasNext()) {
219 response.append(incoming.nextLine());
220 }
221 incoming.close();
222
223 // Interpret what the directory server told us.
224 JSONObject data = new JSONObject(String.valueOf(response)).getJSONObject("data");
225 mHosts = data.getJSONArray("items");
226 Log.i("hostlist", "Received host listing from directory server");
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100227 } catch (RuntimeException ex) {
228 // Make sure any other failure is reported to the user (as an unknown error).
229 throw ex;
230 } catch (Exception ex) {
Ben Murdochca12bfa2013-07-23 11:17:05 +0100231 // Assemble error message to display to the user.
232 String explanation = getString(R.string.error_unknown);
233 if (ex instanceof OperationCanceledException) {
234 explanation = getString(R.string.error_auth_canceled);
235 } else if (ex instanceof AuthenticatorException) {
236 explanation = getString(R.string.error_no_accounts);
237 } else if (ex instanceof IOException) {
238 if (!mAlreadyTried) { // This was our first connection attempt.
239 Log.w("auth", "Unable to authenticate with (expired?) token");
240
241 // Ask system to renew the auth token in case it expired.
242 AccountManager authenticator = AccountManager.get(mUi);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100243 synchronized (mUi) {
244 authenticator.invalidateAuthToken(mAccount.type, mToken);
245 mToken = null;
246 Log.i("auth", "Requesting auth token renewal");
247 authenticator.getAuthToken(
248 mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork);
249 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100250
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100251 // We're not in an error state *yet*.
252 mAlreadyTried = true;
Ben Murdochca12bfa2013-07-23 11:17:05 +0100253 return;
254 } else { // Authentication truly failed.
255 Log.e("auth", "Fresh auth token was also rejected");
256 explanation = getString(R.string.error_auth_failed);
257 }
258 } else if (ex instanceof JSONException) {
259 explanation = getString(R.string.error_unexpected_response);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100260 runOnUiThread(new HostListDisplayer(mUi));
Ben Murdochca12bfa2013-07-23 11:17:05 +0100261 }
262
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100263 mHosts = null;
Ben Murdochca12bfa2013-07-23 11:17:05 +0100264 Log.w("auth", ex);
265 Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show();
Ben Murdochca12bfa2013-07-23 11:17:05 +0100266 }
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100267
268 // Share our findings with the user.
269 runOnUiThread(new HostListDisplayer(mUi));
Ben Murdochca12bfa2013-07-23 11:17:05 +0100270 }
271 }
272
273 /** Formats the host list and offers it to the user. */
274 private class HostListDisplayer implements Runnable {
275 /** Communication with the screen. */
276 private Activity mUi;
277
278 /** Constructor. */
279 public HostListDisplayer(Activity ui) {
280 mUi = ui;
281 }
282
283 /**
284 * Updates the infotext and host list display.
285 * This method affects the UI and must be run on its same thread.
286 */
287 @Override
288 public void run() {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100289 synchronized (mUi) {
290 mAccountSwitcher.setTitle(mAccount.name);
291 }
292
293 if (mHosts == null) {
294 mGreeting.setText(getString(R.string.inst_empty_list));
295 mList.setAdapter(null);
296 return;
297 }
298
Ben Murdochca12bfa2013-07-23 11:17:05 +0100299 mGreeting.setText(getString(R.string.inst_host_list));
300
301 ArrayAdapter<JSONObject> displayer = new HostListAdapter(mUi, R.layout.host);
302 Log.i("hostlist", "About to populate host list display");
303 try {
304 int index = 0;
305 while (!mHosts.isNull(index)) {
306 displayer.add(mHosts.getJSONObject(index));
307 ++index;
308 }
309 mList.setAdapter(displayer);
310 }
311 catch(JSONException ex) {
312 Log.w("hostlist", ex);
313 Toast.makeText(
314 mUi, getString(R.string.error_cataloging_hosts), Toast.LENGTH_LONG).show();
315
316 // Close the application.
317 finish();
318 }
319 }
320 }
321
322 /** Describes the appearance and behavior of each host list entry. */
323 private class HostListAdapter extends ArrayAdapter<JSONObject> {
324 /** Constructor. */
325 public HostListAdapter(Context context, int textViewResourceId) {
326 super(context, textViewResourceId);
327 }
328
329 /** Generates a View corresponding to this particular host. */
330 @Override
331 public View getView(int position, View convertView, ViewGroup parent) {
332 TextView target = (TextView)super.getView(position, convertView, parent);
333
334 try {
335 final JSONObject host = getItem(position);
336 target.setText(Html.fromHtml(host.getString("hostName") + " (<font color = \"" +
337 (host.getString("status").equals("ONLINE") ? HOST_COLOR_ONLINE :
338 HOST_COLOR_OFFLINE) + "\">" + host.getString("status") + "</font>)"));
339
340 if (host.getString("status").equals("ONLINE")) { // Host is online.
341 target.setOnClickListener(new View.OnClickListener() {
342 @Override
343 public void onClick(View v) {
344 try {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100345 synchronized (getContext()) {
346 JniInterface.connectToHost(mAccount.name, mToken,
347 host.getString("jabberId"),
348 host.getString("hostId"),
349 host.getString("publicKey"),
350 new Runnable() {
351 @Override
352 public void run() {
353 startActivity(
354 new Intent(getContext(), Desktop.class));
355 }
356 });
357 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100358 }
359 catch(JSONException ex) {
360 Log.w("host", ex);
361 Toast.makeText(getContext(),
362 getString(R.string.error_reading_host),
363 Toast.LENGTH_LONG).show();
364
365 // Close the application.
366 finish();
367 }
368 }
369 });
370 } else { // Host is offline.
371 // Disallow interaction with this entry.
372 target.setEnabled(false);
373 }
374 }
375 catch(JSONException ex) {
376 Log.w("hostlist", ex);
377 Toast.makeText(getContext(),
378 getString(R.string.error_displaying_host),
379 Toast.LENGTH_LONG).show();
380
381 // Close the application.
382 finish();
383 }
384
385 return target;
386 }
387 }
388}