| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.chromoting.jni; |
| |
| import android.app.Activity; |
| import android.app.AlertDialog; |
| import android.app.ProgressDialog; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.SharedPreferences; |
| import android.graphics.Bitmap; |
| import android.os.Looper; |
| import android.text.InputType; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.View; |
| import android.view.inputmethod.EditorInfo; |
| import android.widget.CheckBox; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import org.chromium.chromoting.R; |
| |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| |
| /** |
| * Initializes the Chromium remoting library, and provides JNI calls into it. |
| * All interaction with the native code is centralized in this class. |
| */ |
| public class JniInterface { |
| /** The status code indicating successful connection. */ |
| private static final int SUCCESSFUL_CONNECTION = 3; |
| |
| /** The application context. */ |
| private static Activity sContext = null; |
| |
| /* |
| * Library-loading state machine. |
| */ |
| /** Whether we've already loaded the library. */ |
| private static boolean sLoaded = false; |
| |
| /** |
| * To be called once from the main Activity. Any subsequent calls will update the application |
| * context, but not reload the library. This is useful e.g. when the activity is closed and the |
| * user later wants to return to the application. |
| */ |
| public static void loadLibrary(Activity context) { |
| sContext = context; |
| |
| synchronized(JniInterface.class) { |
| if (sLoaded) return; |
| } |
| |
| System.loadLibrary("remoting_client_jni"); |
| loadNative(context); |
| sLoaded = true; |
| } |
| |
| /** Performs the native portion of the initialization. */ |
| private static native void loadNative(Context context); |
| |
| /* |
| * API/OAuth2 keys access. |
| */ |
| public static native String getApiKey(); |
| public static native String getClientId(); |
| public static native String getClientSecret(); |
| |
| /* |
| * Connection-initiating state machine. |
| */ |
| /** Whether the native code is attempting a connection. */ |
| private static boolean sConnected = false; |
| |
| /** Callback to signal upon successful connection. */ |
| private static Runnable sSuccessCallback = null; |
| |
| /** Dialog for reporting connection progress. */ |
| private static ProgressDialog sProgressIndicator = null; |
| |
| /** Attempts to form a connection to the user-selected host. */ |
| public static void connectToHost(String username, String authToken, |
| String hostJid, String hostId, String hostPubkey, Runnable successCallback) { |
| synchronized(JniInterface.class) { |
| if (!sLoaded) return; |
| |
| if (sConnected) { |
| disconnectFromHost(); |
| } |
| } |
| |
| sSuccessCallback = successCallback; |
| SharedPreferences prefs = sContext.getPreferences(Activity.MODE_PRIVATE); |
| connectNative(username, authToken, hostJid, hostId, hostPubkey, |
| prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", "")); |
| sConnected = true; |
| } |
| |
| /** Severs the connection and cleans up. */ |
| public static void disconnectFromHost() { |
| synchronized(JniInterface.class) { |
| if (!sLoaded || !sConnected) return; |
| |
| if (sProgressIndicator != null) { |
| sProgressIndicator.dismiss(); |
| sProgressIndicator = null; |
| } |
| } |
| |
| disconnectNative(); |
| sSuccessCallback = null; |
| sConnected = false; |
| } |
| |
| /** Performs the native portion of the connection. */ |
| private static native void connectNative(String username, String authToken, String hostJid, |
| String hostId, String hostPubkey, String pairId, String pairSecret); |
| |
| /** Performs the native portion of the cleanup. */ |
| private static native void disconnectNative(); |
| |
| /* |
| * Entry points *from* the native code. |
| */ |
| /** Callback to signal whenever we need to redraw. */ |
| private static Runnable sRedrawCallback = null; |
| |
| /** Screen width of the video feed. */ |
| private static int sWidth = 0; |
| |
| /** Screen height of the video feed. */ |
| private static int sHeight = 0; |
| |
| /** Buffer holding the video feed. */ |
| private static ByteBuffer sBuffer = null; |
| |
| /** Reports whenever the connection status changes. */ |
| private static void reportConnectionStatus(int state, int error) { |
| if (state < SUCCESSFUL_CONNECTION && error == 0) { |
| // The connection is still being established, so we'll report the current progress. |
| synchronized (JniInterface.class) { |
| if (sProgressIndicator == null) { |
| sProgressIndicator = ProgressDialog.show(sContext, sContext. |
| getString(R.string.progress_title), sContext.getResources(). |
| getStringArray(R.array.protoc_states)[state], true, true, |
| new DialogInterface.OnCancelListener() { |
| @Override |
| public void onCancel(DialogInterface dialog) { |
| Log.i("jniiface", "User canceled connection initiation"); |
| disconnectFromHost(); |
| } |
| }); |
| } |
| else { |
| sProgressIndicator.setMessage( |
| sContext.getResources().getStringArray(R.array.protoc_states)[state]); |
| } |
| } |
| } |
| else { |
| // The connection is complete or has failed, so we can lose the progress indicator. |
| synchronized (JniInterface.class) { |
| if (sProgressIndicator != null) { |
| sProgressIndicator.dismiss(); |
| sProgressIndicator = null; |
| } |
| } |
| |
| if (state == SUCCESSFUL_CONNECTION) { |
| Toast.makeText(sContext, sContext.getResources(). |
| getStringArray(R.array.protoc_states)[state], Toast.LENGTH_SHORT).show(); |
| |
| // Actually display the remote desktop. |
| sSuccessCallback.run(); |
| } else { |
| Toast.makeText(sContext, sContext.getResources().getStringArray( |
| R.array.protoc_states)[state] + (error == 0 ? "" : ": " + |
| sContext.getResources().getStringArray(R.array.protoc_errors)[error]), |
| Toast.LENGTH_LONG).show(); |
| } |
| } |
| } |
| |
| /** Prompts the user to enter a PIN. */ |
| private static void displayAuthenticationPrompt() { |
| AlertDialog.Builder pinPrompt = new AlertDialog.Builder(sContext); |
| pinPrompt.setTitle(sContext.getString(R.string.pin_entry_title)); |
| pinPrompt.setMessage(sContext.getString(R.string.pin_entry_message)); |
| pinPrompt.setIcon(android.R.drawable.ic_lock_lock); |
| |
| final View pinEntry = sContext.getLayoutInflater().inflate(R.layout.pin_dialog, null); |
| pinPrompt.setView(pinEntry); |
| |
| pinPrompt.setPositiveButton( |
| R.string.pin_entry_connect, new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| Log.i("jniiface", "User provided a PIN code"); |
| authenticationResponse(String.valueOf( |
| ((TextView) |
| pinEntry.findViewById(R.id.pin_dialog_text)).getText()), |
| ((CheckBox) |
| pinEntry.findViewById(R.id.pin_dialog_check)).isChecked()); |
| } |
| }); |
| |
| pinPrompt.setNegativeButton( |
| R.string.pin_entry_cancel, new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| Log.i("jniiface", "User canceled pin entry prompt"); |
| Toast.makeText(sContext, |
| sContext.getString(R.string.msg_pin_canceled), |
| Toast.LENGTH_LONG).show(); |
| disconnectFromHost(); |
| } |
| }); |
| |
| final AlertDialog pinDialog = pinPrompt.create(); |
| |
| ((TextView)pinEntry.findViewById(R.id.pin_dialog_text)).setOnEditorActionListener( |
| new TextView.OnEditorActionListener() { |
| @Override |
| public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { |
| // The user pressed enter on the keypad (equivalent to the connect button). |
| pinDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); |
| pinDialog.dismiss(); |
| return true; |
| } |
| }); |
| |
| pinDialog.setOnCancelListener( |
| new DialogInterface.OnCancelListener() { |
| @Override |
| public void onCancel(DialogInterface dialog) { |
| // The user backed out of the dialog (equivalent to the cancel button). |
| pinDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick(); |
| } |
| }); |
| |
| pinDialog.show(); |
| } |
| |
| /** Saves newly-received pairing credentials to permanent storage. */ |
| private static void commitPairingCredentials(String host, byte[] id, byte[] secret) { |
| synchronized (sContext) { |
| sContext.getPreferences(Activity.MODE_PRIVATE).edit(). |
| putString(host + "_id", new String(id)). |
| putString(host + "_secret", new String(secret)). |
| apply(); |
| } |
| } |
| |
| /** |
| * Sets the redraw callback to the provided functor. Provide a value of null whenever the |
| * window is no longer visible so that we don't continue to draw onto it. |
| */ |
| public static void provideRedrawCallback(Runnable redrawCallback) { |
| sRedrawCallback = redrawCallback; |
| } |
| |
| /** Forces the native graphics thread to redraw to the canvas. */ |
| public static boolean redrawGraphics() { |
| synchronized(JniInterface.class) { |
| if (!sConnected || sRedrawCallback == null) return false; |
| } |
| |
| scheduleRedrawNative(); |
| return true; |
| } |
| |
| /** Performs the redrawing callback. This is a no-op if the window isn't visible. */ |
| private static void redrawGraphicsInternal() { |
| if (sRedrawCallback != null) |
| sRedrawCallback.run(); |
| } |
| |
| /** |
| * Obtains the image buffer. |
| * This should not be called from the UI thread. (We prefer the native graphics thread.) |
| */ |
| public static Bitmap retrieveVideoFrame() { |
| if (Looper.myLooper() == Looper.getMainLooper()) { |
| Log.w("jniiface", "Canvas being redrawn on UI thread"); |
| } |
| |
| if (!sConnected) { |
| return null; |
| } |
| |
| int[] frame = new int[sWidth * sHeight]; |
| |
| sBuffer.order(ByteOrder.LITTLE_ENDIAN); |
| sBuffer.asIntBuffer().get(frame, 0, frame.length); |
| |
| return Bitmap.createBitmap(frame, 0, sWidth, sWidth, sHeight, Bitmap.Config.ARGB_8888); |
| } |
| |
| /** Moves the mouse cursor, possibly while clicking the specified (nonnegative) button. */ |
| public static void mouseAction(int x, int y, int whichButton, boolean buttonDown) { |
| if (!sConnected) { |
| return; |
| } |
| |
| mouseActionNative(x, y, whichButton, buttonDown); |
| } |
| |
| /** Presses and releases the specified (nonnegative) key. */ |
| public static void keyboardAction(int keyCode, boolean keyDown) { |
| if (!sConnected) { |
| return; |
| } |
| |
| keyboardActionNative(keyCode, keyDown); |
| } |
| |
| /** Performs the native response to the user's PIN. */ |
| private static native void authenticationResponse(String pin, boolean createPair); |
| |
| /** Schedules a redraw on the native graphics thread. */ |
| private static native void scheduleRedrawNative(); |
| |
| /** Passes mouse information to the native handling code. */ |
| private static native void mouseActionNative(int x, int y, int whichButton, boolean buttonDown); |
| |
| /** Passes key press information to the native handling code. */ |
| private static native void keyboardActionNative(int keyCode, boolean keyDown); |
| } |