15d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// Copyright 2014 The Chromium Authors. All rights reserved. 25d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be 35d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)// found in the LICENSE file. 45d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 55d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)package org.chromium.chromoting; 65d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 75d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import android.os.Handler; 85d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import android.os.HandlerThread; 95d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import android.os.Looper; 105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import android.util.Log; 115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import org.json.JSONArray; 135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import org.json.JSONException; 145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import org.json.JSONObject; 155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.io.IOException; 175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.net.HttpURLConnection; 185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.net.MalformedURLException; 195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.net.URL; 205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.util.ArrayList; 215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.util.Collections; 225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.util.Comparator; 23a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles)import java.util.Locale; 245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)import java.util.Scanner; 255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)/** Helper for fetching the host list. */ 275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)public class HostListLoader { 285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) public enum Error { 295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) AUTH_FAILED, 305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) NETWORK_ERROR, 315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) SERVICE_UNAVAILABLE, 325d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) UNEXPECTED_RESPONSE, 335d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) UNKNOWN, 345d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 355d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 365d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) /** Callback for receiving the host list, or getting notified of an error. */ 375d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) public interface Callback { 385d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) void onHostListReceived(HostInfo[] hosts); 395d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) void onError(Error error); 405d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 415d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 425d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) /** Path from which to download a user's host list JSON object. */ 435d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) private static final String HOST_LIST_PATH = 441675a649fd7a8b3cb80ffddae2dc181f122353c5Ben Murdoch "https://www.googleapis.com/chromoting/v1/@me/hosts"; 455d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 465d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) /** Callback handler to be used for network operations. */ 475d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) private Handler mNetworkThread; 485d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 495d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) /** Handler for main thread. */ 505d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) private Handler mMainThread; 515d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 525d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) public HostListLoader() { 535d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) // Thread responsible for downloading the host list. 545d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 555d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) mMainThread = new Handler(Looper.getMainLooper()); 565d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 575d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 585d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) private void initNetworkThread() { 595d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (mNetworkThread == null) { 605d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) HandlerThread thread = new HandlerThread("network"); 615d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) thread.start(); 625d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) mNetworkThread = new Handler(thread.getLooper()); 635d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 645d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 655d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 665d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) /** 675d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) * Causes the host list to be fetched on a background thread. This should be called on the 685d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) * main thread, and callbacks will also be invoked on the main thread. On success, 695d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) * callback.onHostListReceived() will be called, otherwise callback.onError() will be called 705d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) * with an error-code describing the failure. 715d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) */ 725d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) public void retrieveHostList(String authToken, Callback callback) { 735d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) initNetworkThread(); 745d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) final String authTokenFinal = authToken; 755d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) final Callback callbackFinal = callback; 765d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) mNetworkThread.post(new Runnable() { 775d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) @Override 785d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) public void run() { 795d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) doRetrieveHostList(authTokenFinal, callbackFinal); 805d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 815d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) }); 825d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 835d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 845d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) private void doRetrieveHostList(String authToken, Callback callback) { 855d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) HttpURLConnection link = null; 865d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) String response = null; 875d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) try { 881675a649fd7a8b3cb80ffddae2dc181f122353c5Ben Murdoch link = (HttpURLConnection) new URL(HOST_LIST_PATH).openConnection(); 895d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) link.setRequestProperty("Authorization", "OAuth " + authToken); 905d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 915d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) // Listen for the server to respond. 925d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) int status = link.getResponseCode(); 935d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) switch (status) { 945d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) case HttpURLConnection.HTTP_OK: // 200 955d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) break; 965d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) case HttpURLConnection.HTTP_UNAUTHORIZED: // 401 975d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) postError(callback, Error.AUTH_FAILED); 985d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 995d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) case HttpURLConnection.HTTP_BAD_GATEWAY: // 502 1005d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) case HttpURLConnection.HTTP_UNAVAILABLE: // 503 1015d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) postError(callback, Error.SERVICE_UNAVAILABLE); 1025d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 1035d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) default: 1045d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) postError(callback, Error.UNKNOWN); 1055d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 1065d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1075d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1085d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) StringBuilder responseBuilder = new StringBuilder(); 1095d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) Scanner incoming = new Scanner(link.getInputStream()); 1105d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) Log.i("auth", "Successfully authenticated to directory server"); 1115d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) while (incoming.hasNext()) { 1125d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) responseBuilder.append(incoming.nextLine()); 1135d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1145d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) response = String.valueOf(responseBuilder); 1155d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) incoming.close(); 1165d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } catch (MalformedURLException ex) { 1175d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) // This should never happen. 1185d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) throw new RuntimeException("Unexpected error while fetching host list: " + ex); 1195d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } catch (IOException ex) { 1205d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) postError(callback, Error.NETWORK_ERROR); 1215d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 1225d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } finally { 1235d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (link != null) { 1245d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) link.disconnect(); 1255d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1265d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1275d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1285d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) // Parse directory response. 1295d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) ArrayList<HostInfo> hostList = new ArrayList<HostInfo>(); 1305d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) try { 1315d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) JSONObject data = new JSONObject(response).getJSONObject("data"); 132a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) if (data.has("items")) { 133a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) JSONArray hostsJson = data.getJSONArray("items"); 134a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) Log.i("hostlist", "Received host listing from directory server"); 135a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) 136a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) int index = 0; 137a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) while (!hostsJson.isNull(index)) { 138a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) JSONObject hostJson = hostsJson.getJSONObject(index); 139a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // If a host is only recently registered, it may be missing some of the keys 140a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // below. It should still be visible in the list, even though a connection 141a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // attempt will fail because of the missing keys. The failed attempt will 142a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // trigger reloading of the host-list, by which time the keys will hopefully be 143a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) // present, and the retried connection can succeed. 1446d86b77056ed63eb6871182f42a9fd5f07550f90Torne (Richard Coles) HostInfo host = HostInfo.create(hostJson); 145a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) hostList.add(host); 146a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) ++index; 147a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) } 1485d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1495d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } catch (JSONException ex) { 150a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) Log.e("hostlist", "Error parsing host list response: ", ex); 1515d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) postError(callback, Error.UNEXPECTED_RESPONSE); 1525d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return; 1535d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1545d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1555d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) sortHosts(hostList); 1565d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1575d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) final Callback callbackFinal = callback; 1585d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) final HostInfo[] hosts = hostList.toArray(new HostInfo[hostList.size()]); 1595d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) mMainThread.post(new Runnable() { 1605d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) @Override 1615d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) public void run() { 1625d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) callbackFinal.onHostListReceived(hosts); 1635d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1645d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) }); 1655d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1665d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1675d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) /** Posts error to callback on main thread. */ 1685d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) private void postError(Callback callback, Error error) { 1695d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) final Callback callbackFinal = callback; 1705d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) final Error errorFinal = error; 1715d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) mMainThread.post(new Runnable() { 1725d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) @Override 1735d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) public void run() { 1745d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) callbackFinal.onError(errorFinal); 1755d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1765d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) }); 1775d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1785d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) 1795d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) private static void sortHosts(ArrayList<HostInfo> hosts) { 1805d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) Comparator<HostInfo> hostComparator = new Comparator<HostInfo>() { 181a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) @Override 1825d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) public int compare(HostInfo a, HostInfo b) { 1835d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) if (a.isOnline != b.isOnline) { 1845d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return a.isOnline ? -1 : 1; 1855d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 186a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) String aName = a.name.toUpperCase(Locale.getDefault()); 187a1401311d1ab56c4ed0a474bd38c108f75cb0cd9Torne (Richard Coles) String bName = b.name.toUpperCase(Locale.getDefault()); 1885d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) return aName.compareTo(bName); 1895d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1905d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) }; 1915d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) Collections.sort(hosts, hostComparator); 1925d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles) } 1935d1f7b1de12d16ceb2c938c56701a3e8bfa558f7Torne (Richard Coles)} 194