Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 1 | // 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 | |
| 5 | package org.chromium.chromoting.jni; |
| 6 | |
| 7 | import android.app.Activity; |
| 8 | import android.app.AlertDialog; |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 9 | import android.app.ProgressDialog; |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 10 | import android.content.Context; |
| 11 | import android.content.DialogInterface; |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 12 | import android.content.SharedPreferences; |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 13 | import android.graphics.Bitmap; |
| 14 | import android.os.Looper; |
| 15 | import android.text.InputType; |
| 16 | import android.util.Log; |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 17 | import android.view.KeyEvent; |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 18 | import android.view.View; |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 19 | import android.view.inputmethod.EditorInfo; |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 20 | import android.widget.CheckBox; |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 21 | import android.widget.TextView; |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 22 | import android.widget.Toast; |
| 23 | |
| 24 | import org.chromium.chromoting.R; |
| 25 | |
| 26 | import java.nio.ByteBuffer; |
| 27 | import 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 | */ |
| 33 | public 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 Murdoch | 558790d | 2013-07-30 15:19:42 +0100 | [diff] [blame] | 46 | /** |
| 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 Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 51 | public static void loadLibrary(Activity context) { |
Ben Murdoch | 558790d | 2013-07-30 15:19:42 +0100 | [diff] [blame] | 52 | sContext = context; |
| 53 | |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 54 | synchronized(JniInterface.class) { |
| 55 | if (sLoaded) return; |
| 56 | } |
| 57 | |
| 58 | System.loadLibrary("remoting_client_jni"); |
| 59 | loadNative(context); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 60 | 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 79 | /** Callback to signal upon successful connection. */ |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 80 | private static Runnable sSuccessCallback = null; |
| 81 | |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 82 | /** Dialog for reporting connection progress. */ |
| 83 | private static ProgressDialog sProgressIndicator = null; |
| 84 | |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 85 | /** 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 90 | |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 91 | if (sConnected) { |
| 92 | disconnectFromHost(); |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | sSuccessCallback = successCallback; |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 97 | SharedPreferences prefs = sContext.getPreferences(Activity.MODE_PRIVATE); |
| 98 | connectNative(username, authToken, hostJid, hostId, hostPubkey, |
| 99 | prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", "")); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 100 | 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 107 | |
| 108 | if (sProgressIndicator != null) { |
| 109 | sProgressIndicator.dismiss(); |
| 110 | sProgressIndicator = null; |
| 111 | } |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 112 | } |
| 113 | |
| 114 | disconnectNative(); |
| 115 | sSuccessCallback = null; |
| 116 | sConnected = false; |
| 117 | } |
| 118 | |
| 119 | /** Performs the native portion of the connection. */ |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 120 | private static native void connectNative(String username, String authToken, String hostJid, |
| 121 | String hostId, String hostPubkey, String pairId, String pairSecret); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 122 | |
| 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 129 | /** Callback to signal whenever we need to redraw. */ |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 130 | 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 143 | 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 Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 163 | } |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 164 | 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 Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 172 | |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 173 | 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 Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 186 | } |
| 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 193 | pinPrompt.setIcon(android.R.drawable.ic_lock_lock); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 194 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 195 | final View pinEntry = sContext.getLayoutInflater().inflate(R.layout.pin_dialog, null); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 196 | pinPrompt.setView(pinEntry); |
| 197 | |
| 198 | pinPrompt.setPositiveButton( |
| 199 | R.string.pin_entry_connect, new DialogInterface.OnClickListener() { |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 200 | @Override |
| 201 | public void onClick(DialogInterface dialog, int which) { |
| 202 | Log.i("jniiface", "User provided a PIN code"); |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 203 | 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 208 | } |
| 209 | }); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 210 | |
| 211 | pinPrompt.setNegativeButton( |
| 212 | R.string.pin_entry_cancel, new DialogInterface.OnClickListener() { |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 213 | @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 Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 222 | |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 223 | final AlertDialog pinDialog = pinPrompt.create(); |
| 224 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 225 | ((TextView)pinEntry.findViewById(R.id.pin_dialog_text)).setOnEditorActionListener( |
Torne (Richard Coles) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 226 | 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 Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 246 | } |
| 247 | |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 248 | /** 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 Murdoch | 558790d | 2013-07-30 15:19:42 +0100 | [diff] [blame] | 258 | /** |
| 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 Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 262 | 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 269 | if (!sConnected || sRedrawCallback == null) return false; |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 270 | } |
| 271 | |
| 272 | scheduleRedrawNative(); |
| 273 | return true; |
| 274 | } |
| 275 | |
Ben Murdoch | 558790d | 2013-07-30 15:19:42 +0100 | [diff] [blame] | 276 | /** Performs the redrawing callback. This is a no-op if the window isn't visible. */ |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 277 | private static void redrawGraphicsInternal() { |
Ben Murdoch | 558790d | 2013-07-30 15:19:42 +0100 | [diff] [blame] | 278 | if (sRedrawCallback != null) |
| 279 | sRedrawCallback.run(); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 280 | } |
| 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 288 | Log.w("jniiface", "Canvas being redrawn on UI thread"); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 289 | } |
| 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 303 | /** Moves the mouse cursor, possibly while clicking the specified (nonnegative) button. */ |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 304 | 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 312 | /** 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 Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 321 | /** Performs the native response to the user's PIN. */ |
Ben Murdoch | bb1529c | 2013-08-08 10:24:53 +0100 | [diff] [blame^] | 322 | private static native void authenticationResponse(String pin, boolean createPair); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 323 | |
| 324 | /** Schedules a redraw on the native graphics thread. */ |
| 325 | private static native void scheduleRedrawNative(); |
Ben Murdoch | a3f7b4e | 2013-07-24 10:36:34 +0100 | [diff] [blame] | 326 | |
| 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) | a36e592 | 2013-08-05 13:57:33 +0100 | [diff] [blame] | 329 | |
| 330 | /** Passes key press information to the native handling code. */ |
| 331 | private static native void keyboardActionNative(int keyCode, boolean keyDown); |
Ben Murdoch | ca12bfa | 2013-07-23 11:17:05 +0100 | [diff] [blame] | 332 | } |