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