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 60 State(int value) { 61 mValue = value; 62 } 63 64 public int value() { 65 return mValue; 66 } 67 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 91 Error(int value, int message) { 92 mValue = value; 93 mMessage = message; 94 } 95 96 public int value() { 97 return mValue; 98 } 99 100 public int message() { 101 return mMessage; 102 } 103 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 */ 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 */ 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. */ 162 private static native void nativeLoadNative(Context context); 163 164 /* 165 * API/OAuth2 keys access. 166 */ 167 public static native String nativeGetApiKey(); 168 public static native String nativeGetClientId(); 169 public static native String nativeGetClientSecret(); 170 171 /** Attempts to form a connection to the user-selected host. Called on the UI thread. */ 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. */ 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. */ 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. */ 205 private static native void nativeDisconnect(); 206 207 /** Reports whenever the connection status changes. Called on the UI thread. */ 208 @CalledByNative 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 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 */ 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 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 */ 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. */ 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. */ 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. */ 322 private static native void nativeSendMouseWheelEvent(int deltaX, int deltaY); 323 324 /** Presses or releases the specified (nonnegative) key. Called on the UI thread. */ 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. */ 334 private static native boolean nativeSendKeyEvent(int keyCode, boolean keyDown); 335 336 /** Sends TextEvent to the host. Called on the UI thread. */ 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. */ 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 */ 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. */ 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. */ 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 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 */ 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 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 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 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. */ 433 public static Point getCursorHotspot() { return sCursorHotspot; } 434 435 /** Returns the current cursor shape. Called on the graphics thread. */ 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 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 */ 452 public static native void nativeOnThirdPartyTokenFetched(String token, String sharedSecret); 453 } 454