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