blob: 039bcb90f31340c978f93a1187095ed305cd99bc [file] [log] [blame]
// 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);
}