summaryrefslogtreecommitdiffstats
path: root/remoting
diff options
context:
space:
mode:
authorsolb@chromium.org <solb@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-07-19 01:49:09 +0000
committersolb@chromium.org <solb@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-07-19 01:49:09 +0000
commit8812dce4ee4efb8c652e6150bb7085c8e40c63da (patch)
tree985556b5f2a374e532fbfb3004ae52765fae1fb9 /remoting
parent9992898cbe9872e5b5e0b9cead5b02ea9e830756 (diff)
downloadchromium_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/OWNERS3
-rw-r--r--remoting/android/java/AndroidManifest.xml22
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/Chromoting.java317
-rw-r--r--remoting/android/java/src/org/chromium/chromoting/jni/JniInterface.java182
-rw-r--r--remoting/protocol/connection_to_host.h6
-rw-r--r--remoting/protocol/errors.h3
-rw-r--r--remoting/remoting.gyp43
-rw-r--r--remoting/resources/layout/host.xml9
-rw-r--r--remoting/resources/layout/main.xml12
-rw-r--r--remoting/resources/strings.xml54
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>