37d3a212432b86d8d9f079c1a3876301aade7f4a
[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 */
232     if (0) {
233       LOGI("motion action=%d index=%d source=%d", action, pidx, source);
234       int count = AMotionEvent_getPointerCount(event);
235       int i;
236       for (i = 0; i < count; i++) {
237         LOGI("multi(%d): %.01f,%.01f",
238              AMotionEvent_getPointerId(event, i),
239              AMotionEvent_getX(event, i), AMotionEvent_getY(event, i));
240       }
241     }
242     float x = AMotionEvent_getX(event, 0);
243     float y = AMotionEvent_getY(event, 0);
244
245     /* Virtual arrows PAD */
246     /* Don't interfere with existing mouse move event */
247     if (!touchscreen.in_mmotion) {
248       struct vpad_state prev_vpad = touchscreen.vpad;
249       touchscreen.vpad.left = touchscreen.vpad.right
250         = touchscreen.vpad.up = touchscreen.vpad.down = false;
251
252       /* int32_t width = ANativeWindow_getWidth(window->Window.Handle); */
253       int32_t height = ANativeWindow_getHeight(window->Window.Handle);
254       if (action == AMOTION_EVENT_ACTION_DOWN || action == AMOTION_EVENT_ACTION_MOVE) {
255         if ((x > 0 && x < 100) && (y > (height - 100) && y < height))
256           touchscreen.vpad.left = true;
257         if ((x > 200 && x < 300) && (y > (height - 100) && y < height))
258           touchscreen.vpad.right = true;
259         if ((x > 100 && x < 200) && (y > (height - 100) && y < height))
260           touchscreen.vpad.down = true;
261         if ((x > 100 && x < 200) && (y > (height - 200) && y < (height - 100)))
262           touchscreen.vpad.up = true;
263       }
264       if (action == AMOTION_EVENT_ACTION_DOWN && 
265           (touchscreen.vpad.left || touchscreen.vpad.right || touchscreen.vpad.down || touchscreen.vpad.up))
266         touchscreen.vpad.on = true;
267       if (action == AMOTION_EVENT_ACTION_UP)
268         touchscreen.vpad.on = false;
269       if (prev_vpad.left != touchscreen.vpad.left
270           || prev_vpad.right != touchscreen.vpad.right
271           || prev_vpad.up != touchscreen.vpad.up
272           || prev_vpad.down != touchscreen.vpad.down
273           || prev_vpad.on != touchscreen.vpad.on) {
274         if (FETCH_WCB(*window, Special)) {
275           if (prev_vpad.left == false && touchscreen.vpad.left == true)
276             INVOKE_WCB(*window, Special, (GLUT_KEY_LEFT, x, y));
277           else if (prev_vpad.right == false && touchscreen.vpad.right == true)
278             INVOKE_WCB(*window, Special, (GLUT_KEY_RIGHT, x, y));
279           else if (prev_vpad.up == false && touchscreen.vpad.up == true)
280             INVOKE_WCB(*window, Special, (GLUT_KEY_UP, x, y));
281           else if (prev_vpad.down == false && touchscreen.vpad.down == true)
282             INVOKE_WCB(*window, Special, (GLUT_KEY_DOWN, x, y));
283         }
284         if (FETCH_WCB(*window, SpecialUp)) {
285           if (prev_vpad.left == true && touchscreen.vpad.left == false)
286             INVOKE_WCB(*window, SpecialUp, (GLUT_KEY_LEFT, x, y));
287           if (prev_vpad.right == true && touchscreen.vpad.right == false)
288             INVOKE_WCB(*window, SpecialUp, (GLUT_KEY_RIGHT, x, y));
289           if (prev_vpad.up == true && touchscreen.vpad.up == false)
290             INVOKE_WCB(*window, SpecialUp, (GLUT_KEY_UP, x, y));
291           if (prev_vpad.down == true && touchscreen.vpad.down == false)
292             INVOKE_WCB(*window, SpecialUp, (GLUT_KEY_DOWN, x, y));
293         }
294         return EVENT_HANDLED;
295       }
296     }
297     
298     /* Normal mouse events */
299     if (!touchscreen.vpad.on) {
300       window->State.MouseX = x;
301       window->State.MouseY = y;
302       if (action == AMOTION_EVENT_ACTION_DOWN && FETCH_WCB(*window, Mouse)) {
303         touchscreen.in_mmotion = true;
304         INVOKE_WCB(*window, Mouse, (GLUT_LEFT_BUTTON, GLUT_DOWN, x, y));
305       } else if (action == AMOTION_EVENT_ACTION_UP && FETCH_WCB(*window, Mouse)) {
306         touchscreen.in_mmotion = false;
307         INVOKE_WCB(*window, Mouse, (GLUT_LEFT_BUTTON, GLUT_UP, x, y));
308       } else if (action == AMOTION_EVENT_ACTION_MOVE && FETCH_WCB(*window, Motion)) {
309         INVOKE_WCB(*window, Motion, (x, y));
310       }
311     }
312     
313     return EVENT_HANDLED;
314   }
315
316   /* Let Android handle other events (e.g. Back and Menu buttons) */
317   return EVENT_NOT_HANDLED;
318 }
319
320 /**
321  * Process the next main command.
322  */
323 void handle_cmd(struct android_app* app, int32_t cmd) {
324   SFG_Window* window = fgWindowByHandle(app->window);  /* may be NULL */
325   switch (cmd) {
326   /* App life cycle, in that order: */
327   case APP_CMD_START:
328     LOGI("handle_cmd: APP_CMD_START");
329     break;
330   case APP_CMD_RESUME:
331     LOGI("handle_cmd: APP_CMD_RESUME");
332     /* Cf. fgPlatformProcessSingleEvent */
333     break;
334   case APP_CMD_INIT_WINDOW: /* surfaceCreated */
335     /* The window is being shown, get it ready. */
336     LOGI("handle_cmd: APP_CMD_INIT_WINDOW %p", app->window);
337     fgDisplay.pDisplay.single_native_window = app->window;
338     /* start|resume: glPlatformOpenWindow was waiting for Handle to be
339        defined and will now continue processing */
340     break;
341   case APP_CMD_GAINED_FOCUS:
342     LOGI("handle_cmd: APP_CMD_GAINED_FOCUS");
343     break;
344   case APP_CMD_WINDOW_RESIZED:
345     LOGI("handle_cmd: APP_CMD_WINDOW_RESIZED");
346     if (window->Window.pContext.egl.Surface != EGL_NO_SURFACE)
347       /* Make ProcessSingleEvent detect the new size, only available
348          after the next SwapBuffer */
349       glutPostRedisplay();
350     break;
351
352   case APP_CMD_SAVE_STATE: /* onSaveInstanceState */
353     /* The system has asked us to save our current state, when it
354        pauses the application without destroying it right after. */
355     app->savedState = strdup("Detect me as non-NULL on next android_main");
356     app->savedStateSize = strlen(app->savedState) + 1;
357     LOGI("handle_cmd: APP_CMD_SAVE_STATE");
358     break;
359   case APP_CMD_PAUSE:
360     LOGI("handle_cmd: APP_CMD_PAUSE");
361     /* Cf. fgPlatformProcessSingleEvent */
362     break;
363   case APP_CMD_LOST_FOCUS:
364     LOGI("handle_cmd: APP_CMD_LOST_FOCUS");
365     break;
366   case APP_CMD_TERM_WINDOW: /* surfaceDestroyed */
367     /* The application is being hidden, but may be restored */
368     LOGI("handle_cmd: APP_CMD_TERM_WINDOW");
369     fghPlatformCloseWindowEGL(window);
370     window->State.NeedToFixMyNameInitContext = GL_TRUE;
371     fgDisplay.pDisplay.single_native_window = NULL;
372     break;
373   case APP_CMD_STOP:
374     LOGI("handle_cmd: APP_CMD_STOP");
375     break;
376   case APP_CMD_DESTROY: /* Activity.onDestroy */
377     LOGI("handle_cmd: APP_CMD_DESTROY");
378     /* User closed the application for good, let's kill the window */
379     {
380       /* Can't use fgWindowByHandle as app->window is NULL */
381       SFG_Window* window = fgStructure.CurrentWindow;
382       if (window != NULL) {
383         fgDestroyWindow(window);
384       } else {
385         LOGI("APP_CMD_DESTROY: No current window");
386       }
387     }
388     /* glue has already set android_app->destroyRequested=1 */
389     break;
390
391   case APP_CMD_CONFIG_CHANGED:
392     /* Handle rotation / orientation change */
393     LOGI("handle_cmd: APP_CMD_CONFIG_CHANGED");
394     break;
395   case APP_CMD_LOW_MEMORY:
396     LOGI("handle_cmd: APP_CMD_LOW_MEMORY");
397     break;
398   default:
399     LOGI("handle_cmd: unhandled cmd=%d", cmd);
400   }
401 }
402
403 void fgPlatformOpenWindow( SFG_Window* window, const char* title,
404                            GLboolean positionUse, int x, int y,
405                            GLboolean sizeUse, int w, int h,
406                            GLboolean gameMode, GLboolean isSubWindow );
407
408 void fgPlatformProcessSingleEvent ( void )
409 {
410   /* When the screen is resized, the window handle still points to the
411      old window until the next SwapBuffer, while it's crucial to set
412      the size (onShape) correctly before the next onDisplay callback.
413      Plus we don't know if the next SwapBuffer already occurred at the
414      time we process the event (e.g. during onDisplay). */
415   /* So we do the check each time rather than on event. */
416   /* Interestingly, on a Samsung Galaxy S/PowerVR SGX540 GPU/Android
417      2.3, that next SwapBuffer is fake (but still necessary to get the
418      new size). */
419   SFG_Window* window = fgStructure.CurrentWindow;
420   if (window != NULL && window->Window.Handle != NULL) {
421     int32_t width = ANativeWindow_getWidth(window->Window.Handle);
422     int32_t height = ANativeWindow_getHeight(window->Window.Handle);
423     if (width != window->State.pWState.LastWidth || height != window->State.pWState.LastHeight) {
424       window->State.pWState.LastWidth = width;
425       window->State.pWState.LastHeight = height;
426       LOGI("width=%d, height=%d", width, height);
427       if( FETCH_WCB( *window, Reshape ) )
428         INVOKE_WCB( *window, Reshape, ( width, height ) );
429       else
430         glViewport( 0, 0, width, height );
431       glutPostRedisplay();
432     }
433   }
434
435   /* Read pending event. */
436   int ident;
437   int events;
438   struct android_poll_source* source;
439   /* This is called "ProcessSingleEvent" but this means we'd only
440      process ~60 (screen Hz) mouse events per second, plus other ports
441      are processing all events already.  So let's process all pending
442      events. */
443   /* if ((ident=ALooper_pollOnce(0, NULL, &events, (void**)&source)) >= 0) { */
444   while ((ident=ALooper_pollAll(0, NULL, &events, (void**)&source)) >= 0) {
445     /* Process this event. */
446     if (source != NULL) {
447       source->process(source->app, source);
448     }
449   }
450
451   /* If we're not in RESUME state, Android paused us, so wait */
452   struct android_app* app = fgDisplay.pDisplay.app;
453   if (app->destroyRequested != 1 && app->activityState != APP_CMD_RESUME) {
454     int FOREVER = -1;
455     while (app->destroyRequested != 1 && (app->activityState != APP_CMD_RESUME)) {
456       if ((ident=ALooper_pollOnce(FOREVER, NULL, &events, (void**)&source)) >= 0) {
457         /* Process this event. */
458         if (source != NULL) {
459           source->process(source->app, source);
460         }
461       }
462     }
463     /* Coming back from a pause: */
464     /* - Recreate window context and surface */
465     /* - Call user-defined hook to restore resources (textures...) */
466     /* - Exit pause looop */
467     if (app->destroyRequested != 1) {
468       /* Android is full-screen only, simplified call.. */
469       /* Ideally we'd have a fgPlatformReopenWindow() */
470       /* If we're hidden by a non-fullscreen or translucent activity,
471          we'll be paused but not stopped, and keep the current
472          surface; in which case fgPlatformOpenWindow will no-op. */
473       fgPlatformOpenWindow(window, "", GL_FALSE, 0, 0, GL_FALSE, 0, 0, GL_FALSE, GL_FALSE);
474       /* TODO: INVOKE_WCB(*window, Pause?); */
475       /* TODO: INVOKE_WCB(*window, Resume?); */
476       if (!FETCH_WCB(*window, FixMyNameInitContext)
477           fgWarning("Resuming application, but no callback to reload context resources (glutFixMyNameInitContextFunc)");
478     }
479   }
480 }
481
482 void fgPlatformMainLoopPreliminaryWork ( void )
483 {
484   LOGI("fgPlatformMainLoopPreliminaryWork\n");
485
486   key_init();
487
488   /* Make sure glue isn't stripped. */
489   /* JNI entry points need to be bundled even when linking statically */
490   app_dummy();
491 }