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; 9 import android.content.Context; 10 import android.content.DialogInterface; 11 import android.content.SharedPreferences; 12 import android.graphics.Bitmap; 13 import android.graphics.Point; 14 import android.os.Build; 15 import android.os.Looper; 16 import android.util.Log; 17 import android.view.KeyEvent; 18 import android.view.View; 19 import android.widget.CheckBox; 20 import android.widget.TextView; 21 import android.widget.Toast; 22 23 import org.chromium.base.CalledByNative; 24 import org.chromium.base.JNINamespace; 25 import org.chromium.chromoting.CapabilityManager; 26 import org.chromium.chromoting.Chromoting; 27 import org.chromium.chromoting.R; 28 29 import java.nio.ByteBuffer; 30 import java.nio.ByteOrder; 31 32 /** 33 * Initializes the Chromium remoting library, and provides JNI calls into it. 34 * All interaction with the native code is centralized in this class. 35 */ 36 @JNINamespace("remoting") 37 public class JniInterface { 38 /* 39 * Library-loading state machine. 40 */ 41 /** Whether the library has been loaded. Accessed on the UI thread. */ 42 private static boolean sLoaded = false; 43 44 /** The application context. Accessed on the UI thread. */ 45 private static Activity sContext = null; 46 47 /** Interface used for connection state notifications. */ 48 public interface ConnectionListener { 49 /** 50 * This enum must match the C++ enumeration remoting::protocol::ConnectionToHost::State. 51 */ 52 public enum State { 53 INITIALIZING(0), 54 CONNECTING(1), 55 AUTHENTICATED(2), 56 CONNECTED(3), 57 FAILED(4), 58 CLOSED(5); 59 60 private final int mValue; 61 62 State(int value) { 63 mValue = value; 64 } 65 66 public int value() { 67 return mValue; 68 } 69 70 public static State fromValue(int value) { 71 return values()[value]; 72 } 73 } 74 75 /** 76 * This enum must match the C++ enumeration remoting::protocol::ErrorCode. 77 */ 78 public enum Error { 79 OK(0, 0), 80 PEER_IS_OFFLINE(1, R.string.error_host_is_offline), 81 SESSION_REJECTED(2, R.string.error_invalid_access_code), 82 INCOMPATIBLE_PROTOCOL(3, R.string.error_incompatible_protocol), 83 AUTHENTICATION_FAILED(4, R.string.error_invalid_access_code), 84 CHANNEL_CONNECTION_ERROR(5, R.string.error_p2p_failure), 85 SIGNALING_ERROR(6, R.string.error_p2p_failure), 86 SIGNALING_TIMEOUT(7, R.string.error_p2p_failure), 87 HOST_OVERLOAD(8, R.string.error_host_overload), 88 UNKNOWN_ERROR(9, R.string.error_unexpected); 89 90 private final int mValue; 91 private final int mMessage; 92 93 Error(int value, int message) { 94 mValue = value; 95 mMessage = message; 96 } 97 98 public int value() { 99 return mValue; 100 } 101 102 public int message() { 103 return mMessage; 104 } 105 106 public static Error fromValue(int value) { 107 return values()[value]; 108 } 109 } 110 111 112 /** 113 * Notified on connection state change. 114 * @param state The new connection state. 115 * @param error The error code, if state is STATE_FAILED. 116 */ 117 void onConnectionState(State state, Error error); 118 } 119 120 /* 121 * Connection-initiating state machine. 122 */ 123 /** Whether the native code is attempting a connection. Accessed on the UI thread. */ 124 private static boolean sConnected = false; 125 126 /** Notified upon successful connection or disconnection. Accessed on the UI thread. */ 127 private static ConnectionListener sConnectionListener = null; 128 129 /** 130 * Callback invoked on the graphics thread to repaint the desktop. Accessed on the UI and 131 * graphics threads. 132 */ 133 private static Runnable sRedrawCallback = null; 134 135 /** Bitmap holding a copy of the latest video frame. Accessed on the UI and graphics threads. */ 136 private static Bitmap sFrameBitmap = null; 137 138 /** Protects access to sFrameBitmap. */ 139 private static final Object sFrameLock = new Object(); 140 141 /** Position of cursor hot-spot. Accessed on the graphics thread. */ 142 private static Point sCursorHotspot = new Point(); 143 144 /** Bitmap holding the cursor shape. Accessed on the graphics thread. */ 145 private static Bitmap sCursorBitmap = null; 146 147 /** Capability Manager through which capabilities and extensions are handled. */ 148 private static CapabilityManager sCapabilityManager = CapabilityManager.getInstance(); 149 150 /** 151 * To be called once from the main Activity. Any subsequent calls will update the application 152 * context, but not reload the library. This is useful e.g. when the activity is closed and the 153 * user later wants to return to the application. Called on the UI thread. 154 */ 155 public static void loadLibrary(Activity context) { 156 sContext = context; 157 158 if (sLoaded) return; 159 160 System.loadLibrary("remoting_client_jni"); 161 162 nativeLoadNative(context); 163 sLoaded = true; 164 } 165 166 /** Performs the native portion of the initialization. */ 167 private static native void nativeLoadNative(Context context); 168 169 /* 170 * API/OAuth2 keys access. 171 */ 172 public static native String nativeGetApiKey(); 173 public static native String nativeGetClientId(); 174 public static native String nativeGetClientSecret(); 175 176 /** Attempts to form a connection to the user-selected host. Called on the UI thread. */ 177 public static void connectToHost(String username, String authToken, 178 String hostJid, String hostId, String hostPubkey, ConnectionListener listener) { 179 disconnectFromHost(); 180 181 sConnectionListener = listener; 182 SharedPreferences prefs = sContext.getPreferences(Activity.MODE_PRIVATE); 183 nativeConnect(username, authToken, hostJid, hostId, hostPubkey, 184 prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", ""), 185 sCapabilityManager.getLocalCapabilities()); 186 sConnected = true; 187 } 188 189 /** Performs the native portion of the connection. */ 190 private static native void nativeConnect(String username, String authToken, String hostJid, 191 String hostId, String hostPubkey, String pairId, String pairSecret, 192 String capabilities); 193 194 /** Severs the connection and cleans up. Called on the UI thread. */ 195 public static void disconnectFromHost() { 196 if (!sConnected) { 197 return; 198 } 199 200 sConnectionListener.onConnectionState(ConnectionListener.State.CLOSED, 201 ConnectionListener.Error.OK); 202 203 disconnectFromHostWithoutNotification(); 204 } 205 206 /** Same as disconnectFromHost() but without notifying the ConnectionListener. */ 207 private static void disconnectFromHostWithoutNotification() { 208 if (!sConnected) { 209 return; 210 } 211 212 nativeDisconnect(); 213 sConnectionListener = null; 214 sConnected = false; 215 216 // Drop the reference to free the Bitmap for GC. 217 synchronized (sFrameLock) { 218 sFrameBitmap = null; 219 } 220 } 221 222 /** Performs the native portion of the cleanup. */ 223 private static native void nativeDisconnect(); 224 225 /** Called by native code whenever the connection status changes. Called on the UI thread. */ 226 @CalledByNative 227 private static void onConnectionState(int stateCode, int errorCode) { 228 ConnectionListener.State state = ConnectionListener.State.fromValue(stateCode); 229 ConnectionListener.Error error = ConnectionListener.Error.fromValue(errorCode); 230 sConnectionListener.onConnectionState(state, error); 231 if (state == ConnectionListener.State.FAILED || state == ConnectionListener.State.CLOSED) { 232 // Disconnect from the host here, otherwise the next time connectToHost() is called, 233 // it will try to disconnect, triggering an incorrect status notification. 234 disconnectFromHostWithoutNotification(); 235 } 236 } 237 238 /** Prompts the user to enter a PIN. Called on the UI thread. */ 239 @CalledByNative 240 private static void displayAuthenticationPrompt(boolean pairingSupported) { 241 AlertDialog.Builder pinPrompt = new AlertDialog.Builder(sContext); 242 pinPrompt.setTitle(sContext.getString(R.string.title_authenticate)); 243 pinPrompt.setMessage(sContext.getString(R.string.pin_message_android)); 244 pinPrompt.setIcon(android.R.drawable.ic_lock_lock); 245 246 final View pinEntry = sContext.getLayoutInflater().inflate(R.layout.pin_dialog, null); 247 pinPrompt.setView(pinEntry); 248 249 final TextView pinTextView = (TextView) pinEntry.findViewById(R.id.pin_dialog_text); 250 final CheckBox pinCheckBox = (CheckBox) pinEntry.findViewById(R.id.pin_dialog_check); 251 252 if (!pairingSupported) { 253 pinCheckBox.setChecked(false); 254 pinCheckBox.setVisibility(View.GONE); 255 } 256 257 pinPrompt.setPositiveButton( 258 R.string.connect_button, new DialogInterface.OnClickListener() { 259 @Override 260 public void onClick(DialogInterface dialog, int which) { 261 Log.i("jniiface", "User provided a PIN code"); 262 if (sConnected) { 263 nativeAuthenticationResponse(String.valueOf(pinTextView.getText()), 264 pinCheckBox.isChecked(), Build.MODEL); 265 } else { 266 String message = sContext.getString(R.string.error_network_error); 267 Toast.makeText(sContext, message, Toast.LENGTH_LONG).show(); 268 } 269 } 270 }); 271 272 pinPrompt.setNegativeButton( 273 R.string.cancel, new DialogInterface.OnClickListener() { 274 @Override 275 public void onClick(DialogInterface dialog, int which) { 276 Log.i("jniiface", "User canceled pin entry prompt"); 277 disconnectFromHost(); 278 } 279 }); 280 281 final AlertDialog pinDialog = pinPrompt.create(); 282 283 pinTextView.setOnEditorActionListener( 284 new TextView.OnEditorActionListener() { 285 @Override 286 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 287 // The user pressed enter on the keypad (equivalent to the connect button). 288 pinDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); 289 pinDialog.dismiss(); 290 return true; 291 } 292 }); 293 294 pinDialog.setOnCancelListener( 295 new DialogInterface.OnCancelListener() { 296 @Override 297 public void onCancel(DialogInterface dialog) { 298 // The user backed out of the dialog (equivalent to the cancel button). 299 pinDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick(); 300 } 301 }); 302 303 pinDialog.show(); 304 } 305 306 /** 307 * Performs the native response to the user's PIN. 308 * @param pin The entered PIN. 309 * @param createPair Whether to create a new pairing for this client. 310 * @param deviceName The device name to appear in the pairing registry. Only used if createPair 311 * is true. 312 */ 313 private static native void nativeAuthenticationResponse(String pin, boolean createPair, 314 String deviceName); 315 316 /** Saves newly-received pairing credentials to permanent storage. Called on the UI thread. */ 317 @CalledByNative 318 private static void commitPairingCredentials(String host, String id, String secret) { 319 // Empty |id| indicates that pairing needs to be removed. 320 if (id.isEmpty()) { 321 sContext.getPreferences(Activity.MODE_PRIVATE).edit(). 322 remove(host + "_id"). 323 remove(host + "_secret"). 324 apply(); 325 } else { 326 sContext.getPreferences(Activity.MODE_PRIVATE).edit(). 327 putString(host + "_id", id). 328 putString(host + "_secret", secret). 329 apply(); 330 } 331 } 332 333 /** 334 * Moves the mouse cursor, possibly while clicking the specified (nonnegative) button. Called 335 * on the UI thread. 336 */ 337 public static void sendMouseEvent(int x, int y, int whichButton, boolean buttonDown) { 338 if (!sConnected) { 339 return; 340 } 341 342 nativeSendMouseEvent(x, y, whichButton, buttonDown); 343 } 344 345 /** Passes mouse information to the native handling code. */ 346 private static native void nativeSendMouseEvent(int x, int y, int whichButton, 347 boolean buttonDown); 348 349 /** Injects a mouse-wheel event with delta values. Called on the UI thread. */ 350 public static void sendMouseWheelEvent(int deltaX, int deltaY) { 351 if (!sConnected) { 352 return; 353 } 354 355 nativeSendMouseWheelEvent(deltaX, deltaY); 356 } 357 358 /** Passes mouse-wheel information to the native handling code. */ 359 private static native void nativeSendMouseWheelEvent(int deltaX, int deltaY); 360 361 /** Presses or releases the specified (nonnegative) key. Called on the UI thread. */ 362 public static boolean sendKeyEvent(int keyCode, boolean keyDown) { 363 if (!sConnected) { 364 return false; 365 } 366 367 return nativeSendKeyEvent(keyCode, keyDown); 368 } 369 370 /** Passes key press information to the native handling code. */ 371 private static native boolean nativeSendKeyEvent(int keyCode, boolean keyDown); 372 373 /** Sends TextEvent to the host. Called on the UI thread. */ 374 public static void sendTextEvent(String text) { 375 if (!sConnected) { 376 return; 377 } 378 379 nativeSendTextEvent(text); 380 } 381 382 /** Passes text event information to the native handling code. */ 383 private static native void nativeSendTextEvent(String text); 384 385 /** 386 * Sets the redraw callback to the provided functor. Provide a value of null whenever the 387 * window is no longer visible so that we don't continue to draw onto it. Called on the UI 388 * thread. 389 */ 390 public static void provideRedrawCallback(Runnable redrawCallback) { 391 sRedrawCallback = redrawCallback; 392 } 393 394 /** Forces the native graphics thread to redraw to the canvas. Called on the UI thread. */ 395 public static boolean redrawGraphics() { 396 if (!sConnected || sRedrawCallback == null) return false; 397 398 nativeScheduleRedraw(); 399 return true; 400 } 401 402 /** Schedules a redraw on the native graphics thread. */ 403 private static native void nativeScheduleRedraw(); 404 405 /** 406 * Performs the redrawing callback. This is a no-op if the window isn't visible. Called on the 407 * graphics thread. 408 */ 409 @CalledByNative 410 private static void redrawGraphicsInternal() { 411 Runnable callback = sRedrawCallback; 412 if (callback != null) { 413 callback.run(); 414 } 415 } 416 417 /** 418 * Returns a bitmap of the latest video frame. Called on the native graphics thread when 419 * DesktopView is repainted. 420 */ 421 public static Bitmap getVideoFrame() { 422 if (Looper.myLooper() == Looper.getMainLooper()) { 423 Log.w("jniiface", "Canvas being redrawn on UI thread"); 424 } 425 426 synchronized (sFrameLock) { 427 return sFrameBitmap; 428 } 429 } 430 431 /** 432 * Sets a new video frame. Called on the native graphics thread when a new frame is allocated. 433 */ 434 @CalledByNative 435 private static void setVideoFrame(Bitmap bitmap) { 436 if (Looper.myLooper() == Looper.getMainLooper()) { 437 Log.w("jniiface", "Video frame updated on UI thread"); 438 } 439 440 synchronized (sFrameLock) { 441 sFrameBitmap = bitmap; 442 } 443 } 444 445 /** 446 * Creates a new Bitmap to hold video frame pixels. Called by native code which stores a global 447 * reference to the Bitmap and writes the decoded frame pixels to it. 448 */ 449 @CalledByNative 450 private static Bitmap newBitmap(int width, int height) { 451 return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 452 } 453 454 /** 455 * Updates the cursor shape. This is called on the graphics thread when receiving a new cursor 456 * shape from the host. 457 */ 458 @CalledByNative 459 public static void updateCursorShape(int width, int height, int hotspotX, int hotspotY, 460 ByteBuffer buffer) { 461 sCursorHotspot = new Point(hotspotX, hotspotY); 462 463 int[] data = new int[width * height]; 464 buffer.order(ByteOrder.LITTLE_ENDIAN); 465 buffer.asIntBuffer().get(data, 0, data.length); 466 sCursorBitmap = Bitmap.createBitmap(data, width, height, Bitmap.Config.ARGB_8888); 467 } 468 469 /** Position of cursor hotspot within cursor image. Called on the graphics thread. */ 470 public static Point getCursorHotspot() { return sCursorHotspot; } 471 472 /** Returns the current cursor shape. Called on the graphics thread. */ 473 public static Bitmap getCursorBitmap() { return sCursorBitmap; } 474 475 // 476 // Third Party Authentication 477 // 478 479 /** Pops up a third party login page to fetch the token required for authentication. */ 480 @CalledByNative 481 public static void fetchThirdPartyToken(String tokenUrl, String clientId, String scope) { 482 Chromoting app = (Chromoting) sContext; 483 app.fetchThirdPartyToken(tokenUrl, clientId, scope); 484 } 485 486 /** 487 * Notify the native code to continue authentication with the |token| and the |sharedSecret|. 488 */ 489 public static void onThirdPartyTokenFetched(String token, String sharedSecret) { 490 if (!sConnected) { 491 return; 492 } 493 494 nativeOnThirdPartyTokenFetched(token, sharedSecret); 495 } 496 497 /** Passes authentication data to the native handling code. */ 498 private static native void nativeOnThirdPartyTokenFetched(String token, String sharedSecret); 499 500 // 501 // Host and Client Capabilities 502 // 503 504 /** Set the list of negotiated capabilities between host and client. Called on the UI thread. */ 505 @CalledByNative 506 public static void setCapabilities(String capabilities) { 507 sCapabilityManager.setNegotiatedCapabilities(capabilities); 508 } 509 510 // 511 // Extension Message Handling 512 // 513 514 /** Passes on the deconstructed ExtensionMessage to the app. Called on the UI thread. */ 515 @CalledByNative 516 public static void handleExtensionMessage(String type, String data) { 517 sCapabilityManager.onExtensionMessage(type, data); 518 } 519 520 /** Sends an extension message to the Chromoting host. Called on the UI thread. */ 521 public static void sendExtensionMessage(String type, String data) { 522 if (!sConnected) { 523 return; 524 } 525 526 nativeSendExtensionMessage(type, data); 527 } 528 529 private static native void nativeSendExtensionMessage(String type, String data); 530 } 531