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.
5 package org
.chromium
.chromoting
.jni
;
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
;
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
;
29 import java
.nio
.ByteBuffer
;
30 import java
.nio
.ByteOrder
;
33 * Initializes the Chromium remoting library, and provides JNI calls into it.
34 * All interaction with the native code is centralized in this class.
36 @JNINamespace("remoting")
37 public class JniInterface
{
39 * Library-loading state machine.
41 /** Whether the library has been loaded. Accessed on the UI thread. */
42 private static boolean sLoaded
= false;
44 /** The application context. Accessed on the UI thread. */
45 private static Activity sContext
= null;
47 /** Interface used for connection state notifications. */
48 public interface ConnectionListener
{
50 * This enum must match the C++ enumeration remoting::protocol::ConnectionToHost::State.
60 private final int mValue
;
70 public static State
fromValue(int value
) {
71 return values()[value
];
76 * This enum must match the C++ enumeration remoting::protocol::ErrorCode.
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
);
90 private final int mValue
;
91 private final int mMessage
;
93 Error(int value
, int message
) {
102 public int message() {
106 public static Error
fromValue(int value
) {
107 return values()[value
];
113 * Notified on connection state change.
114 * @param state The new connection state.
115 * @param error The error code, if state is STATE_FAILED.
117 void onConnectionState(State state
, Error error
);
121 * Connection-initiating state machine.
123 /** Whether the native code is attempting a connection. Accessed on the UI thread. */
124 private static boolean sConnected
= false;
126 /** Notified upon successful connection or disconnection. Accessed on the UI thread. */
127 private static ConnectionListener sConnectionListener
= null;
130 * Callback invoked on the graphics thread to repaint the desktop. Accessed on the UI and
133 private static Runnable sRedrawCallback
= null;
135 /** Bitmap holding a copy of the latest video frame. Accessed on the UI and graphics threads. */
136 private static Bitmap sFrameBitmap
= null;
138 /** Protects access to sFrameBitmap. */
139 private static final Object sFrameLock
= new Object();
141 /** Position of cursor hot-spot. Accessed on the graphics thread. */
142 private static Point sCursorHotspot
= new Point();
144 /** Bitmap holding the cursor shape. Accessed on the graphics thread. */
145 private static Bitmap sCursorBitmap
= null;
147 /** Capability Manager through which capabilities and extensions are handled. */
148 private static CapabilityManager sCapabilityManager
= CapabilityManager
.getInstance();
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.
155 public static void loadLibrary(Activity context
) {
160 System
.loadLibrary("remoting_client_jni");
162 nativeLoadNative(context
);
166 /** Performs the native portion of the initialization. */
167 private static native void nativeLoadNative(Context context
);
170 * API/OAuth2 keys access.
172 public static native String
nativeGetApiKey();
173 public static native String
nativeGetClientId();
174 public static native String
nativeGetClientSecret();
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();
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());
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
);
194 /** Severs the connection and cleans up. Called on the UI thread. */
195 public static void disconnectFromHost() {
200 sConnectionListener
.onConnectionState(ConnectionListener
.State
.CLOSED
,
201 ConnectionListener
.Error
.OK
);
203 disconnectFromHostWithoutNotification();
206 /** Same as disconnectFromHost() but without notifying the ConnectionListener. */
207 private static void disconnectFromHostWithoutNotification() {
213 sConnectionListener
= null;
216 // Drop the reference to free the Bitmap for GC.
217 synchronized (sFrameLock
) {
222 /** Performs the native portion of the cleanup. */
223 private static native void nativeDisconnect();
225 /** Called by native code whenever the connection status changes. Called on the UI thread. */
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();
238 /** Prompts the user to enter a PIN. Called on the UI thread. */
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
);
246 final View pinEntry
= sContext
.getLayoutInflater().inflate(R
.layout
.pin_dialog
, null);
247 pinPrompt
.setView(pinEntry
);
249 final TextView pinTextView
= (TextView
)pinEntry
.findViewById(R
.id
.pin_dialog_text
);
250 final CheckBox pinCheckBox
= (CheckBox
)pinEntry
.findViewById(R
.id
.pin_dialog_check
);
252 if (!pairingSupported
) {
253 pinCheckBox
.setChecked(false);
254 pinCheckBox
.setVisibility(View
.GONE
);
257 pinPrompt
.setPositiveButton(
258 R
.string
.connect_button
, new DialogInterface
.OnClickListener() {
260 public void onClick(DialogInterface dialog
, int which
) {
261 Log
.i("jniiface", "User provided a PIN code");
263 nativeAuthenticationResponse(String
.valueOf(pinTextView
.getText()),
264 pinCheckBox
.isChecked(), Build
.MODEL
);
266 String message
= sContext
.getString(R
.string
.error_network_error
);
267 Toast
.makeText(sContext
, message
, Toast
.LENGTH_LONG
).show();
272 pinPrompt
.setNegativeButton(
273 R
.string
.cancel
, new DialogInterface
.OnClickListener() {
275 public void onClick(DialogInterface dialog
, int which
) {
276 Log
.i("jniiface", "User canceled pin entry prompt");
277 disconnectFromHost();
281 final AlertDialog pinDialog
= pinPrompt
.create();
283 pinTextView
.setOnEditorActionListener(
284 new TextView
.OnEditorActionListener() {
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();
294 pinDialog
.setOnCancelListener(
295 new DialogInterface
.OnCancelListener() {
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();
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
313 private static native void nativeAuthenticationResponse(String pin
, boolean createPair
,
316 /** Saves newly-received pairing credentials to permanent storage. Called on the UI thread. */
318 private static void commitPairingCredentials(String host
, String id
, String secret
) {
319 // Empty |id| indicates that pairing needs to be removed.
321 sContext
.getPreferences(Activity
.MODE_PRIVATE
).edit().
322 remove(host
+ "_id").
323 remove(host
+ "_secret").
326 sContext
.getPreferences(Activity
.MODE_PRIVATE
).edit().
327 putString(host
+ "_id", id
).
328 putString(host
+ "_secret", secret
).
334 * Moves the mouse cursor, possibly while clicking the specified (nonnegative) button. Called
337 public static void sendMouseEvent(int x
, int y
, int whichButton
, boolean buttonDown
) {
342 nativeSendMouseEvent(x
, y
, whichButton
, buttonDown
);
345 /** Passes mouse information to the native handling code. */
346 private static native void nativeSendMouseEvent(int x
, int y
, int whichButton
,
349 /** Injects a mouse-wheel event with delta values. Called on the UI thread. */
350 public static void sendMouseWheelEvent(int deltaX
, int deltaY
) {
355 nativeSendMouseWheelEvent(deltaX
, deltaY
);
358 /** Passes mouse-wheel information to the native handling code. */
359 private static native void nativeSendMouseWheelEvent(int deltaX
, int deltaY
);
361 /** Presses or releases the specified (nonnegative) key. Called on the UI thread. */
362 public static boolean sendKeyEvent(int keyCode
, boolean keyDown
) {
367 return nativeSendKeyEvent(keyCode
, keyDown
);
370 /** Passes key press information to the native handling code. */
371 private static native boolean nativeSendKeyEvent(int keyCode
, boolean keyDown
);
373 /** Sends TextEvent to the host. Called on the UI thread. */
374 public static void sendTextEvent(String text
) {
379 nativeSendTextEvent(text
);
382 /** Passes text event information to the native handling code. */
383 private static native void nativeSendTextEvent(String text
);
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
390 public static void provideRedrawCallback(Runnable redrawCallback
) {
391 sRedrawCallback
= redrawCallback
;
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;
398 nativeScheduleRedraw();
402 /** Schedules a redraw on the native graphics thread. */
403 private static native void nativeScheduleRedraw();
406 * Performs the redrawing callback. This is a no-op if the window isn't visible. Called on the
410 private static void redrawGraphicsInternal() {
411 Runnable callback
= sRedrawCallback
;
412 if (callback
!= null) {
418 * Returns a bitmap of the latest video frame. Called on the native graphics thread when
419 * DesktopView is repainted.
421 public static Bitmap
getVideoFrame() {
422 if (Looper
.myLooper() == Looper
.getMainLooper()) {
423 Log
.w("jniiface", "Canvas being redrawn on UI thread");
426 synchronized (sFrameLock
) {
432 * Sets a new video frame. Called on the native graphics thread when a new frame is allocated.
435 private static void setVideoFrame(Bitmap bitmap
) {
436 if (Looper
.myLooper() == Looper
.getMainLooper()) {
437 Log
.w("jniiface", "Video frame updated on UI thread");
440 synchronized (sFrameLock
) {
441 sFrameBitmap
= bitmap
;
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.
450 private static Bitmap
newBitmap(int width
, int height
) {
451 return Bitmap
.createBitmap(width
, height
, Bitmap
.Config
.ARGB_8888
);
455 * Updates the cursor shape. This is called on the graphics thread when receiving a new cursor
456 * shape from the host.
459 public static void updateCursorShape(int width
, int height
, int hotspotX
, int hotspotY
,
461 sCursorHotspot
= new Point(hotspotX
, hotspotY
);
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
);
469 /** Position of cursor hotspot within cursor image. Called on the graphics thread. */
470 public static Point
getCursorHotspot() { return sCursorHotspot
; }
472 /** Returns the current cursor shape. Called on the graphics thread. */
473 public static Bitmap
getCursorBitmap() { return sCursorBitmap
; }
476 // Third Party Authentication
479 /** Pops up a third party login page to fetch the token required for authentication. */
481 public static void fetchThirdPartyToken(String tokenUrl
, String clientId
, String scope
) {
482 Chromoting app
= (Chromoting
) sContext
;
483 app
.fetchThirdPartyToken(tokenUrl
, clientId
, scope
);
487 * Notify the native code to continue authentication with the |token| and the |sharedSecret|.
489 public static void onThirdPartyTokenFetched(String token
, String sharedSecret
) {
494 nativeOnThirdPartyTokenFetched(token
, sharedSecret
);
497 /** Passes authentication data to the native handling code. */
498 private static native void nativeOnThirdPartyTokenFetched(String token
, String sharedSecret
);
501 // Host and Client Capabilities
504 /** Set the list of negotiated capabilities between host and client. Called on the UI thread. */
506 public static void setCapabilities(String capabilities
) {
507 sCapabilityManager
.setNegotiatedCapabilities(capabilities
);
511 // Extension Message Handling
514 /** Passes on the deconstructed ExtensionMessage to the app. Called on the UI thread. */
516 public static void handleExtensionMessage(String type
, String data
) {
517 sCapabilityManager
.onExtensionMessage(type
, data
);
520 /** Sends an extension message to the Chromoting host. Called on the UI thread. */
521 public static void sendExtensionMessage(String type
, String data
) {
526 nativeSendExtensionMessage(type
, data
);
529 private static native void nativeSendExtensionMessage(String type
, String data
);