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 import android.widget.Toast;
     22 
     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;
     28 
     29 import java.nio.ByteBuffer;
     30 import java.nio.ByteOrder;
     31 
     32 /**
     33  * Initializes the Chromium remoting library, and provides JNI calls into it.
     34  * All interaction with the native code is centralized in this class.
     35  */
     36 @JNINamespace("remoting")
     37 public class JniInterface {
     38     /*
     39      * Library-loading state machine.
     40      */
     41     /** Whether the library has been loaded. Accessed on the UI thread. */
     42     private static boolean sLoaded = false;
     43 
     44     /** The application context. Accessed on the UI thread. */
     45     private static Activity sContext = null;
     46 
     47     /** Interface used for connection state notifications. */
     48     public interface ConnectionListener {
     49         /**
     50          * This enum must match the C++ enumeration remoting::protocol::ConnectionToHost::State.
     51          */
     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