1eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch// Copyright 2014 The Chromium Authors. All rights reserved.
2eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch// Use of this source code is governed by a BSD-style license that can be
3eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch// found in the LICENSE file.
4eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
5eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochpackage org.chromium.chromoting;
6eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
7eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport android.os.Handler;
8eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport android.os.HandlerThread;
9eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport android.os.Looper;
10eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport android.util.Log;
11eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
12eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport org.chromium.chromoting.jni.JniInterface;
13eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport org.json.JSONArray;
14eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport org.json.JSONException;
15eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport org.json.JSONObject;
16eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
17eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.io.IOException;
18eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.net.HttpURLConnection;
19eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.net.MalformedURLException;
20eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.net.URL;
21eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.ArrayList;
22eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.Collections;
23eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.Comparator;
24eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.Locale;
25eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochimport java.util.Scanner;
26eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
27eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch/** Helper for fetching the host list. */
28eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdochpublic class HostListLoader {
29eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    public enum Error {
30eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        AUTH_FAILED,
31eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        NETWORK_ERROR,
32eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        SERVICE_UNAVAILABLE,
33eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        UNEXPECTED_RESPONSE,
34eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        UNKNOWN,
35eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    }
36eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
37eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    /** Callback for receiving the host list, or getting notified of an error. */
38eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    public interface Callback {
39eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        void onHostListReceived(HostInfo[] hosts);
40eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        void onError(Error error);
41eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    }
42eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
43eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    /** Path from which to download a user's host list JSON object. */
44eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    private static final String HOST_LIST_PATH =
45eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            "https://www.googleapis.com/chromoting/v1/@me/hosts?key=";
46eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
47eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    /** Callback handler to be used for network operations. */
48eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    private Handler mNetworkThread;
49eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
50eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    /** Handler for main thread. */
51eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    private Handler mMainThread;
52eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
53eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    public HostListLoader() {
54eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        // Thread responsible for downloading the host list.
55eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
56eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        mMainThread = new Handler(Looper.getMainLooper());
57eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    }
58eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
59eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    private void initNetworkThread() {
60eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        if (mNetworkThread == null) {
61eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            HandlerThread thread = new HandlerThread("network");
62eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            thread.start();
63eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            mNetworkThread = new Handler(thread.getLooper());
64eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        }
65eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    }
66eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
67eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    /**
68eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch      * Causes the host list to be fetched on a background thread. This should be called on the
69eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch      * main thread, and callbacks will also be invoked on the main thread. On success,
70eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch      * callback.onHostListReceived() will be called, otherwise callback.onError() will be called
71eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch      * with an error-code describing the failure.
72eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch      */
73eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    public void retrieveHostList(String authToken, Callback callback) {
74eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        initNetworkThread();
75eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        final String authTokenFinal = authToken;
76eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        final Callback callbackFinal = callback;
77eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        mNetworkThread.post(new Runnable() {
78eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            @Override
79eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            public void run() {
80eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                doRetrieveHostList(authTokenFinal, callbackFinal);
81eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            }
82eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        });
83eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    }
84eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
85eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    private void doRetrieveHostList(String authToken, Callback callback) {
86eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        HttpURLConnection link = null;
87eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        String response = null;
88eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        try {
89eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            link = (HttpURLConnection)
90eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    new URL(HOST_LIST_PATH + JniInterface.nativeGetApiKey()).openConnection();
91eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            link.addRequestProperty("client_id", JniInterface.nativeGetClientId());
92eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            link.addRequestProperty("client_secret", JniInterface.nativeGetClientSecret());
93eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            link.setRequestProperty("Authorization", "OAuth " + authToken);
94eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
95eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            // Listen for the server to respond.
96eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            int status = link.getResponseCode();
97eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            switch (status) {
98eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                case HttpURLConnection.HTTP_OK:  // 200
99eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    break;
100eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                case HttpURLConnection.HTTP_UNAUTHORIZED:  // 401
101eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    postError(callback, Error.AUTH_FAILED);
102eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    return;
103eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                case HttpURLConnection.HTTP_BAD_GATEWAY:  // 502
104eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                case HttpURLConnection.HTTP_UNAVAILABLE:  // 503
105eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    postError(callback, Error.SERVICE_UNAVAILABLE);
106eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    return;
107eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                default:
108eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    postError(callback, Error.UNKNOWN);
109eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    return;
110eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            }
111eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
112eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            StringBuilder responseBuilder = new StringBuilder();
113eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            Scanner incoming = new Scanner(link.getInputStream());
114eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            Log.i("auth", "Successfully authenticated to directory server");
115eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            while (incoming.hasNext()) {
116eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                responseBuilder.append(incoming.nextLine());
117eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            }
118eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            response = String.valueOf(responseBuilder);
119eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            incoming.close();
120eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        } catch (MalformedURLException ex) {
121eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            // This should never happen.
122eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            throw new RuntimeException("Unexpected error while fetching host list: " + ex);
123eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        } catch (IOException ex) {
124eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            postError(callback, Error.NETWORK_ERROR);
125eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            return;
126eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        } finally {
127eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            if (link != null) {
128eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                link.disconnect();
129eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            }
130eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        }
131eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
132eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        // Parse directory response.
133eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        ArrayList<HostInfo> hostList = new ArrayList<HostInfo>();
134eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        try {
135eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            JSONObject data = new JSONObject(response).getJSONObject("data");
136eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            if (data.has("items")) {
137eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                JSONArray hostsJson = data.getJSONArray("items");
138eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                Log.i("hostlist", "Received host listing from directory server");
139eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
140eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                int index = 0;
141eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                while (!hostsJson.isNull(index)) {
142eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    JSONObject hostJson = hostsJson.getJSONObject(index);
143eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    // If a host is only recently registered, it may be missing some of the keys
144eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    // below. It should still be visible in the list, even though a connection
145eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    // attempt will fail because of the missing keys. The failed attempt will
146eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    // trigger reloading of the host-list, by which time the keys will hopefully be
147eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    // present, and the retried connection can succeed.
148eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    HostInfo host = HostInfo.create(hostJson);
149eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    hostList.add(host);
150eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                    ++index;
151eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch                }
152eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            }
153eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        } catch (JSONException ex) {
154eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            Log.e("hostlist", "Error parsing host list response: ", ex);
155eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            postError(callback, Error.UNEXPECTED_RESPONSE);
156eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch            return;
157eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        }
158eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
159eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch        sortHosts(hostList);
160eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch
161        final Callback callbackFinal = callback;
162        final HostInfo[] hosts = hostList.toArray(new HostInfo[hostList.size()]);
163        mMainThread.post(new Runnable() {
164            @Override
165            public void run() {
166                callbackFinal.onHostListReceived(hosts);
167            }
168        });
169    }
170
171    /** Posts error to callback on main thread. */
172    private void postError(Callback callback, Error error) {
173        final Callback callbackFinal = callback;
174        final Error errorFinal = error;
175        mMainThread.post(new Runnable() {
176            @Override
177            public void run() {
178                callbackFinal.onError(errorFinal);
179            }
180        });
181    }
182
183    private static void sortHosts(ArrayList<HostInfo> hosts) {
184        Comparator<HostInfo> hostComparator = new Comparator<HostInfo>() {
185            @Override
186            public int compare(HostInfo a, HostInfo b) {
187                if (a.isOnline != b.isOnline) {
188                    return a.isOnline ? -1 : 1;
189                }
190                String aName = a.name.toUpperCase(Locale.getDefault());
191                String bName = b.name.toUpperCase(Locale.getDefault());
192                return aName.compareTo(bName);
193            }
194        };
195        Collections.sort(hosts, hostComparator);
196    }
197}
198