1002e8696020f084ce9425ff833099e9346454f2dJohnny Chen// Copyright 2013 The Chromium Authors. All rights reserved.
2002e8696020f084ce9425ff833099e9346454f2dJohnny Chen// Use of this source code is governed by a BSD-style license that can be
3002e8696020f084ce9425ff833099e9346454f2dJohnny Chen// found in the LICENSE file.
4002e8696020f084ce9425ff833099e9346454f2dJohnny Chen
5002e8696020f084ce9425ff833099e9346454f2dJohnny Chenpackage org.chromium.chromoting.jni;
6002e8696020f084ce9425ff833099e9346454f2dJohnny Chen
7002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.app.Activity;
8002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.app.AlertDialog;
9002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.content.Context;
10002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.content.DialogInterface;
11002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.content.SharedPreferences;
12002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.graphics.Bitmap;
13002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.graphics.Point;
14002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.os.Build;
15002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.os.Looper;
16002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.util.Log;
17002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.view.KeyEvent;
18002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.view.View;
19002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.widget.CheckBox;
20002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.widget.TextView;
21002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport android.widget.Toast;
22002e8696020f084ce9425ff833099e9346454f2dJohnny Chen
23002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport org.chromium.base.CalledByNative;
24002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport org.chromium.base.JNINamespace;
25002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport org.chromium.chromoting.CapabilityManager;
26002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport org.chromium.chromoting.Chromoting;
27002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport org.chromium.chromoting.R;
28002e8696020f084ce9425ff833099e9346454f2dJohnny Chen
29002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport java.nio.ByteBuffer;
30002e8696020f084ce9425ff833099e9346454f2dJohnny Chenimport java.nio.ByteOrder;
31002e8696020f084ce9425ff833099e9346454f2dJohnny Chen
32002e8696020f084ce9425ff833099e9346454f2dJohnny Chen/**
33002e8696020f084ce9425ff833099e9346454f2dJohnny Chen * Initializes the Chromium remoting library, and provides JNI calls into it.
34002e8696020f084ce9425ff833099e9346454f2dJohnny Chen * All interaction with the native code is centralized in this class.
35002e8696020f084ce9425ff833099e9346454f2dJohnny Chen */
36002e8696020f084ce9425ff833099e9346454f2dJohnny Chen@JNINamespace("remoting")
37002e8696020f084ce9425ff833099e9346454f2dJohnny Chenpublic class JniInterface {
38002e8696020f084ce9425ff833099e9346454f2dJohnny Chen    /*
39002e8696020f084ce9425ff833099e9346454f2dJohnny Chen     * Library-loading state machine.
40002e8696020f084ce9425ff833099e9346454f2dJohnny Chen     */
41002e8696020f084ce9425ff833099e9346454f2dJohnny Chen    /** Whether the library has been loaded. Accessed on the UI thread. */
42002e8696020f084ce9425ff833099e9346454f2dJohnny Chen    private static boolean sLoaded = false;
43002e8696020f084ce9425ff833099e9346454f2dJohnny Chen
44002e8696020f084ce9425ff833099e9346454f2dJohnny Chen    /** The application context. Accessed on the UI thread. */
45002e8696020f084ce9425ff833099e9346454f2dJohnny Chen    private static Activity sContext = null;
46002e8696020f084ce9425ff833099e9346454f2dJohnny Chen
47002e8696020f084ce9425ff833099e9346454f2dJohnny Chen    /** Interface used for connection state notifications. */
48002e8696020f084ce9425ff833099e9346454f2dJohnny Chen    public interface ConnectionListener {
49002e8696020f084ce9425ff833099e9346454f2dJohnny Chen        /**
50002e8696020f084ce9425ff833099e9346454f2dJohnny Chen         * This enum must match the C++ enumeration remoting::protocol::ConnectionToHost::State.
51002e8696020f084ce9425ff833099e9346454f2dJohnny Chen         */
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