OLD | NEW |
---|---|
(Empty) | |
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.os.Bundle; | |
17 import android.os.Handler; | |
18 import android.os.HandlerThread; | |
19 import android.text.Html; | |
20 import android.util.Log; | |
21 import android.view.View; | |
22 import android.view.ViewGroup; | |
23 import android.widget.ArrayAdapter; | |
24 import android.widget.TextView; | |
25 import android.widget.ListView; | |
26 import android.widget.Toast; | |
27 | |
28 import org.chromium.chromoting.jni.JniInterface; | |
29 import org.json.JSONArray; | |
30 import org.json.JSONException; | |
31 import org.json.JSONObject; | |
32 | |
33 import java.io.IOException; | |
34 import java.net.URL; | |
35 import java.net.URLConnection; | |
36 import java.util.Scanner; | |
37 | |
38 /** | |
39 * The user interface for querying and displaying a user's host list from the di rectory server. It | |
40 * also requests and renews authentication tokens using the system account manag er. | |
41 */ | |
42 public class Chromoting extends Activity { | |
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 /** Path from which to download a user's host list JSON object. */ | |
51 private static final String HOST_LIST_PATH = | |
52 "https://www.googleapis.com/chromoting/v1/@me/hosts?key="; | |
53 | |
54 /** Color to use for hosts that are online. */ | |
55 private static final String HOST_COLOR_ONLINE = "green"; | |
56 | |
57 /** Color to use for hosts that are offline. */ | |
58 private static final String HOST_COLOR_OFFLINE = "red"; | |
59 | |
60 /** User's account details. */ | |
61 Account mAccount; | |
62 | |
63 /** Account auth token. */ | |
64 String mToken; | |
65 | |
66 /** List of hosts. */ | |
67 JSONArray mHosts; | |
68 | |
69 /** Greeting at the top of the displayed list. */ | |
70 TextView mGreeting; | |
71 | |
72 /** Host list as it appears to the user. */ | |
73 ListView mList; | |
74 | |
75 /** Callback handler to be used for network operations. */ | |
76 Handler mNetwork; | |
77 | |
78 /** | |
79 * Called when the activity is first created. Loads the native library and r equests an | |
80 * authentication token from the system. | |
81 */ | |
82 @Override | |
83 public void onCreate(Bundle savedInstanceState) { | |
84 super.onCreate(savedInstanceState); | |
85 setContentView(R.layout.main); | |
86 | |
87 // Get ahold of our view widgets. | |
88 mGreeting = (TextView)findViewById(R.id.hostList_greeting); | |
89 mList = (ListView)findViewById(R.id.hostList_chooser); | |
90 | |
91 // Bring native components online. | |
92 JniInterface.loadLibrary(this); | |
93 | |
94 // Thread responsible for downloading/displaying host list. | |
95 HandlerThread thread = new HandlerThread("auth_callback"); | |
96 thread.start(); | |
97 mNetwork = new Handler(thread.getLooper()); | |
98 | |
99 // Request callback once user has chosen an account. | |
100 Log.i("auth", "Requesting auth token from system"); | |
101 AccountManager.get(this).getAuthTokenByFeatures( | |
102 ACCOUNT_TYPE, | |
103 TOKEN_SCOPE, | |
104 null, | |
105 this, | |
106 null, | |
107 null, | |
108 new HostListDirectoryGrabber(this), | |
109 mNetwork | |
110 ); | |
111 } | |
112 | |
113 /** Called when the activity is finally finished. */ | |
114 @Override | |
115 public void onDestroy() { | |
116 super.onDestroy(); | |
117 | |
118 JniInterface.disconnectFromHost(); | |
119 } | |
120 | |
121 /** | |
122 * Processes the authentication token once the system provides it. Once in p ossession of such a | |
123 * token, attempts to request a host list from the directory server. In case of a bad response, | |
124 * this is retried once in case the system's cached auth token had expired. | |
125 */ | |
126 private class HostListDirectoryGrabber implements AccountManagerCallback<Bun dle> { | |
127 /** Whether authentication has already been attempted. */ | |
128 private boolean mAlreadyTried; | |
129 | |
130 /** Communication with the screen. */ | |
131 private Activity mUi; | |
132 | |
133 /** Constructor. */ | |
134 public HostListDirectoryGrabber(Activity ui) { | |
135 mAlreadyTried = false; | |
136 mUi = ui; | |
137 } | |
138 | |
139 /** | |
140 * Retrieves the host list from the directory server. This method perfor ms | |
141 * network operations and must be run an a non-UI thread. | |
garykac
2013/07/17 23:52:28
Is there a way to assert this in the code?
solb
2013/07/18 01:23:36
Everything I've seen indicates that actual asserti
| |
142 */ | |
143 @Override | |
144 public void run(AccountManagerFuture<Bundle> future) { | |
145 Log.i("auth", "User finished with auth dialogs"); | |
146 mAlreadyTried = true; | |
147 try { | |
148 // Here comes our auth token from the Android system. | |
149 Bundle result = future.getResult(); | |
150 Log.i("auth", "Received an auth token from system"); | |
151 mAccount = new Account(result.getString(AccountManager.KEY_ACCOU NT_NAME), | |
152 result.getString(AccountManager.KEY_ACCOUNT_TYPE)); | |
153 mToken = result.getString(AccountManager.KEY_AUTHTOKEN); | |
154 | |
155 // Send our HTTP request to the directory server. | |
156 URLConnection link = | |
157 new URL(HOST_LIST_PATH+JniInterface.getApiKey()).openCon nection(); | |
garykac
2013/07/17 23:52:28
spaces around +
solb
2013/07/18 01:23:36
Done.
| |
158 link.addRequestProperty("client_id", JniInterface.getClientId()) ; | |
159 link.addRequestProperty("client_secret", JniInterface.getClientS ecret()); | |
160 link.setRequestProperty("Authorization", "OAuth "+mToken); | |
161 | |
162 // Listen for the server to respond. | |
163 String response = ""; | |
164 Scanner incoming = new Scanner(link.getInputStream()); | |
165 Log.i("auth", "Successfully authenticated to directory server"); | |
166 while (incoming.hasNext()) { | |
167 response += incoming.nextLine(); | |
168 } | |
169 incoming.close(); | |
170 | |
171 // Interpret what the directory server told us. | |
172 JSONObject data = new JSONObject(response).getJSONObject("data") ; | |
173 mHosts = data.getJSONArray("items"); | |
174 Log.i("hostlist", "Received host listing from directory server") ; | |
175 | |
176 // Share our findings with the user. | |
177 runOnUiThread(new HostListDisplayer(mUi)); | |
178 } | |
179 catch(Exception ex) { | |
180 // Assemble error message to display to the user. | |
181 String explanation = getString(R.string.error_unknown); | |
182 if (ex instanceof OperationCanceledException) { | |
183 explanation = getString(R.string.error_auth_canceled); | |
184 } else if (ex instanceof AuthenticatorException) { | |
185 explanation = getString(R.string.error_no_accounts); | |
186 } else if (ex instanceof IOException) { | |
187 if (!mAlreadyTried) { // This was our first connection atte mpt. | |
188 Log.w("auth", "Unable to authenticate with (expired?) to ken"); | |
189 | |
190 // Ask system to renew the auth token in case it expired . | |
191 AccountManager authenticator = AccountManager.get(mUi); | |
192 authenticator.invalidateAuthToken(mAccount.type, mToken) ; | |
193 Log.i("auth", "Requesting auth token renewal"); | |
194 authenticator.getAuthToken( | |
195 mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork ); | |
196 | |
197 // We're not in an error state *yet.* | |
198 return; | |
199 } else { // Authentication truly failed. | |
200 Log.e("auth", "Fresh auth token was also rejected"); | |
201 explanation = getString(R.string.error_auth_failed); | |
202 } | |
203 } else if (ex instanceof JSONException) { | |
204 explanation = getString(R.string.error_unexpected_response); | |
205 } | |
206 | |
207 Log.w("auth", ex); | |
208 Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show(); | |
209 | |
210 // Bail out. | |
garykac
2013/07/17 23:52:28
I'm not sure this comment provides more info than
solb
2013/07/18 01:23:36
Good point: I'm rewording this to, "Close the appl
| |
211 finish(); | |
212 } | |
213 } | |
214 } | |
215 | |
216 /** Formats the host list and offers it to the user. */ | |
217 private class HostListDisplayer implements Runnable { | |
218 /** Communication with the screen. */ | |
219 private Activity mUi; | |
220 | |
221 /** Constructor. */ | |
222 public HostListDisplayer(Activity ui) { | |
223 mUi = ui; | |
224 } | |
225 | |
226 /** | |
227 * Updates the infotext and host list display. | |
228 * This method affects the UI and must be run on its same thread. | |
229 */ | |
230 @Override | |
231 public void run() { | |
232 mGreeting.setText(mAccount.name+getString(R.string.inst_host_list)); | |
garykac
2013/07/17 23:52:28
spaces around +
solb
2013/07/18 01:23:36
Done.
| |
233 | |
234 ArrayAdapter<JSONObject> displayer = new HostListAdapter(mUi, R.layo ut.host); | |
235 Log.i("hostlist", "About to populate host list display"); | |
236 try { | |
237 int index = 0; | |
238 while (!mHosts.isNull(index)) { | |
239 displayer.add(mHosts.getJSONObject(index)); | |
240 ++index; | |
241 } | |
242 mList.setAdapter(displayer); | |
243 } | |
244 catch(JSONException ex) { | |
245 Log.w("hostlist", ex); | |
246 Toast.makeText( | |
247 mUi, getString(R.string.error_cataloging_hosts), Toast.L ENGTH_LONG).show(); | |
248 | |
249 // Bail out. | |
250 finish(); | |
251 } | |
252 } | |
253 } | |
254 | |
255 /** Describes the appearance and behavior of each host list entry. */ | |
256 private class HostListAdapter extends ArrayAdapter<JSONObject> { | |
257 /** Constructor. */ | |
258 public HostListAdapter(Context context, int textViewResourceId) { | |
259 super(context, textViewResourceId); | |
260 } | |
261 | |
262 /** Generates a View corresponding to this particular host. */ | |
263 @Override | |
264 public View getView(int position, View convertView, ViewGroup parent) { | |
265 TextView target = (TextView)super.getView(position, convertView, par ent); | |
266 | |
267 try { | |
268 final JSONObject host = getItem(position); | |
269 target.setText(Html.fromHtml(host.getString("hostName")+" (<font color = \""+ | |
270 (host.getString("status").equals("ONLINE") ? HOST_COLOR_ ONLINE : | |
garykac
2013/07/17 23:52:28
This looks like it would only work for English.
solb
2013/07/18 01:23:36
Okay if I fix this in a separate internationalizat
| |
271 HOST_COLOR_OFFLINE)+"\">"+host.getString("status")+"</fo nt>)")); | |
272 | |
273 if (host.getString("status").equals("ONLINE")) { // Host is onl ine. | |
274 target.setOnClickListener(new View.OnClickListener() { | |
275 @Override | |
276 public void onClick(View v) { | |
277 try { | |
278 JniInterface.connectToHost( | |
279 mAccount.name, mToken, host.getStrin g("jabberId"), | |
280 host.getString("hostId"), host.getSt ring("publicKey"), | |
281 new Runnable() { | |
282 @Override | |
283 public void run() { | |
284 // TODO(solb) Start an Activity to d isplay the desktop. | |
garykac
2013/07/17 23:52:28
I'm not sure it's worthwhile to add it, but is the
solb
2013/07/18 01:23:36
I'm afraid I don't know of one. This is a placehol
| |
285 } | |
286 }); | |
287 } | |
288 catch(JSONException ex) { | |
289 Log.w("host", ex); | |
290 Toast.makeText(getContext(), | |
291 getString(R.string.error_reading_hos t), | |
292 Toast.LENGTH_LONG).show(); | |
293 | |
294 // Bail out. | |
295 finish(); | |
296 } | |
297 } | |
298 }); | |
299 } else { // Host is offline. | |
300 // Disallow interaction with this entry. | |
301 target.setEnabled(false); | |
302 } | |
303 } | |
304 catch(JSONException ex) { | |
305 Log.w("hostlist", ex); | |
306 Toast.makeText(getContext(), | |
307 getString(R.string.error_displaying_host), | |
308 Toast.LENGTH_LONG).show(); | |
309 | |
310 // Bail out. | |
311 finish(); | |
312 } | |
313 | |
314 return target; | |
315 } | |
316 } | |
317 } | |
OLD | NEW |