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.app.ProgressDialog; |
10 | import android.content.Context; |
11 | import android.content.DialogInterface; |
12 | import android.content.SharedPreferences; |
13 | import android.graphics.Bitmap; |
14 | import android.os.Looper; |
15 | import android.text.InputType; |
16 | import android.util.Log; |
17 | import android.view.KeyEvent; |
18 | import android.view.View; |
19 | import android.view.inputmethod.EditorInfo; |
20 | import android.widget.CheckBox; |
21 | import android.widget.TextView; |
22 | import android.widget.Toast; |
23 | |
24 | import org.chromium.chromoting.R; |
25 | |
26 | import java.nio.ByteBuffer; |
27 | import 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 | */ |
33 | public 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(boolean pairingSupported) { |
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 | final TextView pinTextView = (TextView)pinEntry.findViewById(R.id.pin_dialog_text); |
199 | final CheckBox pinCheckBox = (CheckBox)pinEntry.findViewById(R.id.pin_dialog_check); |
200 | |
201 | if (!pairingSupported) { |
202 | pinCheckBox.setChecked(false); |
203 | pinCheckBox.setVisibility(View.GONE); |
204 | } |
205 | |
206 | pinPrompt.setPositiveButton( |
207 | R.string.pin_entry_connect, new DialogInterface.OnClickListener() { |
208 | @Override |
209 | public void onClick(DialogInterface dialog, int which) { |
210 | Log.i("jniiface", "User provided a PIN code"); |
211 | authenticationResponse(String.valueOf(pinTextView.getText()), |
212 | pinCheckBox.isChecked()); |
213 | } |
214 | }); |
215 | |
216 | pinPrompt.setNegativeButton( |
217 | R.string.pin_entry_cancel, new DialogInterface.OnClickListener() { |
218 | @Override |
219 | public void onClick(DialogInterface dialog, int which) { |
220 | Log.i("jniiface", "User canceled pin entry prompt"); |
221 | Toast.makeText(sContext, |
222 | sContext.getString(R.string.msg_pin_canceled), |
223 | Toast.LENGTH_LONG).show(); |
224 | disconnectFromHost(); |
225 | } |
226 | }); |
227 | |
228 | final AlertDialog pinDialog = pinPrompt.create(); |
229 | |
230 | pinTextView.setOnEditorActionListener( |
231 | new TextView.OnEditorActionListener() { |
232 | @Override |
233 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { |
234 | // The user pressed enter on the keypad (equivalent to the connect button). |
235 | pinDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick(); |
236 | pinDialog.dismiss(); |
237 | return true; |
238 | } |
239 | }); |
240 | |
241 | pinDialog.setOnCancelListener( |
242 | new DialogInterface.OnCancelListener() { |
243 | @Override |
244 | public void onCancel(DialogInterface dialog) { |
245 | // The user backed out of the dialog (equivalent to the cancel button). |
246 | pinDialog.getButton(AlertDialog.BUTTON_NEGATIVE).performClick(); |
247 | } |
248 | }); |
249 | |
250 | pinDialog.show(); |
251 | } |
252 | |
253 | /** Saves newly-received pairing credentials to permanent storage. */ |
254 | private static void commitPairingCredentials(String host, byte[] id, byte[] secret) { |
255 | synchronized (sContext) { |
256 | sContext.getPreferences(Activity.MODE_PRIVATE).edit(). |
257 | putString(host + "_id", new String(id)). |
258 | putString(host + "_secret", new String(secret)). |
259 | apply(); |
260 | } |
261 | } |
262 | |
263 | /** |
264 | * Sets the redraw callback to the provided functor. Provide a value of null whenever the |
265 | * window is no longer visible so that we don't continue to draw onto it. |
266 | */ |
267 | public static void provideRedrawCallback(Runnable redrawCallback) { |
268 | sRedrawCallback = redrawCallback; |
269 | } |
270 | |
271 | /** Forces the native graphics thread to redraw to the canvas. */ |
272 | public static boolean redrawGraphics() { |
273 | synchronized(JniInterface.class) { |
274 | if (!sConnected || sRedrawCallback == null) return false; |
275 | } |
276 | |
277 | scheduleRedrawNative(); |
278 | return true; |
279 | } |
280 | |
281 | /** Performs the redrawing callback. This is a no-op if the window isn't visible. */ |
282 | private static void redrawGraphicsInternal() { |
283 | if (sRedrawCallback != null) |
284 | sRedrawCallback.run(); |
285 | } |
286 | |
287 | /** |
288 | * Obtains the image buffer. |
289 | * This should not be called from the UI thread. (We prefer the native graphics thread.) |
290 | */ |
291 | public static Bitmap retrieveVideoFrame() { |
292 | if (Looper.myLooper() == Looper.getMainLooper()) { |
293 | Log.w("jniiface", "Canvas being redrawn on UI thread"); |
294 | } |
295 | |
296 | if (!sConnected) { |
297 | return null; |
298 | } |
299 | |
300 | int[] frame = new int[sWidth * sHeight]; |
301 | |
302 | sBuffer.order(ByteOrder.LITTLE_ENDIAN); |
303 | sBuffer.asIntBuffer().get(frame, 0, frame.length); |
304 | |
305 | return Bitmap.createBitmap(frame, 0, sWidth, sWidth, sHeight, Bitmap.Config.ARGB_8888); |
306 | } |
307 | |
308 | /** Moves the mouse cursor, possibly while clicking the specified (nonnegative) button. */ |
309 | public static void mouseAction(int x, int y, int whichButton, boolean buttonDown) { |
310 | if (!sConnected) { |
311 | return; |
312 | } |
313 | |
314 | mouseActionNative(x, y, whichButton, buttonDown); |
315 | } |
316 | |
317 | /** Presses and releases the specified (nonnegative) key. */ |
318 | public static void keyboardAction(int keyCode, boolean keyDown) { |
319 | if (!sConnected) { |
320 | return; |
321 | } |
322 | |
323 | keyboardActionNative(keyCode, keyDown); |
324 | } |
325 | |
326 | /** Performs the native response to the user's PIN. */ |
327 | private static native void authenticationResponse(String pin, boolean createPair); |
328 | |
329 | /** Schedules a redraw on the native graphics thread. */ |
330 | private static native void scheduleRedrawNative(); |
331 | |
332 | /** Passes mouse information to the native handling code. */ |
333 | private static native void mouseActionNative(int x, int y, int whichButton, boolean buttonDown); |
334 | |
335 | /** Passes key press information to the native handling code. */ |
336 | private static native void keyboardActionNative(int keyCode, boolean keyDown); |
337 | } |