cf2690b2d5485164a6ad9aa2477aae905f1e5941
[freeglut] / src / android / fg_main_android.c
1 /*
2  * fg_main_android.c
3  *
4  * The Android-specific windows message processing methods.
5  *
6  * Copyright (c) 1999-2000 Pawel W. Olszta. All Rights Reserved.
7  * Written by Pawel W. Olszta, <olszta@sourceforge.net>
8  * Copied for Platform code by Evan Felix <karcaw at gmail.com>
9  * Copyright (C) 2012  Sylvain Beucler
10  *
11  * Permission is hereby granted, free of charge, to any person obtaining a
12  * copy of this software and associated documentation files (the "Software"),
13  * to deal in the Software without restriction, including without limitation
14  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
15  * and/or sell copies of the Software, and to permit persons to whom the
16  * Software is furnished to do so, subject to the following conditions:
17  *
18  * The above copyright notice and this permission notice shall be included
19  * in all copies or substantial portions of the Software.
20  *
21  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22  * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
24  * PAWEL W. OLSZTA BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
25  * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
26  * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27  */
28
29 #include <GL/freeglut.h>
30 #include "fg_internal.h"
31 #include "fg_main.h"
32 #include "egl/fg_window_egl.h"
33
34 #include <android/log.h>
35 #define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "FreeGLUT", __VA_ARGS__))
36 #define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, "FreeGLUT", __VA_ARGS__))
37 #include <android/native_app_glue/android_native_app_glue.h>
38 #include <android/keycodes.h>
39
40 static struct touchscreen touchscreen;
41 static unsigned char key_a2fg[256];
42
43 /* Cf. http://developer.android.com/reference/android/view/KeyEvent.html */
44 /* These codes are missing in <android/keycodes.h> */
45 /* Don't convert to enum, since it may conflict with future version of
46    that <android/keycodes.h> */
47 #define AKEYCODE_FORWARD_DEL 112
48 #define AKEYCODE_CTRL_LEFT 113
49 #define AKEYCODE_CTRL_RIGHT 114
50 #define AKEYCODE_MOVE_HOME 122
51 #define AKEYCODE_MOVE_END 123
52 #define AKEYCODE_INSERT 124
53 #define AKEYCODE_ESCAPE 127
54 #define AKEYCODE_F1 131
55 #define AKEYCODE_F2 132
56 #define AKEYCODE_F3 133
57 #define AKEYCODE_F4 134
58 #define AKEYCODE_F5 135
59 #define AKEYCODE_F6 136
60 #define AKEYCODE_F7 137
61 #define AKEYCODE_F8 138
62 #define AKEYCODE_F9 139
63 #define AKEYCODE_F10 140
64 #define AKEYCODE_F11 141
65 #define AKEYCODE_F12 142
66
67 #define EVENT_HANDLED 1
68 #define EVENT_NOT_HANDLED 0
69
70 /**
71  * Initialize Android keycode to GLUT keycode mapping
72  */
73 static void key_init() {
74   memset(key_a2fg, 0, sizeof(key_a2fg));
75
76   key_a2fg[AKEYCODE_F1]  = GLUT_KEY_F1;
77   key_a2fg[AKEYCODE_F2]  = GLUT_KEY_F2;
78   key_a2fg[AKEYCODE_F3]  = GLUT_KEY_F3;
79   key_a2fg[AKEYCODE_F4]  = GLUT_KEY_F4;
80   key_a2fg[AKEYCODE_F5]  = GLUT_KEY_F5;
81   key_a2fg[AKEYCODE_F6]  = GLUT_KEY_F6;
82   key_a2fg[AKEYCODE_F7]  = GLUT_KEY_F7;
83   key_a2fg[AKEYCODE_F8]  = GLUT_KEY_F8;
84   key_a2fg[AKEYCODE_F9]  = GLUT_KEY_F9;
85   key_a2fg[AKEYCODE_F10] = GLUT_KEY_F10;
86   key_a2fg[AKEYCODE_F11] = GLUT_KEY_F11;
87   key_a2fg[AKEYCODE_F12] = GLUT_KEY_F12;
88
89   key_a2fg[AKEYCODE_PAGE_UP]   = GLUT_KEY_PAGE_UP;
90   key_a2fg[AKEYCODE_PAGE_DOWN] = GLUT_KEY_PAGE_DOWN;
91   key_a2fg[AKEYCODE_MOVE_HOME] = GLUT_KEY_HOME;
92   key_a2fg[AKEYCODE_MOVE_END]  = GLUT_KEY_END;
93   key_a2fg[AKEYCODE_INSERT]    = GLUT_KEY_INSERT;
94
95   key_a2fg[AKEYCODE_DPAD_UP]    = GLUT_KEY_UP;
96   key_a2fg[AKEYCODE_DPAD_DOWN]  = GLUT_KEY_DOWN;
97   key_a2fg[AKEYCODE_DPAD_LEFT]  = GLUT_KEY_LEFT;
98   key_a2fg[AKEYCODE_DPAD_RIGHT] = GLUT_KEY_RIGHT;
99
100   key_a2fg[AKEYCODE_ALT_LEFT]    = GLUT_KEY_ALT_L;
101   key_a2fg[AKEYCODE_ALT_RIGHT]   = GLUT_KEY_ALT_R;
102   key_a2fg[AKEYCODE_SHIFT_LEFT]  = GLUT_KEY_SHIFT_L;
103   key_a2fg[AKEYCODE_SHIFT_RIGHT] = GLUT_KEY_SHIFT_R;
104   key_a2fg[AKEYCODE_CTRL_LEFT]   = GLUT_KEY_CTRL_L;
105   key_a2fg[AKEYCODE_CTRL_RIGHT]  = GLUT_KEY_CTRL_R;
106 }
107
108 /**
109  * Convert an Android key event to ASCII.
110  */
111 static unsigned char key_ascii(struct android_app* app, AInputEvent* event) {
112   int32_t code = AKeyEvent_getKeyCode(event);
113
114   /* Handle a few special cases: */
115   switch (code) {
116   case AKEYCODE_DEL:
117     return 8;
118   case AKEYCODE_FORWARD_DEL:
119     return 127;
120   case AKEYCODE_ESCAPE:
121     return 27;
122   }
123
124   /* Get usable JNI context */
125   JNIEnv* env = app->activity->env;
126   JavaVM* vm = app->activity->vm;
127   (*vm)->AttachCurrentThread(vm, &env, NULL);
128
129   jclass KeyEventClass = (*env)->FindClass(env, "android/view/KeyEvent");
130   jmethodID KeyEventConstructor = (*env)->GetMethodID(env, KeyEventClass, "<init>", "(II)V");
131   jobject keyEvent = (*env)->NewObject(env, KeyEventClass, KeyEventConstructor,
132                                        AKeyEvent_getAction(event), AKeyEvent_getKeyCode(event));
133   jmethodID KeyEvent_getUnicodeChar = (*env)->GetMethodID(env, KeyEventClass, "getUnicodeChar", "(I)I");
134   int ascii = (*env)->CallIntMethod(env, keyEvent, KeyEvent_getUnicodeChar, AKeyEvent_getMetaState(event));
135
136   /* LOGI("getUnicodeChar(%d) = %d ('%c')", AKeyEvent_getKeyCode(event), ascii, ascii); */
137   (*vm)->DetachCurrentThread(vm);
138
139   return ascii;
140 }
141
142 /*
143  * Request a window resize
144  */
145 void fgPlatformReshapeWindow ( SFG_Window *window, int width, int height )
146 {
147   fprintf(stderr, "fgPlatformReshapeWindow: STUB\n");
148 }
149
150 /*
151  * A static helper function to execute display callback for a window
152  */
153 void fgPlatformDisplayWindow ( SFG_Window *window )
154 {
155   fghRedrawWindow ( window ) ;
156 }
157
158 unsigned long fgPlatformSystemTime ( void )
159 {
160   struct timeval now;
161   gettimeofday( &now, NULL );
162   return now.tv_usec/1000 + now.tv_sec*1000;
163 }
164
165 /*
166  * Does the magic required to relinquish the CPU until something interesting
167  * happens.
168  */
169 void fgPlatformSleepForEvents( long msec )
170 {
171     /* Android's NativeActivity relies on a Looper/ALooper object to
172        notify about events.  The Looper object is plugged on two
173        internal pipe(2)s to detect system and input events.  Sadly you
174        can only ask the Looper for an event, not just ask whether
175        there is a pending event (and process it later).  Consequently,
176        short of redesigning NativeActivity, we cannot
177        SleepForEvents. */
178 }
179
180 /**
181  * Process the next input event.
182  */
183 int32_t handle_input(struct android_app* app, AInputEvent* event) {
184   SFG_Window* window = fgWindowByHandle(app->window);
185   if (window == NULL)
186     return EVENT_NOT_HANDLED;
187
188   /* FIXME: in Android, when a key is repeated, down
189      and up events happen most often at the exact same time.  This
190      makes it impossible to animate based on key press time. */
191   /* e.g. down/up/wait/down/up rather than down/wait/down/wait/up */ 
192   /* This looks like a bug in the Android virtual keyboard system :/
193      Real buttons such as the Back button appear to work correctly
194      (series of down events with proper getRepeatCount value). */
195   
196   if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_KEY) {
197     /* LOGI("action: %d", AKeyEvent_getAction(event)); */
198     /* LOGI("keycode: %d", code); */
199     int32_t code = AKeyEvent_getKeyCode(event);
200
201     if (AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_DOWN) {
202       int32_t keypress = 0;
203       unsigned char ascii = 0;
204       if ((keypress = key_a2fg[code]) && FETCH_WCB(*window, Special)) {
205         INVOKE_WCB(*window, Special, (keypress, window->State.MouseX, window->State.MouseY));
206         return EVENT_HANDLED;
207       } else if ((ascii = key_ascii(app, event)) && FETCH_WCB(*window, Keyboard)) {
208         INVOKE_WCB(*window, Keyboard, (ascii, window->State.MouseX, window->State.MouseY));
209         return EVENT_HANDLED;
210       }
211     }
212     else if (AKeyEvent_getAction(event) == AKEY_EVENT_ACTION_UP) {
213       int32_t keypress = 0;
214       unsigned char ascii = 0;
215       if ((keypress = key_a2fg[code]) && FETCH_WCB(*window, Special)) {
216         INVOKE_WCB(*window, SpecialUp, (keypress, window->State.MouseX, window->State.MouseY));
217         return EVENT_HANDLED;
218       } else if ((ascii = key_ascii(app, event)) && FETCH_WCB(*window, Keyboard)) {
219         INVOKE_WCB(*window, KeyboardUp, (ascii, window->State.MouseX, window->State.MouseY));
220         return EVENT_HANDLED;
221       }
222     }
223   }
224
225   int32_t source = AInputEvent_getSource(event);
226   if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION
227       && source == AINPUT_SOURCE_TOUCHSCREEN) {
228     int32_t action = AMotionEvent_getAction(event) & AMOTION_EVENT_ACTION_MASK;
229     /* Pointer ID for clicks */
230     int32_t pidx = AMotionEvent_getAction(event) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
231     /* TODO: Handle multi-touch; also handle multiple sources/devices */
232     /* cf. http://sourceforge.net/mailarchive/forum.php?thread_name=20120518071314.GA28061%40perso.beuc.net&forum_name=freeglut-developer */
233     if (0) {
234       LOGI("motion action=%d index=%d source=%d", action, pidx, source);
235       int count = AMotionEvent_getPointerCount(event);
236       int i;
237       for (i = 0; i < count; i++) {
238         LOGI("multi(%d): %.01f,%.01f",
239              AMotionEvent_getPointerId(event, i),
240              AMotionEvent_getX(event, i), AMotionEvent_getY(event, i));
241       }
242     }
243     float x = AMotionEvent_getX(event, 0);
244     float y = AMotionEvent_getY(event, 0);
245
246     /* Virtual arrows PAD */
247     /* Don't interfere with existing mouse move event */
248     if (!touchscreen.in_mmotion) {
249       struct vpad_state prev_vpad = touchscreen.vpad;
250       touchscreen.vpad.left = touchscreen.vpad.right
251         = touchscreen.vpad.up = touchscreen.vpad.down = false;
252
253       /* int32_t width = ANativeWindow_getWidth(window->Window.Handle); */
254       int32_t height = ANativeWindow_getHeight(window->Window.Handle);
255       if (action == AMOTION_EVENT_ACTION_DOWN || action == AMOTION_EVENT_ACTION_MOVE) {
256         if ((x > 0 && x < 100) && (y > (height - 100) && y < height))
257           touchscreen.vpad.left = true;
258         if ((x > 200 && x < 300) && (y > (height - 100) && y < height))
259           touchscreen.vpad.right = true;
260         if ((x > 100 && x < 200) && (y > (height - 100) && y < height))
261           touchscreen.vpad.down = true;
262         if ((x > 100 && x < 200) && (y > (height - 200) && y < (height - 100)))
263           touchscreen.vpad.up = true;
264       }
265       if (action == AMOTION_EVENT_ACTION_DOWN && 
266           (touchscreen.vpad.left || touchscreen.vpad.right || touchscreen.vpad.down || touchscreen.vpad.up))
267         touchscreen.vpad.on = true;
268       if (action == AMOTION_EVENT_ACTION_UP)
269         touchscreen.vpad.on = false;
270       if (prev_vpad.left != touchscreen.vpad.left
271           || prev_vpad.right != touchscreen.vpad.right
272           || prev_vpad.up != touchscreen.vpad.up
273           || prev_vpad.down != touchscreen.vpad.down
274           || prev_vpad.on != touchscreen.vpad.on) {
275         if (FETCH_WCB(*window, Special)) {
276           if (prev_vpad.left == false && touchscreen.vpad.left == true)
277             INVOKE_WCB(*window, Special, (GLUT_KEY_LEFT, x, y));
278           else if (prev_vpad.right == false && touchscreen.vpad.right == true)
279             INVOKE_WCB(*window, Special, (GLUT_KEY_RIGHT, x, y));
280           else if (prev_vpad.up == false && touchscreen.vpad.up == true)
281             INVOKE_WCB(*window, Special, (GLUT_KEY_UP, x, y));
282           else if (prev_vpad.down == false && touchscreen.vpad.down == true)
283             INVOKE_WCB(*window, Special, (GLUT_KEY_DOWN, x, y));
284         }
285         if (FETCH_WCB(*window, SpecialUp)) {
286           if (prev_vpad.left == true && touchscreen.vpad.left == false)
287             INVOKE_WCB(*window, SpecialUp, (GLUT_KEY_LEFT, x, y));
288           if (prev_vpad.right == true && touchscreen.vpad.right == false)
289             INVOKE_WCB(*window, SpecialUp, (GLUT_KEY_RIGHT, x, y));
290           if (prev_vpad.up == true && touchscreen.vpad.up == false)
291             INVOKE_WCB(*window, SpecialUp, (GLUT_KEY_UP, x, y));
292           if (prev_vpad.down == true && touchscreen.vpad.down == false)
293             INVOKE_WCB(*window, SpecialUp, (GLUT_KEY_DOWN, x, y));
294         }
295         return EVENT_HANDLED;
296       }
297     }
298     
299     /* Normal mouse events */
300     if (!touchscreen.vpad.on) {
301       window->State.MouseX = x;
302       window->State.MouseY = y;
303       if (action == AMOTION_EVENT_ACTION_DOWN && FETCH_WCB(*window, Mouse)) {
304         touchscreen.in_mmotion = true;
305         INVOKE_WCB(*window, Mouse, (GLUT_LEFT_BUTTON, GLUT_DOWN, x, y));
306       } else if (action == AMOTION_EVENT_ACTION_UP && FETCH_WCB(*window, Mouse)) {
307         touchscreen.in_mmotion = false;
308         INVOKE_WCB(*window, Mouse, (GLUT_LEFT_BUTTON, GLUT_UP, x, y));
309       } else if (action == AMOTION_EVENT_ACTION_MOVE && FETCH_WCB(*window, Motion)) {
310         INVOKE_WCB(*window, Motion, (x, y));
311       }
312     }
313     
314     return EVENT_HANDLED;
315   }
316
317   /* Let Android handle other events (e.g. Back and Menu buttons) */
318   return EVENT_NOT_HANDLED;
319 }
320
321 /**
322  * Process the next main command.
323  */
324 void handle_cmd(struct android_app* app, int32_t cmd) {
325   SFG_Window* window = fgWindowByHandle(app->window);  /* may be NULL */
326   switch (cmd) {
327   /* App life cycle, in that order: */
328   case APP_CMD_START:
329     LOGI("handle_cmd: APP_CMD_START");
330     break;
331   case APP_CMD_RESUME:
332     LOGI("handle_cmd: APP_CMD_RESUME");
333     /* Cf. fgPlatformProcessSingleEvent */
334     break;
335   case APP_CMD_INIT_WINDOW: /* surfaceCreated */
336     /* The window is being shown, get it ready. */
337     LOGI("handle_cmd: APP_CMD_INIT_WINDOW %p", app->window);
338     fgDisplay.pDisplay.single_native_window = app->window;
339     /* start|resume: glPlatformOpenWindow was waiting for Handle to be
340        defined and will now continue processing */
341     break;
342   case APP_CMD_GAINED_FOCUS:
343     LOGI("handle_cmd: APP_CMD_GAINED_FOCUS");
344     break;
345   case APP_CMD_WINDOW_RESIZED:
346     LOGI("handle_cmd: APP_CMD_WINDOW_RESIZED");
347     if (window->Window.pContext.egl.Surface != EGL_NO_SURFACE)
348       /* Make ProcessSingleEvent detect the new size, only available
349          after the next SwapBuffer */
350       glutPostRedisplay();
351     break;
352
353   case APP_CMD_SAVE_STATE: /* onSaveInstanceState */
354     /* The system has asked us to save our current state, when it
355        pauses the application without destroying it right after. */
356     app->savedState = strdup("Detect me as non-NULL on next android_main");
357     app->savedStateSize = strlen(app->savedState) + 1;
358     LOGI("handle_cmd: APP_CMD_SAVE_STATE");
359     break;
360   case APP_CMD_PAUSE:
361     LOGI("handle_cmd: APP_CMD_PAUSE");
362     /* Cf. fgPlatformProcessSingleEvent */
363     break;
364   case APP_CMD_LOST_FOCUS:
365     LOGI("handle_cmd: APP_CMD_LOST_FOCUS");
366     break;
367   case APP_CMD_TERM_WINDOW: /* surfaceDestroyed */
368     /* The application is being hidden, but may be restored */
369     LOGI("handle_cmd: APP_CMD_TERM_WINDOW");
370     fghPlatformCloseWindowEGL(window);
371     window->State.NeedToInitContext = GL_TRUE;
372     fgDisplay.pDisplay.single_native_window = NULL;
373     break;
374   case APP_CMD_STOP:
375     LOGI("handle_cmd: APP_CMD_STOP");
376     break;
377   case APP_CMD_DESTROY: /* Activity.onDestroy */
378     LOGI("handle_cmd: APP_CMD_DESTROY");
379     /* User closed the application for good, let's kill the window */
380     {
381       /* Can't use fgWindowByHandle as app->window is NULL */
382       SFG_Window* window = fgStructure.CurrentWindow;
383       if (window != NULL) {
384         fgDestroyWindow(window);
385       } else {
386         LOGI("APP_CMD_DESTROY: No current window");
387       }
388     }
389     /* glue has already set android_app->destroyRequested=1 */
390     break;
391
392   case APP_CMD_CONFIG_CHANGED:
393     /* Handle rotation / orientation change */
394     LOGI("handle_cmd: APP_CMD_CONFIG_CHANGED");
395     break;
396   case APP_CMD_LOW_MEMORY:
397     LOGI("handle_cmd: APP_CMD_LOW_MEMORY");
398     break;
399   default:
400     LOGI("handle_cmd: unhandled cmd=%d", cmd);
401   }
402 }
403
404 void fgPlatformOpenWindow( SFG_Window* window, const char* title,
405                            GLboolean positionUse, int x, int y,
406                            GLboolean sizeUse, int w, int h,
407                            GLboolean gameMode, GLboolean isSubWindow );
408
409 void fgPlatformProcessSingleEvent ( void )
410 {
411   /* When the screen is resized, the window handle still points to the
412      old window until the next SwapBuffer, while it's crucial to set
413      the size (onShape) correctly before the next onDisplay callback.
414      Plus we don't know if the next SwapBuffer already occurred at the
415      time we process the event (e.g. during onDisplay). */
416   /* So we do the check each time rather than on event. */
417   /* Interestingly, on a Samsung Galaxy S/PowerVR SGX540 GPU/Android
418      2.3, that next SwapBuffer is fake (but still necessary to get the
419      new size). */
420   SFG_Window* window = fgStructure.CurrentWindow;
421   if (window != NULL && window->Window.Handle != NULL) {
422     int32_t width = ANativeWindow_getWidth(window->Window.Handle);
423     int32_t height = ANativeWindow_getHeight(window->Window.Handle);
424     if (width != window->State.pWState.LastWidth || height != window->State.pWState.LastHeight) {
425       window->State.pWState.LastWidth = width;
426       window->State.pWState.LastHeight = height;
427       LOGI("width=%d, height=%d", width, height);
428       if( FETCH_WCB( *window, Reshape ) )
429         INVOKE_WCB( *window, Reshape, ( width, height ) );
430       else
431         glViewport( 0, 0, width, height );
432       glutPostRedisplay();
433     }
434   }
435
436   /* Read pending event. */
437   int ident;
438   int events;
439   struct android_poll_source* source;
440   /* This is called "ProcessSingleEvent" but this means we'd only
441      process ~60 (screen Hz) mouse events per second, plus other ports
442      are processing all events already.  So let's process all pending
443      events. */
444   /* if ((ident=ALooper_pollOnce(0, NULL, &events, (void**)&source)) >= 0) { */
445   while ((ident=ALooper_pollAll(0, NULL, &events, (void**)&source)) >= 0) {
446     /* Process this event. */
447     if (source != NULL) {
448       source->process(source->app, source);
449     }
450   }
451
452   /* If we're not in RESUME state, Android paused us, so wait */
453   struct android_app* app = fgDisplay.pDisplay.app;
454   if (app->destroyRequested != 1 && app->activityState != APP_CMD_RESUME) {
455       INVOKE_WCB(*window, Pause, ());
456
457     int FOREVER = -1;
458     while (app->destroyRequested != 1 && (app->activityState != APP_CMD_RESUME)) {
459       if ((ident=ALooper_pollOnce(FOREVER, NULL, &events, (void**)&source)) >= 0) {
460         /* Process this event. */
461         if (source != NULL) {
462           source->process(source->app, source);
463         }
464       }
465     }
466     /* Coming back from a pause: */
467     /* - Recreate window context and surface */
468     /* - Call user-defined hook to restore resources (textures...) */
469     /* - Exit pause looop */
470     if (app->destroyRequested != 1) {
471       /* Android is full-screen only, simplified call.. */
472       /* Ideally we'd have a fgPlatformReopenWindow() */
473       /* If we're hidden by a non-fullscreen or translucent activity,
474          we'll be paused but not stopped, and keep the current
475          surface; in which case fgPlatformOpenWindow will no-op. */
476       fgPlatformOpenWindow(window, "", GL_FALSE, 0, 0, GL_FALSE, 0, 0, GL_FALSE, GL_FALSE);
477
478       if (!FETCH_WCB(*window, InitContext))
479           fgWarning("Resuming application, but no callback to reload context resources (glutInitContextFunc)");
480     }
481
482     INVOKE_WCB(*window, Resume, ());
483   }
484 }
485
486 void fgPlatformMainLoopPreliminaryWork ( void )
487 {
488   LOGI("fgPlatformMainLoopPreliminaryWork\n");
489
490   key_init();
491
492   /* Make sure glue isn't stripped. */
493   /* JNI entry points need to be bundled even when linking statically */
494   app_dummy();
495 }