blob: 039bcb90f31340c978f93a1187095ed305cd99bc [file] [log] [blame]
Ben Murdochca12bfa2013-07-23 11:17:05 +01001// 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
5package org.chromium.chromoting.jni;
6
7import android.app.Activity;
8import android.app.AlertDialog;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +01009import android.app.ProgressDialog;
Ben Murdochca12bfa2013-07-23 11:17:05 +010010import android.content.Context;
11import android.content.DialogInterface;
Ben Murdochbb1529c2013-08-08 10:24:53 +010012import android.content.SharedPreferences;
Ben Murdochca12bfa2013-07-23 11:17:05 +010013import android.graphics.Bitmap;
14import android.os.Looper;
15import android.text.InputType;
16import android.util.Log;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010017import android.view.KeyEvent;
Ben Murdochbb1529c2013-08-08 10:24:53 +010018import android.view.View;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010019import android.view.inputmethod.EditorInfo;
Ben Murdochbb1529c2013-08-08 10:24:53 +010020import android.widget.CheckBox;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010021import android.widget.TextView;
Ben Murdochca12bfa2013-07-23 11:17:05 +010022import android.widget.Toast;
23
24import org.chromium.chromoting.R;
25
26import java.nio.ByteBuffer;
27import java.nio.ByteOrder;
28
29/**
30 * Initializes the Chromium remoting library, and provides JNI calls into it.
31 * All interaction with the native code is centralized in this class.
32 */
33public class JniInterface {
34 /** The status code indicating successful connection. */
35 private static final int SUCCESSFUL_CONNECTION = 3;
36
37 /** The application context. */
38 private static Activity sContext = null;
39
40 /*
41 * Library-loading state machine.
42 */
43 /** Whether we've already loaded the library. */
44 private static boolean sLoaded = false;
45
Ben Murdoch558790d2013-07-30 15:19:42 +010046 /**
47 * To be called once from the main Activity. Any subsequent calls will update the application
48 * context, but not reload the library. This is useful e.g. when the activity is closed and the
49 * user later wants to return to the application.
50 */
Ben Murdochca12bfa2013-07-23 11:17:05 +010051 public static void loadLibrary(Activity context) {
Ben Murdoch558790d2013-07-30 15:19:42 +010052 sContext = context;
53
Ben Murdochca12bfa2013-07-23 11:17:05 +010054 synchronized(JniInterface.class) {
55 if (sLoaded) return;
56 }
57
58 System.loadLibrary("remoting_client_jni");
59 loadNative(context);
Ben Murdochca12bfa2013-07-23 11:17:05 +010060 sLoaded = true;
61 }
62
63 /** Performs the native portion of the initialization. */
64 private static native void loadNative(Context context);
65
66 /*
67 * API/OAuth2 keys access.
68 */
69 public static native String getApiKey();
70 public static native String getClientId();
71 public static native String getClientSecret();
72
73 /*
74 * Connection-initiating state machine.
75 */
76 /** Whether the native code is attempting a connection. */
77 private static boolean sConnected = false;
78
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010079 /** Callback to signal upon successful connection. */
Ben Murdochca12bfa2013-07-23 11:17:05 +010080 private static Runnable sSuccessCallback = null;
81
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010082 /** Dialog for reporting connection progress. */
83 private static ProgressDialog sProgressIndicator = null;
84
Ben Murdochca12bfa2013-07-23 11:17:05 +010085 /** Attempts to form a connection to the user-selected host. */
86 public static void connectToHost(String username, String authToken,
87 String hostJid, String hostId, String hostPubkey, Runnable successCallback) {
88 synchronized(JniInterface.class) {
89 if (!sLoaded) return;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +010090
Ben Murdochca12bfa2013-07-23 11:17:05 +010091 if (sConnected) {
92 disconnectFromHost();
93 }
94 }
95
96 sSuccessCallback = successCallback;
Ben Murdochbb1529c2013-08-08 10:24:53 +010097 SharedPreferences prefs = sContext.getPreferences(Activity.MODE_PRIVATE);
98 connectNative(username, authToken, hostJid, hostId, hostPubkey,
99 prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", ""));
Ben Murdochca12bfa2013-07-23 11:17:05 +0100100 sConnected = true;
101 }
102
103 /** Severs the connection and cleans up. */
104 public static void disconnectFromHost() {
105 synchronized(JniInterface.class) {
106 if (!sLoaded || !sConnected) return;
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100107
108 if (sProgressIndicator != null) {
109 sProgressIndicator.dismiss();
110 sProgressIndicator = null;
111 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100112 }
113
114 disconnectNative();
115 sSuccessCallback = null;
116 sConnected = false;
117 }
118
119 /** Performs the native portion of the connection. */
Ben Murdochbb1529c2013-08-08 10:24:53 +0100120 private static native void connectNative(String username, String authToken, String hostJid,
121 String hostId, String hostPubkey, String pairId, String pairSecret);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100122
123 /** Performs the native portion of the cleanup. */
124 private static native void disconnectNative();
125
126 /*
127 * Entry points *from* the native code.
128 */
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100129 /** Callback to signal whenever we need to redraw. */
Ben Murdochca12bfa2013-07-23 11:17:05 +0100130 private static Runnable sRedrawCallback = null;
131
132 /** Screen width of the video feed. */
133 private static int sWidth = 0;
134
135 /** Screen height of the video feed. */
136 private static int sHeight = 0;
137
138 /** Buffer holding the video feed. */
139 private static ByteBuffer sBuffer = null;
140
141 /** Reports whenever the connection status changes. */
142 private static void reportConnectionStatus(int state, int error) {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100143 if (state < SUCCESSFUL_CONNECTION && error == 0) {
144 // The connection is still being established, so we'll report the current progress.
145 synchronized (JniInterface.class) {
146 if (sProgressIndicator == null) {
147 sProgressIndicator = ProgressDialog.show(sContext, sContext.
148 getString(R.string.progress_title), sContext.getResources().
149 getStringArray(R.array.protoc_states)[state], true, true,
150 new DialogInterface.OnCancelListener() {
151 @Override
152 public void onCancel(DialogInterface dialog) {
153 Log.i("jniiface", "User canceled connection initiation");
154 disconnectFromHost();
155 }
156 });
157 }
158 else {
159 sProgressIndicator.setMessage(
160 sContext.getResources().getStringArray(R.array.protoc_states)[state]);
161 }
162 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100163 }
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100164 else {
165 // The connection is complete or has failed, so we can lose the progress indicator.
166 synchronized (JniInterface.class) {
167 if (sProgressIndicator != null) {
168 sProgressIndicator.dismiss();
169 sProgressIndicator = null;
170 }
171 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100172
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100173 if (state == SUCCESSFUL_CONNECTION) {
174 Toast.makeText(sContext, sContext.getResources().
175 getStringArray(R.array.protoc_states)[state], Toast.LENGTH_SHORT).show();
176
177 // Actually display the remote desktop.
178 sSuccessCallback.run();
179 } else {
180 Toast.makeText(sContext, sContext.getResources().getStringArray(
181 R.array.protoc_states)[state] + (error == 0 ? "" : ": " +
182 sContext.getResources().getStringArray(R.array.protoc_errors)[error]),
183 Toast.LENGTH_LONG).show();
184 }
185 }
Ben Murdochca12bfa2013-07-23 11:17:05 +0100186 }
187
188 /** Prompts the user to enter a PIN. */
189 private static void displayAuthenticationPrompt() {
190 AlertDialog.Builder pinPrompt = new AlertDialog.Builder(sContext);
191 pinPrompt.setTitle(sContext.getString(R.string.pin_entry_title));
192 pinPrompt.setMessage(sContext.getString(R.string.pin_entry_message));
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100193 pinPrompt.setIcon(android.R.drawable.ic_lock_lock);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100194
Ben Murdochbb1529c2013-08-08 10:24:53 +0100195 final View pinEntry = sContext.getLayoutInflater().inflate(R.layout.pin_dialog, null);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100196 pinPrompt.setView(pinEntry);
197
198 pinPrompt.setPositiveButton(
199 R.string.pin_entry_connect, new DialogInterface.OnClickListener() {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100200 @Override
201 public void onClick(DialogInterface dialog, int which) {
202 Log.i("jniiface", "User provided a PIN code");
Ben Murdochbb1529c2013-08-08 10:24:53 +0100203 authenticationResponse(String.valueOf(
204 ((TextView)
205 pinEntry.findViewById(R.id.pin_dialog_text)).getText()),
206 ((CheckBox)
207 pinEntry.findViewById(R.id.pin_dialog_check)).isChecked());
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100208 }
209 });
Ben Murdochca12bfa2013-07-23 11:17:05 +0100210
211 pinPrompt.setNegativeButton(
212 R.string.pin_entry_cancel, new DialogInterface.OnClickListener() {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100213 @Override
214 public void onClick(DialogInterface dialog, int which) {
215 Log.i("jniiface", "User canceled pin entry prompt");
216 Toast.makeText(sContext,
217 sContext.getString(R.string.msg_pin_canceled),
218 Toast.LENGTH_LONG).show();
219 disconnectFromHost();
220 }
221 });
Ben Murdochca12bfa2013-07-23 11:17:05 +0100222
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100223 final AlertDialog pinDialog = pinPrompt.create();
224
Ben Murdochbb1529c2013-08-08 10:24:53 +0100225 ((TextView)pinEntry.findViewById(R.id.pin_dialog_text)).setOnEditorActionListener(
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100226 new TextView.OnEditorActionListener() {
227 @Override
228 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
229 // The user pressed enter on the keypad (equivalent to the connect button).
230 pinDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
231 pinDialog.dismiss();
232 return true;
233 }
234 });
235
236 pinDialog.setOnCancelListener(
237 new DialogInterface.OnCancelListener() {
238 @Override
239 public void onCancel(DialogInterface dialog) {
240 // The user backed out of the dialog (equivalent to the cancel button).
241 pinDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick();
242 }
243 });
244
245 pinDialog.show();
Ben Murdochca12bfa2013-07-23 11:17:05 +0100246 }
247
Ben Murdochbb1529c2013-08-08 10:24:53 +0100248 /** Saves newly-received pairing credentials to permanent storage. */
249 private static void commitPairingCredentials(String host, byte[] id, byte[] secret) {
250 synchronized (sContext) {
251 sContext.getPreferences(Activity.MODE_PRIVATE).edit().
252 putString(host + "_id", new String(id)).
253 putString(host + "_secret", new String(secret)).
254 apply();
255 }
256 }
257
Ben Murdoch558790d2013-07-30 15:19:42 +0100258 /**
259 * Sets the redraw callback to the provided functor. Provide a value of null whenever the
260 * window is no longer visible so that we don't continue to draw onto it.
261 */
Ben Murdochca12bfa2013-07-23 11:17:05 +0100262 public static void provideRedrawCallback(Runnable redrawCallback) {
263 sRedrawCallback = redrawCallback;
264 }
265
266 /** Forces the native graphics thread to redraw to the canvas. */
267 public static boolean redrawGraphics() {
268 synchronized(JniInterface.class) {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100269 if (!sConnected || sRedrawCallback == null) return false;
Ben Murdochca12bfa2013-07-23 11:17:05 +0100270 }
271
272 scheduleRedrawNative();
273 return true;
274 }
275
Ben Murdoch558790d2013-07-30 15:19:42 +0100276 /** Performs the redrawing callback. This is a no-op if the window isn't visible. */
Ben Murdochca12bfa2013-07-23 11:17:05 +0100277 private static void redrawGraphicsInternal() {
Ben Murdoch558790d2013-07-30 15:19:42 +0100278 if (sRedrawCallback != null)
279 sRedrawCallback.run();
Ben Murdochca12bfa2013-07-23 11:17:05 +0100280 }
281
282 /**
283 * Obtains the image buffer.
284 * This should not be called from the UI thread. (We prefer the native graphics thread.)
285 */
286 public static Bitmap retrieveVideoFrame() {
287 if (Looper.myLooper() == Looper.getMainLooper()) {
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100288 Log.w("jniiface", "Canvas being redrawn on UI thread");
Ben Murdochca12bfa2013-07-23 11:17:05 +0100289 }
290
291 if (!sConnected) {
292 return null;
293 }
294
295 int[] frame = new int[sWidth * sHeight];
296
297 sBuffer.order(ByteOrder.LITTLE_ENDIAN);
298 sBuffer.asIntBuffer().get(frame, 0, frame.length);
299
300 return Bitmap.createBitmap(frame, 0, sWidth, sWidth, sHeight, Bitmap.Config.ARGB_8888);
301 }
302
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100303 /** Moves the mouse cursor, possibly while clicking the specified (nonnegative) button. */
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100304 public static void mouseAction(int x, int y, int whichButton, boolean buttonDown) {
305 if (!sConnected) {
306 return;
307 }
308
309 mouseActionNative(x, y, whichButton, buttonDown);
310 }
311
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100312 /** Presses and releases the specified (nonnegative) key. */
313 public static void keyboardAction(int keyCode, boolean keyDown) {
314 if (!sConnected) {
315 return;
316 }
317
318 keyboardActionNative(keyCode, keyDown);
319 }
320
Ben Murdochca12bfa2013-07-23 11:17:05 +0100321 /** Performs the native response to the user's PIN. */
Ben Murdochbb1529c2013-08-08 10:24:53 +0100322 private static native void authenticationResponse(String pin, boolean createPair);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100323
324 /** Schedules a redraw on the native graphics thread. */
325 private static native void scheduleRedrawNative();
Ben Murdocha3f7b4e2013-07-24 10:36:34 +0100326
327 /** Passes mouse information to the native handling code. */
328 private static native void mouseActionNative(int x, int y, int whichButton, boolean buttonDown);
Torne (Richard Coles)a36e5922013-08-05 13:57:33 +0100329
330 /** Passes key press information to the native handling code. */
331 private static native void keyboardActionNative(int keyCode, boolean keyDown);
Ben Murdochca12bfa2013-07-23 11:17:05 +0100332}