Home | History | Annotate | Download | only in jni
      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