• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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