- add sources.
[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.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;
30
31 import org.chromium.chromoting.jni.JniInterface;
32 import org.json.JSONArray;
33 import org.json.JSONException;
34 import org.json.JSONObject;
35
36 import java.io.IOException;
37 import java.net.URL;
38 import java.net.URLConnection;
39 import 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  */
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";
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. */
64     private Account mAccount;
65
66     /** Account auth token. */
67     private String mToken;
68
69     /** List of hosts. */
70     private JSONArray mHosts;
71
72     /** Refresh button. */
73     private MenuItem mRefreshButton;
74
75     /** Account switcher. */
76     private MenuItem mAccountSwitcher;
77
78     /** Greeting at the top of the displayed list. */
79     private TextView mGreeting;
80
81     /** Host list as it appears to the user. */
82     private ListView mList;
83
84     /** Callback handler to be used for network operations. */
85     private Handler mNetwork;
86
87     /**
88      * Called when the activity is first created. Loads the native library and requests an
89      * authentication token from the system.
90      */
91     @Override
92     public void onCreate(Bundle savedInstanceState) {
93         super.onCreate(savedInstanceState);
94         setContentView(R.layout.main);
95
96         // Get ahold of our view widgets.
97         mGreeting = (TextView)findViewById(R.id.hostList_greeting);
98         mList = (ListView)findViewById(R.id.hostList_chooser);
99
100         // Bring native components online.
101         JniInterface.loadLibrary(this);
102
103         // Thread responsible for downloading/displaying host list.
104         HandlerThread thread = new HandlerThread("auth_callback");
105         thread.start();
106         mNetwork = new Handler(thread.getLooper());
107
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);
117             }
118         } else {
119             // Request auth callback once user has chosen an account.
120             Log.i("auth", "Requesting auth token from system");
121             AccountManager.get(this).getAuthTokenByFeatures(
122                     ACCOUNT_TYPE,
123                     TOKEN_SCOPE,
124                     null,
125                     this,
126                     null,
127                     null,
128                     new HostListDirectoryGrabber(this),
129                     mNetwork
130                 );
131         }
132     }
133
134     /** Called when the activity is finally finished. */
135     @Override
136     public void onDestroy() {
137         super.onDestroy();
138         JniInterface.disconnectFromHost();
139     }
140
141     /** Called to initialize the action bar. */
142     @Override
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);
147
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);
153         }
154
155         if (mAccount == null) {
156             // If no account has been chosen, don't allow the user to refresh the listing.
157             mRefreshButton.setEnabled(false);
158         } else {
159             // If the user has picked an account, show its name directly on the account switcher.
160             mAccountSwitcher.setTitle(mAccount.name);
161         }
162
163         return super.onCreateOptionsMenu(menu);
164     }
165
166     /** Called whenever an action bar button is pressed. */
167     @Override
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(
172                     ACCOUNT_TYPE,
173                     TOKEN_SCOPE,
174                     null,
175                     this,
176                     null,
177                     null,
178                     new HostListDirectoryGrabber(this),
179                     mNetwork
180                 );
181         }
182         else {
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);
186         }
187
188         return true;
189     }
190
191     /**
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.
195      */
196     private class HostListDirectoryGrabber implements AccountManagerCallback<Bundle> {
197         /** Whether authentication has already been attempted. */
198         private boolean mAlreadyTried;
199
200         /** Communication with the screen. */
201         private Activity mUi;
202
203         /** Constructor. */
204         public HostListDirectoryGrabber(Activity ui) {
205             mAlreadyTried = false;
206             mUi = ui;
207         }
208
209         /**
210          * Retrieves the host list from the directory server. This method performs
211          * network operations and must be run an a non-UI thread.
212          */
213         @Override
214         public void run(AccountManagerFuture<Bundle> future) {
215             Log.i("auth", "User finished with auth dialogs");
216             try {
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");
223
224                 synchronized (mUi) {
225                     mAccount = new Account(accountName, accountType);
226                     mToken = authToken;
227                     getPreferences(MODE_PRIVATE).edit().putString("account_name", accountName).
228                             putString("account_type", accountType).apply();
229                 }
230
231                 // Send our HTTP request to the directory server.
232                 URLConnection link =
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);
237
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());
244                 }
245                 incoming.close();
246
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).
253                 throw ex;
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.
264
265                         synchronized (mUi) {
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;
271
272                                 Log.w("auth", "Requesting renewal of rejected auth token");
273                                 authenticator.invalidateAuthToken(mAccount.type, mToken);
274                                 mToken = null;
275                                 authenticator.getAuthToken(
276                                         mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork);
277
278                                 // We're not in an error state *yet*.
279                                 return;
280                             }
281                         }
282
283                         // We didn't even get an account, so the auth server is likely unreachable.
284                         explanation = getString(R.string.error_bad_connection);
285                     } else {
286                         // Authentication truly failed.
287                         Log.e("auth", "Fresh auth token was also rejected");
288                         explanation = getString(R.string.error_auth_failed);
289                     }
290                 } else if (ex instanceof JSONException) {
291                     explanation = getString(R.string.error_unexpected_response);
292                     runOnUiThread(new HostListDisplayer(mUi));
293                 }
294
295                 mHosts = null;
296                 Log.w("auth", ex);
297                 Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show();
298             }
299
300             // Share our findings with the user.
301             runOnUiThread(new HostListDisplayer(mUi));
302         }
303     }
304
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;
309
310         /** Constructor. */
311         public HostListDisplayer(Activity ui) {
312             mUi = ui;
313         }
314
315         /**
316          * Updates the infotext and host list display.
317          * This method affects the UI and must be run on its same thread.
318          */
319         @Override
320         public void run() {
321             synchronized (mUi) {
322                 mRefreshButton.setEnabled(mAccount != null);
323                 if (mAccount != null) {
324                     mAccountSwitcher.setTitle(mAccount.name);
325                 }
326             }
327
328             if (mHosts == null) {
329                 mGreeting.setText(getString(R.string.inst_empty_list));
330                 mList.setAdapter(null);
331                 return;
332             }
333
334             mGreeting.setText(getString(R.string.inst_host_list));
335
336             ArrayAdapter<JSONObject> displayer = new HostListAdapter(mUi, R.layout.host);
337             Log.i("hostlist", "About to populate host list display");
338             try {
339                 int index = 0;
340                 while (!mHosts.isNull(index)) {
341                     displayer.add(mHosts.getJSONObject(index));
342                     ++index;
343                 }
344                 mList.setAdapter(displayer);
345             }
346             catch(JSONException ex) {
347                 Log.w("hostlist", ex);
348                 Toast.makeText(
349                         mUi, getString(R.string.error_cataloging_hosts), Toast.LENGTH_LONG).show();
350
351                 // Close the application.
352                 finish();
353             }
354         }
355     }
356
357     /** Describes the appearance and behavior of each host list entry. */
358     private class HostListAdapter extends ArrayAdapter<JSONObject> {
359         /** Constructor. */
360         public HostListAdapter(Context context, int textViewResourceId) {
361             super(context, textViewResourceId);
362         }
363
364         /** Generates a View corresponding to this particular host. */
365         @Override
366         public View getView(int position, View convertView, ViewGroup parent) {
367             TextView target = (TextView)super.getView(position, convertView, parent);
368
369             try {
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>)"));
374
375                 if (host.getString("status").equals("ONLINE")) {  // Host is online.
376                     target.setOnClickListener(new View.OnClickListener() {
377                             @Override
378                             public void onClick(View v) {
379                                 try {
380                                     synchronized (getContext()) {
381                                         JniInterface.connectToHost(mAccount.name, mToken,
382                                                 host.getString("jabberId"),
383                                                 host.getString("hostId"),
384                                                 host.getString("publicKey"),
385                                                 new Runnable() {
386                                             @Override
387                                             public void run() {
388                                                 startActivity(
389                                                         new Intent(getContext(), Desktop.class));
390                                             }
391                                         });
392                                     }
393                                 }
394                                 catch(JSONException ex) {
395                                     Log.w("host", ex);
396                                     Toast.makeText(getContext(),
397                                             getString(R.string.error_reading_host),
398                                             Toast.LENGTH_LONG).show();
399
400                                     // Close the application.
401                                     finish();
402                                 }
403                             }
404                         });
405                 } else {  // Host is offline.
406                     // Disallow interaction with this entry.
407                     target.setEnabled(false);
408                 }
409             }
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();
415
416                 // Close the application.
417                 finish();
418             }
419
420             return target;
421         }
422     }
423 }