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 5package org.chromium.chromoting.jni; 6 7import android.app.Activity; 8import android.app.AlertDialog; 9import android.app.ProgressDialog; 10import android.content.Context; 11import android.content.DialogInterface; 12import android.content.SharedPreferences; 13import android.graphics.Bitmap; 14import android.os.Looper; 15import android.text.InputType; 16import android.util.Log; 17import android.view.KeyEvent; 18import android.view.View; 19import android.view.inputmethod.EditorInfo; 20import android.widget.CheckBox; 21import android.widget.TextView; 22import android.widget.Toast; 23 24import org.chromium.chromoting.R; 25 26import java.nio.ByteBuffer; 27import java.nio.ByteOrder; 28 29/** 30 * Initializes the Chromium remoting library, and provides JNI calls into it. 31 * All interaction with the native code is centralized in this class. 32 */ 33public class JniInterface { 34 /** The status code indicating successful connection. */ 35 private static final int SUCCESSFUL_CONNECTION = 3; 36 37 /** The application context. */ 38 private static Activity sContext = null; 39 40 /* 41 * Library-loading state machine. 42 */ 43 /** Whether we've already loaded the library. */ 44 private static boolean sLoaded = false; 45 46 /** 47 * To be called once from the main Activity. Any subsequent calls will update the application 48 * context, but not reload the library. This is useful e.g. when the activity is closed and the 49 * user later wants to return to the application. 50 */ 51 public static void loadLibrary(Activity context) { 52 sContext = context; 53 54 synchronized(JniInterface.class) { 55 if (sLoaded) return; 56 } 57 58 System.loadLibrary("remoting_client_jni"); 59 loadNative(context); 60 sLoaded = true; 61 } 62 63 /** Performs the native portion of the initialization. */ 64 private static native void loadNative(Context context); 65 66 /* 67 * API/OAuth2 keys access. 68 */ 69 public static native String getApiKey(); 70 public static native String getClientId(); 71 public static native String getClientSecret(); 72 73 /* 74 * Connection-initiating state machine. 75 */ 76 /** Whether the native code is attempting a connection. */ 77 private static boolean sConnected = false; 78 79 /** Callback to signal upon successful connection. */ 80 private static Runnable sSuccessCallback = null; 81 82 /** Dialog for reporting connection progress. */ 83 private static ProgressDialog sProgressIndicator = null; 84 85 /** Attempts to form a connection to the user-selected host. */ 86 public static void connectToHost(String username, String authToken, 87 String hostJid, String hostId, String hostPubkey, Runnable successCallback) { 88 synchronized(JniInterface.class) { 89 if (!sLoaded) return; 90 91 if (sConnected) { 92 disconnectFromHost(); 93 } 94 } 95 96 sSuccessCallback = successCallback; 97 SharedPreferences prefs = sContext.getPreferences(Activity.MODE_PRIVATE); 98 connectNative(username, authToken, hostJid, hostId, hostPubkey, 99 prefs.getString(hostId + "_id", ""), prefs.getString(hostId + "_secret", "")); 100 sConnected = true; 101 } 102 103 /** Severs the connection and cleans up. */ 104 public static void disconnectFromHost() { 105 synchronized(JniInterface.class) { 106 if (!sLoaded || !sConnected) return; 107 108 if (sProgressIndicator != null) { 109 sProgressIndicator.dismiss(); 110 sProgressIndicator = null; 111 } 112 } 113 114 disconnectNative(); 115 sSuccessCallback = null; 116 sConnected = false; 117 } 118 119 /** Performs the native portion of the connection. */ 120 private static native void connectNative(String username, String authToken, String hostJid, 121 String hostId, String hostPubkey, String pairId, String pairSecret); 122 123 /** Performs the native portion of the cleanup. */ 124 private static native void disconnectNative(); 125 126 /* 127 * Entry points *from* the native code. 128 */ 129 /** Callback to signal whenever we need to redraw. */ 130 private static Runnable sRedrawCallback = null; 131 132 /** Screen width of the video feed. */ 133 private static int sWidth = 0; 134 135 /** Screen height of the video feed. */ 136 private static int sHeight = 0; 137 138 /** Buffer holding the video feed. */ 139 private static ByteBuffer sBuffer = null; 140 141 /** Reports whenever the connection status changes. */ 142 private static void reportConnectionStatus(int state, int error) { 143 if (state < SUCCESSFUL_CONNECTION && error == 0) { 144 // The connection is still being established, so we'll report the current progress. 145 synchronized (JniInterface.class) { 146 if (sProgressIndicator == null) { 147 sProgressIndicator = ProgressDialog.show(sContext, sContext. 148 getString(R.string.progress_title), sContext.getResources(). 149 getStringArray(R.array.protoc_states)[state], true, true, 150 new DialogInterface.OnCancelListener() { 151 @Override 152 public void onCancel(DialogInterface dialog) { 153 Log.i("jniiface", "User canceled connection initiation"); 154 disconnectFromHost(); 155 } 156 }); 157 } 158 else { 159 sProgressIndicator.setMessage( 160 sContext.getResources().getStringArray(R.array.protoc_states)[state]); 161 } 162 } 163 } 164 else { 165 // The connection is complete or has failed, so we can lose the progress indicator. 166 synchronized (JniInterface.class) { 167 if (sProgressIndicator != null) { 168 sProgressIndicator.dismiss(); 169 sProgressIndicator = null; 170 } 171 } 172 173 if (state == SUCCESSFUL_CONNECTION) { 174 Toast.makeText(sContext, sContext.getResources(). 175 getStringArray(R.array.protoc_states)[state], Toast.LENGTH_SHORT).show(); 176 177 // Actually display the remote desktop. 178 sSuccessCallback.run(); 179 } else { 180 Toast.makeText(sContext, sContext.getResources().getStringArray( 181 R.array.protoc_states)[state] + (error == 0 ? "" : ": " + 182 sContext.getResources().getStringArray(R.array.protoc_errors)[error]), 183 Toast.LENGTH_LONG).show(); 184 } 185 } 186 } 187 188 /** Prompts the user to enter a PIN. */ 189 private static void displayAuthenticationPrompt() { 190 AlertDialog.Builder pinPrompt = new AlertDialog.Builder(sContext); 191 pinPrompt.setTitle(sContext.getString(R.string.pin_entry_title)); 192 pinPrompt.setMessage(sContext.getString(R.string.pin_entry_message)); 193 pinPrompt.setIcon(android.R.drawable.ic_lock_lock); 194 195 final View pinEntry = sContext.getLayoutInflater().inflate(R.layout.pin_dialog, null); 196 pinPrompt.setView(pinEntry); 197 198 pinPrompt.setPositiveButton( 199 R.string.pin_entry_connect, new DialogInterface.OnClickListener() { 200 @Override 201 public void onClick(DialogInterface dialog, int which) { 202 Log.i("jniiface", "User provided a PIN code"); 203 authenticationResponse(String.valueOf( 204 ((TextView) 205 pinEntry.findViewById(R.id.pin_dialog_text)).getText()), 206 ((CheckBox) 207 pinEntry.findViewById(R.id.pin_dialog_check)).isChecked()); 208 } 209 }); 210 211 pinPrompt.setNegativeButton( 212 R.string.pin_entry_cancel, new DialogInterface.OnClickListener() { 213 @Override 214 public void onClick(DialogInterface dialog, int which) { 215 Log.i("jniiface", "User canceled pin entry prompt"); 216 Toast.makeText(sContext, 217 sContext.getString(R.string.msg_pin_canceled), 218 Toast.LENGTH_LONG).show(); 219 disconnectFromHost(); 220 } 221 }); 222 223 final AlertDialog pinDialog = pinPrompt.create(); 224 225 ((TextView)pinEntry.findViewById(R.id.pin_dialog_text)).setOnEditorActionListener( 226 new TextView.OnEditorActionListener() { 227 @Override 228 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 229 // The user pressed enter on the keypad (equivalent to the connect button). 230 pinDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); 231 pinDialog.dismiss(); 232 return true; 233 } 234 }); 235 236 pinDialog.setOnCancelListener( 237 new DialogInterface.OnCancelListener() { 238 @Override 239 public void onCancel(DialogInterface dialog) { 240 // The user backed out of the dialog (equivalent to the cancel button). 241 pinDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick(); 242 } 243 }); 244 245 pinDialog.show(); 246 } 247 248 /** Saves newly-received pairing credentials to permanent storage. */ 249 private static void commitPairingCredentials(String host, byte[] id, byte[] secret) { 250 synchronized (sContext) { 251 sContext.getPreferences(Activity.MODE_PRIVATE).edit(). 252 putString(host + "_id", new String(id)). 253 putString(host + "_secret", new String(secret)). 254 apply(); 255 } 256 } 257 258 /** 259 * Sets the redraw callback to the provided functor. Provide a value of null whenever the 260 * window is no longer visible so that we don't continue to draw onto it. 261 */ 262 public static void provideRedrawCallback(Runnable redrawCallback) { 263 sRedrawCallback = redrawCallback; 264 } 265 266 /** Forces the native graphics thread to redraw to the canvas. */ 267 public static boolean redrawGraphics() { 268 synchronized(JniInterface.class) { 269 if (!sConnected || sRedrawCallback == null) return false; 270 } 271 272 scheduleRedrawNative(); 273 return true; 274 } 275 276 /** Performs the redrawing callback. This is a no-op if the window isn't visible. */ 277 private static void redrawGraphicsInternal() { 278 if (sRedrawCallback != null) 279 sRedrawCallback.run(); 280 } 281 282 /** 283 * Obtains the image buffer. 284 * This should not be called from the UI thread. (We prefer the native graphics thread.) 285 */ 286 public static Bitmap retrieveVideoFrame() { 287 if (Looper.myLooper() == Looper.getMainLooper()) { 288 Log.w("jniiface", "Canvas being redrawn on UI thread"); 289 } 290 291 if (!sConnected) { 292 return null; 293 } 294 295 int[] frame = new int[sWidth * sHeight]; 296 297 sBuffer.order(ByteOrder.LITTLE_ENDIAN); 298 sBuffer.asIntBuffer().get(frame, 0, frame.length); 299 300 return Bitmap.createBitmap(frame, 0, sWidth, sWidth, sHeight, Bitmap.Config.ARGB_8888); 301 } 302 303 /** Moves the mouse cursor, possibly while clicking the specified (nonnegative) button. */ 304 public static void mouseAction(int x, int y, int whichButton, boolean buttonDown) { 305 if (!sConnected) { 306 return; 307 } 308 309 mouseActionNative(x, y, whichButton, buttonDown); 310 } 311 312 /** Presses and releases the specified (nonnegative) key. */ 313 public static void keyboardAction(int keyCode, boolean keyDown) { 314 if (!sConnected) { 315 return; 316 } 317 318 keyboardActionNative(keyCode, keyDown); 319 } 320 321 /** Performs the native response to the user's PIN. */ 322 private static native void authenticationResponse(String pin, boolean createPair); 323 324 /** Schedules a redraw on the native graphics thread. */ 325 private static native void scheduleRedrawNative(); 326 327 /** Passes mouse information to the native handling code. */ 328 private static native void mouseActionNative(int x, int y, int whichButton, boolean buttonDown); 329 330 /** Passes key press information to the native handling code. */ 331 private static native void keyboardActionNative(int keyCode, boolean keyDown); 332} 333