diff options
author | solb@chromium.org <solb@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-07-19 01:49:09 +0000 |
---|---|---|
committer | solb@chromium.org <solb@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-07-19 01:49:09 +0000 |
commit | 8812dce4ee4efb8c652e6150bb7085c8e40c63da (patch) | |
tree | 985556b5f2a374e532fbfb3004ae52765fae1fb9 /remoting | |
parent | 9992898cbe9872e5b5e0b9cead5b02ea9e830756 (diff) | |
download | chromium_src-8812dce4ee4efb8c652e6150bb7085c8e40c63da.zip chromium_src-8812dce4ee4efb8c652e6150bb7085c8e40c63da.tar.gz chromium_src-8812dce4ee4efb8c652e6150bb7085c8e40c63da.tar.bz2 |
Add the beginnings of a Chromoting Android app
Currently, this has only the following capabilities:
+ Authenticate using a Google account on the phone
+ Query and display the host list from the Chromoting directory server
+ Connect to and communicate with the host service over XMPP/ICE
+ Establish peer-to-peer channels for communicating with the host service
Notable missing features are:
- Display the host's desktop
- Handle any kind of input
Review URL: https://chromiumcodereview.appspot.com/19506004
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@212496 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/android/OWNERS | 3 | ||||
-rw-r--r-- | remoting/android/java/AndroidManifest.xml | 22 | ||||
-rw-r--r-- | remoting/android/java/src/org/chromium/chromoting/Chromoting.java | 317 | ||||
-rw-r--r-- | remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java | 182 | ||||
-rw-r--r-- | remoting/protocol/connection_to_host.h | 6 | ||||
-rw-r--r-- | remoting/protocol/errors.h | 3 | ||||
-rw-r--r-- | remoting/remoting.gyp | 43 | ||||
-rw-r--r-- | remoting/resources/layout/host.xml | 9 | ||||
-rw-r--r-- | remoting/resources/layout/main.xml | 12 | ||||
-rw-r--r-- | remoting/resources/strings.xml | 54 |
10 files changed, 651 insertions, 0 deletions
diff --git a/remoting/android/OWNERS b/remoting/android/OWNERS new file mode 100644 index 0000000..f94f63b --- /dev/null +++ b/remoting/android/OWNERS @@ -0,0 +1,3 @@ +garykac@chromium.org +solb@chromium.org +wez@chromium.org diff --git a/remoting/android/java/AndroidManifest.xml b/remoting/android/java/AndroidManifest.xml new file mode 100644 index 0000000..4aeff08 --- /dev/null +++ b/remoting/android/java/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.chromium.chromoting" + android:versionCode="1" + android:versionName="1.0"> + <uses-sdk android:minSdkVersion="14" + android:targetSdkVersion="14"/> + <uses-permission android:name="android.permission.GET_ACCOUNTS"/> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/> + <uses-permission android:name="android.permission.USE_CREDENTIALS"/> + <application android:label="@string/app_name" + android:icon="@drawable/chromoting128"> + <activity android:name="Chromoting" + android:configChanges="orientation|screenSize"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/remoting/android/java/src/org/chromium/chromoting/Chromoting.java b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java new file mode 100644 index 0000000..23e4677 --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/Chromoting.java @@ -0,0 +1,317 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chromoting; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.text.Html; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import android.widget.ListView; +import android.widget.Toast; + +import org.chromium.chromoting.jni.JniInterface; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.util.Scanner; + +/** + * The user interface for querying and displaying a user's host list from the directory server. It + * also requests and renews authentication tokens using the system account manager. + */ +public class Chromoting extends Activity { + /** Only accounts of this type will be selectable for authentication. */ + private static final String ACCOUNT_TYPE = "com.google"; + + /** Scopes at which the authentication token we request will be valid. */ + private static final String TOKEN_SCOPE = "oauth2:https://www.googleapis.com/auth/chromoting " + + "https://www.googleapis.com/auth/googletalk"; + + /** Path from which to download a user's host list JSON object. */ + private static final String HOST_LIST_PATH = + "https://www.googleapis.com/chromoting/v1/@me/hosts?key="; + + /** Color to use for hosts that are online. */ + private static final String HOST_COLOR_ONLINE = "green"; + + /** Color to use for hosts that are offline. */ + private static final String HOST_COLOR_OFFLINE = "red"; + + /** User's account details. */ + Account mAccount; + + /** Account auth token. */ + String mToken; + + /** List of hosts. */ + JSONArray mHosts; + + /** Greeting at the top of the displayed list. */ + TextView mGreeting; + + /** Host list as it appears to the user. */ + ListView mList; + + /** Callback handler to be used for network operations. */ + Handler mNetwork; + + /** + * Called when the activity is first created. Loads the native library and requests an + * authentication token from the system. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + // Get ahold of our view widgets. + mGreeting = (TextView)findViewById(R.id.hostList_greeting); + mList = (ListView)findViewById(R.id.hostList_chooser); + + // Bring native components online. + JniInterface.loadLibrary(this); + + // Thread responsible for downloading/displaying host list. + HandlerThread thread = new HandlerThread("auth_callback"); + thread.start(); + mNetwork = new Handler(thread.getLooper()); + + // Request callback once user has chosen an account. + Log.i("auth", "Requesting auth token from system"); + AccountManager.get(this).getAuthTokenByFeatures( + ACCOUNT_TYPE, + TOKEN_SCOPE, + null, + this, + null, + null, + new HostListDirectoryGrabber(this), + mNetwork + ); + } + + /** Called when the activity is finally finished. */ + @Override + public void onDestroy() { + super.onDestroy(); + + JniInterface.disconnectFromHost(); + } + + /** + * Processes the authentication token once the system provides it. Once in possession of such a + * token, attempts to request a host list from the directory server. In case of a bad response, + * this is retried once in case the system's cached auth token had expired. + */ + private class HostListDirectoryGrabber implements AccountManagerCallback<Bundle> { + /** Whether authentication has already been attempted. */ + private boolean mAlreadyTried; + + /** Communication with the screen. */ + private Activity mUi; + + /** Constructor. */ + public HostListDirectoryGrabber(Activity ui) { + mAlreadyTried = false; + mUi = ui; + } + + /** + * Retrieves the host list from the directory server. This method performs + * network operations and must be run an a non-UI thread. + */ + @Override + public void run(AccountManagerFuture<Bundle> future) { + Log.i("auth", "User finished with auth dialogs"); + mAlreadyTried = true; + try { + // Here comes our auth token from the Android system. + Bundle result = future.getResult(); + Log.i("auth", "Received an auth token from system"); + mAccount = new Account(result.getString(AccountManager.KEY_ACCOUNT_NAME), + result.getString(AccountManager.KEY_ACCOUNT_TYPE)); + mToken = result.getString(AccountManager.KEY_AUTHTOKEN); + + // Send our HTTP request to the directory server. + URLConnection link = + new URL(HOST_LIST_PATH + JniInterface.getApiKey()).openConnection(); + link.addRequestProperty("client_id", JniInterface.getClientId()); + link.addRequestProperty("client_secret", JniInterface.getClientSecret()); + link.setRequestProperty("Authorization", "OAuth " + mToken); + + // Listen for the server to respond. + StringBuilder response = new StringBuilder(); + Scanner incoming = new Scanner(link.getInputStream()); + Log.i("auth", "Successfully authenticated to directory server"); + while (incoming.hasNext()) { + response.append(incoming.nextLine()); + } + incoming.close(); + + // Interpret what the directory server told us. + JSONObject data = new JSONObject(String.valueOf(response)).getJSONObject("data"); + mHosts = data.getJSONArray("items"); + Log.i("hostlist", "Received host listing from directory server"); + + // Share our findings with the user. + runOnUiThread(new HostListDisplayer(mUi)); + } + catch(Exception ex) { + // Assemble error message to display to the user. + String explanation = getString(R.string.error_unknown); + if (ex instanceof OperationCanceledException) { + explanation = getString(R.string.error_auth_canceled); + } else if (ex instanceof AuthenticatorException) { + explanation = getString(R.string.error_no_accounts); + } else if (ex instanceof IOException) { + if (!mAlreadyTried) { // This was our first connection attempt. + Log.w("auth", "Unable to authenticate with (expired?) token"); + + // Ask system to renew the auth token in case it expired. + AccountManager authenticator = AccountManager.get(mUi); + authenticator.invalidateAuthToken(mAccount.type, mToken); + Log.i("auth", "Requesting auth token renewal"); + authenticator.getAuthToken( + mAccount, TOKEN_SCOPE, null, mUi, this, mNetwork); + + // We're not in an error state *yet.* + return; + } else { // Authentication truly failed. + Log.e("auth", "Fresh auth token was also rejected"); + explanation = getString(R.string.error_auth_failed); + } + } else if (ex instanceof JSONException) { + explanation = getString(R.string.error_unexpected_response); + } + + Log.w("auth", ex); + Toast.makeText(mUi, explanation, Toast.LENGTH_LONG).show(); + + // Close the application. + finish(); + } + } + } + + /** Formats the host list and offers it to the user. */ + private class HostListDisplayer implements Runnable { + /** Communication with the screen. */ + private Activity mUi; + + /** Constructor. */ + public HostListDisplayer(Activity ui) { + mUi = ui; + } + + /** + * Updates the infotext and host list display. + * This method affects the UI and must be run on its same thread. + */ + @Override + public void run() { + mGreeting.setText(getString(R.string.inst_host_list)); + + ArrayAdapter<JSONObject> displayer = new HostListAdapter(mUi, R.layout.host); + Log.i("hostlist", "About to populate host list display"); + try { + int index = 0; + while (!mHosts.isNull(index)) { + displayer.add(mHosts.getJSONObject(index)); + ++index; + } + mList.setAdapter(displayer); + } + catch(JSONException ex) { + Log.w("hostlist", ex); + Toast.makeText( + mUi, getString(R.string.error_cataloging_hosts), Toast.LENGTH_LONG).show(); + + // Close the application. + finish(); + } + } + } + + /** Describes the appearance and behavior of each host list entry. */ + private class HostListAdapter extends ArrayAdapter<JSONObject> { + /** Constructor. */ + public HostListAdapter(Context context, int textViewResourceId) { + super(context, textViewResourceId); + } + + /** Generates a View corresponding to this particular host. */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView target = (TextView)super.getView(position, convertView, parent); + + try { + final JSONObject host = getItem(position); + target.setText(Html.fromHtml(host.getString("hostName") + " (<font color = \"" + + (host.getString("status").equals("ONLINE") ? HOST_COLOR_ONLINE : + HOST_COLOR_OFFLINE) + "\">" + host.getString("status") + "</font>)")); + + if (host.getString("status").equals("ONLINE")) { // Host is online. + target.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + JniInterface.connectToHost( + mAccount.name, mToken, host.getString("jabberId"), + host.getString("hostId"), host.getString("publicKey"), + new Runnable() { + @Override + public void run() { + // TODO(solb) Start an Activity to display the desktop. + } + }); + } + catch(JSONException ex) { + Log.w("host", ex); + Toast.makeText(getContext(), + getString(R.string.error_reading_host), + Toast.LENGTH_LONG).show(); + + // Close the application. + finish(); + } + } + }); + } else { // Host is offline. + // Disallow interaction with this entry. + target.setEnabled(false); + } + } + catch(JSONException ex) { + Log.w("hostlist", ex); + Toast.makeText(getContext(), + getString(R.string.error_displaying_host), + Toast.LENGTH_LONG).show(); + + // Close the application. + finish(); + } + + return target; + } + } +} diff --git a/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java b/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java new file mode 100644 index 0000000..0814993 --- /dev/null +++ b/remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java @@ -0,0 +1,182 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package org.chromium.chromoting.jni; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Bitmap; +import android.text.InputType; +import android.util.Log; +import android.widget.EditText; +import android.widget.Toast; + +import org.chromium.chromoting.R; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Initializes the Chromium remoting library, and provides JNI calls into it. + * All interaction with the native code is centralized in this class. + */ +public class JniInterface { + /** The status code indicating successful connection. */ + private static final int SUCCESSFUL_CONNECTION = 3; + + /** The application context. */ + private static Activity sContext = null; + + /* + * Library-loading state machine. + */ + /** Whether we've already loaded the library. */ + private static boolean sLoaded = false; + + /** To be called once from the main Activity. */ + public static void loadLibrary(Activity context) { + synchronized(JniInterface.class) { + if (sLoaded) return; + } + + System.loadLibrary("remoting_client_jni"); + loadNative(context); + sContext = context; + sLoaded = true; + } + + /** Performs the native portion of the initialization. */ + private static native void loadNative(Context context); + + /* + * API/OAuth2 keys access. + */ + public static native String getApiKey(); + public static native String getClientId(); + public static native String getClientSecret(); + + /* + * Connection-initiating state machine. + */ + /** Whether the native code is attempting a connection. */ + private static boolean sConnected = false; + + /** The callback to signal upon successful connection. */ + private static Runnable sSuccessCallback = null; + + /** Attempts to form a connection to the user-selected host. */ + public static void connectToHost(String username, String authToken, + String hostJid, String hostId, String hostPubkey, Runnable successCallback) { + synchronized(JniInterface.class) { + if (!sLoaded) return; + if (sConnected) { + disconnectFromHost(); + } + } + + sSuccessCallback = successCallback; + connectNative(username, authToken, hostJid, hostId, hostPubkey); + sConnected = true; + } + + /** Severs the connection and cleans up. */ + public static void disconnectFromHost() { + synchronized(JniInterface.class) { + if (!sLoaded || !sConnected) return; + } + + disconnectNative(); + sSuccessCallback = null; + sConnected = false; + } + + /** Performs the native portion of the connection. */ + private static native void connectNative( + String username, String authToken, String hostJid, String hostId, String hostPubkey); + + /** Performs the native portion of the cleanup. */ + private static native void disconnectNative(); + + /* + * Entry points *from* the native code. + */ + /** Screen width of the video feed. */ + private static int sWidth = 0; + + /** Screen height of the video feed. */ + private static int sHeight = 0; + + /** Buffer holding the video feed. */ + private static ByteBuffer sBuffer = null; + + /** Reports whenever the connection status changes. */ + private static void reportConnectionStatus(int state, int error) { + if (state==SUCCESSFUL_CONNECTION) { + sSuccessCallback.run(); + } + + Toast.makeText(sContext, sContext.getResources().getStringArray( + R.array.protoc_states)[state] + (error!=0 ? ": " + + sContext.getResources().getStringArray(R.array.protoc_errors)[error] : ""), + Toast.LENGTH_SHORT).show(); + } + + /** Prompts the user to enter a PIN. */ + private static void displayAuthenticationPrompt() { + AlertDialog.Builder pinPrompt = new AlertDialog.Builder(sContext); + pinPrompt.setTitle(sContext.getString(R.string.pin_entry_title)); + pinPrompt.setMessage(sContext.getString(R.string.pin_entry_message)); + + final EditText pinEntry = new EditText(sContext); + pinEntry.setInputType( + InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); + pinPrompt.setView(pinEntry); + + pinPrompt.setPositiveButton( + R.string.pin_entry_connect, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.i("jniiface", "User provided a PIN code"); + authenticationResponse(String.valueOf(pinEntry.getText())); + } + }); + + pinPrompt.setNegativeButton( + R.string.pin_entry_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.i("jniiface", "User canceled pin entry prompt"); + Toast.makeText(sContext, + sContext.getString(R.string.msg_pin_canceled), + Toast.LENGTH_LONG).show(); + disconnectFromHost(); + } + }); + + pinPrompt.show(); + } + + /** Forces the native graphics thread to redraw to the canvas. */ + public static boolean redrawGraphics() { + synchronized(JniInterface.class) { + if (!sConnected) return false; + } + + scheduleRedrawNative(); + return true; + } + + /** Performs the redrawing callback. */ + private static void redrawGraphicsInternal() { + // TODO(solb) Actually draw the image onto some canvas. + } + + /** Performs the native response to the user's PIN. */ + private static native void authenticationResponse(String pin); + + /** Schedules a redraw on the native graphics thread. */ + private static native void scheduleRedrawNative(); +} diff --git a/remoting/protocol/connection_to_host.h b/remoting/protocol/connection_to_host.h index 01aff15..aadfa7b 100644 --- a/remoting/protocol/connection_to_host.h +++ b/remoting/protocol/connection_to_host.h @@ -51,6 +51,12 @@ class ConnectionToHost : public SignalStrategy::Listener, public Session::EventHandler, public base::NonThreadSafe { public: + // The UI implementations maintain corresponding definitions of this + // enumeration in webapp/client_session.js and + // android/java/res/values/strings.xml. The Android app also includes a + // constant in android/java/src/org/chromium/chromoting/jni/JniInterface.java + // that tracks the numeric value of the CONNECTED state. Be sure to update + // these locations to match this one if you make any changes to the ordering. enum State { INITIALIZING, CONNECTING, diff --git a/remoting/protocol/errors.h b/remoting/protocol/errors.h index 0de2fbb..31bf9265 100644 --- a/remoting/protocol/errors.h +++ b/remoting/protocol/errors.h @@ -8,6 +8,9 @@ namespace remoting { namespace protocol { +// The UI implementations maintain corresponding definitions of this +// enumeration in webapp/error.js and android/java/res/values/strings.xml. +// Be sure to update these locations if you make any changes to the ordering. enum ErrorCode { OK = 0, PEER_IS_OFFLINE, diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp index fd234fb..6011e28 100644 --- a/remoting/remoting.gyp +++ b/remoting/remoting.gyp @@ -1699,6 +1699,49 @@ 'client/jni/jni_interface.cc', ], }, # end of target 'remoting_client_jni' + { + 'target_name': 'remoting_android_resources', + 'type': 'none', + 'copies': [ + { + 'destination': '<(SHARED_INTERMEDIATE_DIR)/remoting/android/res/drawable', + 'files': [ + 'resources/chromoting128.png', + 'resources/icon_host.png', + ], + }, + { + 'destination': '<(SHARED_INTERMEDIATE_DIR)/remoting/android/res/layout', + 'files': [ + 'resources/layout/main.xml', + 'resources/layout/host.xml', + ], + }, + { + 'destination': '<(SHARED_INTERMEDIATE_DIR)/remoting/android/res/values', + 'files': [ + 'resources/strings.xml', + ], + }, + ], + }, # end of target 'remoting_android_resources' + { + 'target_name': 'remoting_apk', + 'type': 'none', + 'dependencies': [ + 'remoting_client_jni', + 'remoting_android_resources', + ], + 'variables': { + 'apk_name': 'Chromoting', + 'manifest_package_name': 'org.chromium.chromoting', + 'native_lib_target': 'libremoting_client_jni', + 'java_in_dir': 'android/java', + 'additional_res_dirs': [ '<(SHARED_INTERMEDIATE_DIR)/remoting/android/res' ], + 'additional_input_paths': [ '<(PRODUCT_DIR)/obj/remoting/remoting_android_resources.actions_rules_copies.stamp' ], + }, + 'includes': [ '../build/java_apk.gypi' ], + }, # end of target 'remoting_apk' ], # end of 'targets' }], # 'OS=="android"' diff --git a/remoting/resources/layout/host.xml b/remoting/resources/layout/host.xml new file mode 100644 index 0000000..c1b4b2d --- /dev/null +++ b/remoting/resources/layout/host.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/host_label" + android:layout_height="wrap_content" + android:layout_width="fill_parent" + android:drawableLeft="@drawable/icon_host" + android:drawablePadding="15sp" + android:padding="15sp" + android:gravity="center_vertical"/> diff --git a/remoting/resources/layout/main.xml b/remoting/resources/layout/main.xml new file mode 100644 index 0000000..d5f419e --- /dev/null +++ b/remoting/resources/layout/main.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_height="fill_parent" + android:layout_width="fill_parent"> + <TextView android:id="@+id/hostList_greeting" + android:layout_height="wrap_content" + android:layout_width="fill_parent"/> + <ListView android:id="@+id/hostList_chooser" + android:layout_height="fill_parent" + android:layout_width="fill_parent"/> +</LinearLayout> diff --git a/remoting/resources/strings.xml b/remoting/resources/strings.xml new file mode 100644 index 0000000..284c2ed --- /dev/null +++ b/remoting/resources/strings.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!--User-facing strings for the Android app--> +<!--TODO(solb) Merge in with localized strings--> +<resources> + <!--Application-wide attributes--> + <string name="app_name">Chromoting</string> + + <!--Instructional blurbs--> + <string name="inst_host_list">My computers:</string> + + <!--Dialog box messages--> + <string name="pin_entry_title">Authenticate to host</string> + <string name="pin_entry_message">Enter the host\'s PIN</string> + <string name="pin_entry_connect">Connect</string> + <string name="pin_entry_cancel">Cancel</string> + + <!--Informative messages--> + <string name="msg_pin_canceled">No PIN was provided, so the connection attempt was canceled</string> + <string name="msg_pin_entered">Attempting to authenticate to specified host with provided PIN</string> + + <!--Error messages--> + <string name="error_unknown">Unexpected error</string> + <string name="error_auth_canceled">Authentication prompt canceled by user</string> + <string name="error_no_accounts">Device not linked to any Google accounts</string> + <string name="error_auth_failed">Authentication with specified account failed</string> + <string name="error_cataloging_hosts">Unable to display host list</string> + <string name="error_displaying_host">Unable to display host entry</string> + <string name="error_unexpected_response">Unexpected response from directory server</string> + <string name="error_reading_host">Unable to read host entry</string> + + <!--Protocol states (see remoting/protocol/connection_to_host.h)--> + <string-array name="protoc_states"> + <item>Initializing protocol</item> + <item>Connecting to host</item> + <item>Authenticated to host</item> + <item>Connected to host</item> + <item>Connection failed</item> + <item>Connection closed</item> + </string-array> + + <!--Protocol errors (see remoting/protocol/errors.h)--> + <string-array name="protoc_errors"> + <item></item> + <item>Host is offline</item> + <item>Host rejected connection</item> + <item>Host using incompatible protocol</item> + <item>Host rejected authentication</item> + <item>Unable to establish data channel</item> + <item>Bad signal</item> + <item>Signal timed out</item> + <item>Host received too many bad PINs</item> + <item>Unknown error</item> + </string-array> +</resources> |