Android crash

Started by karl, February 22, 2019, 08:00:11 PM

Previous topic - Next topic
Hi,

I recently installed Solarus DX on my phone. The theory is great ! But as soon as I come in contact with the android 'home' button it crashes.
I get a nice message asking if I want to open the app again, but that's not quite the same thing.
* Exit should not be 'restart', but exit.
* switch away from solarus ( to answer the phone,e.g.) should work and not crash
* should not crash with other android buttons either
Is there an introduction to how to develop solarus on android ? This seems like such a simple thing ...

- Karl

Hi,

You must probably have found the old hacky android port of Solarus DX which is not done by us.
A new android player for solarus quests is in preparation. This will not be an app generator but a .solarus files player.

But the team is busy with other business for now. I don't really know when this android port will be ready. If you are interested it's  on gitlab :
https://gitlab.com/solarus-games/solarus-android

I would be happy to receive feedback on this one instead.

Thanks

greg

Ok, the new port also has some complications:

* For compilation, after a checkout of solarus-android and solarus, solarus has to be copied or linked to solarus-android/app/jni/solarus .
   ( this should be in the readme or readme.md ).
* For some reason, the android port isn't set up to use CMake in the solarus subdirectory. At the time I checked out, I had to make 2 changes to get a compile working:
   (1) in app/jni/Application.mk, add a line ( otherwise, the OpenGL symbols are all not linked )
         APP_LDFLAGS += -lGLESv2
   (2) in app/jni/solarus/Android.mk, add the glad directory in LOCAL_SRC_FILES:
            $(wildcard $(LOCAL_PATH)/src/third_party/glad/*.c) \
   Otherwise, the glad_ items are undefined.

The insidious thing is, 'build' in Android studio seems to work without this. Only when I try to run, more building and linking takes place, at which point the failure shows.
Now I can build and run. The quest seems to load. But it's all not enough. I can select and launch a quest. But  I can't get past the language selection screen... and exiting the quest from the quest launcher also doesn't seem to work.

- Karl

Quote
For compilation, after a checkout of solarus-android and solarus, solarus has to be copied or linked to solarus-android/app/jni/solarus .
   ( this should be in the readme or readme.md ).

No,
you simply forgot to checkout with --recursive option to fetch the solarus repository that is registered as a git submodule. This would have pointed you to
the right solarus commit in which the two other problems you had would not have existed.

I agree the --recursive option should be stated in the options tough.

Greg

Hi Greg - I didn't "simply forget" because I never knew that requirement in the first place.

Getting the quest to work means that touch events have to be translated into keys for hero actions and movements.
I decided that movement should be just moving your finger on the display, a tap brings up the menu ( i.e. generates 'd' ), and have some floating action buttons for the rest ( space,'c','x','v' ).
Floating action buttons go against the fullscreen schema it seems ( maybe not - it's my first time to do anything with Android programming whatsoever ), so I changed the fullscreen theme. There still is that title-bar - maybe some real expert can fix it.
Then I can play mercuris chess. And maybe get past the 'lighting 5 lamps' in Solarus dx if I can tweak the movement to be intuitive enough.
It would be really cool to have some interaction between the engine and the android interface, so that e.g. the 'x' FAB shows the 'cat food' icon when I select 'cat food', but at least I can play without keyboard now.

- Karl

--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,7 +45,7 @@

         <activity android:name=".SolarusEngine"
             android:configChanges="orientation|screenSize"
-            android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+            android:theme="@style/AppFullScreenTheme">
         </activity>
         <activity
             android:name=".Solarus"
diff --git a/app/src/main/java/org/solarus_games/solarus/SolarusEngine.java b/app/src/main/java/org/solarus_games/solarus/SolarusEngine.java
index 4fa3d91..ca46037 100644
--- a/app/src/main/java/org/solarus_games/solarus/SolarusEngine.java
+++ b/app/src/main/java/org/solarus_games/solarus/SolarusEngine.java
@@ -2,29 +2,99 @@ package org.solarus_games.solarus;

import android.app.Activity;
import android.content.Intent;
+import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
+import android.support.design.widget.FloatingActionButton;
import android.util.Log;
import android.view.KeyEvent;
+import android.view.MotionEvent;
import android.view.PixelCopy;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
import android.widget.Toast;

import org.libsdl.app.SDLActivity;

+import static android.view.KeyEvent.KEYCODE_C;
+import static android.view.KeyEvent.KEYCODE_D;
+import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
+import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
+import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
+import static android.view.KeyEvent.KEYCODE_DPAD_UP;
+import static android.view.KeyEvent.KEYCODE_SPACE;
+import static android.view.KeyEvent.KEYCODE_V;
+import static android.view.KeyEvent.KEYCODE_X;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static java.lang.Math.abs;
+
public class SolarusEngine extends SDLActivity {
     public Quest quest;
     final private String TAG = "SolarusEngine";
     public Bitmap screenCapture;

+
+    protected static float touch_xy[]; // last tracked position of touch
+    protected static int g_delta[]; // computed binary delta for simulated cursor key press
+    protected static int pointerid;
+    protected static int movecount; // number of events in this touch sequence
+    protected static long  move_time;
+
+
+    FloatingActionButton createButton(final int keycode, int offset, int color, final int imageId)
+    {
+   FloatingActionButton floatingActionButton1 = new FloatingActionButton(this);
+   RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                                 ViewGroup.LayoutParams.WRAP_CONTENT);
+   layoutParams.setMargins(32, 32, 32, 32 + 128*offset);
+   layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,RelativeLayout.TRUE);
+   layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM,RelativeLayout.TRUE);
+               
+   floatingActionButton1.setLayoutParams(layoutParams);
+   floatingActionButton1.setBackgroundTintList(ColorStateList.valueOf(color));
+   floatingActionButton1.setImageResource(imageId);
+   floatingActionButton1.setOnTouchListener(new View.OnTouchListener() {
+      @Override
+      public boolean onTouch(View view, MotionEvent event) {
+          int action = event.getActionMasked();
+          switch(action) {
+          case MotionEvent.ACTION_UP:
+         SDLActivity.onNativeKeyUp(keycode);
+         break;
+          case MotionEvent.ACTION_DOWN:
+         SDLActivity.onNativeKeyDown(keycode);
+         break;
+          }
+          return true;
+      }
+       });
+
+   mLayout.addView(floatingActionButton1);
+   return floatingActionButton1;
+    }
+
+
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         quest = Quest.fromPath(getIntent().getExtras().getString("quest_path"));
         SolarusApp.setCurrentQuest(this);
+        {
+       FloatingActionButton floatingActionButton1 = createButton(KEYCODE_SPACE,0, Color.parseColor("#00c0ff"),
+                              android.R.drawable.ic_menu_agenda);
+            FloatingActionButton floatingActionButton2 = createButton(KEYCODE_C,1,Color.parseColor("#90ff90"),
+                              android.R.drawable.ic_menu_compass);
+            FloatingActionButton floatingActionButton3 = createButton(KEYCODE_X,2,Color.parseColor("#999999"),
+                              android.R.drawable.ic_menu_mylocation);
+            FloatingActionButton floatingActionButton4 = createButton(KEYCODE_V,3,Color.parseColor("#999999"),
+                              android.R.drawable.ic_menu_add);
+        }
     }

     @Override
@@ -43,6 +113,63 @@ public class SolarusEngine extends SDLActivity {
                 || "google_sdk".equals(Build.PRODUCT);
     }

+    protected int apply_key_delta(int dx,int newdx,boolean hor, boolean repeat)
+    {
+        if (dx == newdx) {  // arrow repeat for intro screen
+       if (repeat) { // don't make it too finiky. Moving the hero doesn't rely on repeat anyway.
+      if (newdx < 0) {
+          SDLActivity.onNativeKeyUp( hor ? KEYCODE_DPAD_LEFT : KEYCODE_DPAD_UP);
+          SDLActivity.onNativeKeyDown( hor ? KEYCODE_DPAD_LEFT : KEYCODE_DPAD_UP);
+      } else if (newdx > 0) {
+          SDLActivity.onNativeKeyUp( hor ? KEYCODE_DPAD_RIGHT : KEYCODE_DPAD_DOWN);
+          SDLActivity.onNativeKeyDown( hor ? KEYCODE_DPAD_RIGHT : KEYCODE_DPAD_DOWN);
+      }      
+       }
+       return dx;
+   }
+        if (newdx == 0) { return dx; }
+        if (dx == 0) {
+            if (newdx < 0) {
+      movecount += 1;
+                SDLActivity.onNativeKeyDown( hor ? KEYCODE_DPAD_LEFT : KEYCODE_DPAD_UP);
+            } else if (newdx > 0) {
+      movecount += 1;
+                SDLActivity.onNativeKeyDown( hor ? KEYCODE_DPAD_RIGHT : KEYCODE_DPAD_DOWN);
+            }
+            return newdx;
+        } else { // dx,newdx are opposite
+            if (dx < 0) {
+                SDLActivity.onNativeKeyUp( hor ? KEYCODE_DPAD_LEFT : KEYCODE_DPAD_UP);
+            } else if (dx > 0) {
+                SDLActivity.onNativeKeyUp( hor ? KEYCODE_DPAD_RIGHT : KEYCODE_DPAD_DOWN);
+            }
+            return 0;
+        }
+    }
+   
+    protected int[] GetBinaryDelta(float[] mdelta)
+    {
+        int res[] = new int[2];
+   final int limit = 5;
+        res[0] = 0;
+        res[1] = 0;
+   if ( (abs(mdelta[0]) < limit)&& (abs(mdelta[1]) < limit) ) {
+       return res;
+   }
+        if (abs(mdelta[0]) < 0.8 * abs(mdelta[1])){
+            res[0] = 0;
+            res[1] = mdelta[1] > 0 ? 1:-1;
+        } else if (abs(mdelta[1]) < (0.8 * abs(mdelta[0]))) {
+            res[0] = mdelta[0] > 0 ? 1:-1;
+            res[1] = 0;
+        } else {
+            res[0] = mdelta[0] > 0 ? 1:-1;
+            res[1] = mdelta[1] > 0 ? 1:-1;
+        }
+        return res;
+    }
+
+
     private void bringMainActivityToFront() {
         Intent i = new Intent(getApplicationContext(), Solarus.class);
         i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
@@ -70,6 +197,120 @@ public class SolarusEngine extends SDLActivity {
         return super.dispatchKeyEvent(event);
     }

+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+   // turn certain touch events into keys so hero can move
+   View sv = mSurface;
+   int width = sv.getWidth();
+   int height = sv.getHeight();
+   int minw = width > height ? height:width;
+   if (event.getX() < width-minw/4) { // don't do anything with our floating action buttons or surrounds.
+       onTouch(event);
+   }
+   return super.dispatchTouchEvent(event);
+    }
+
+   
+    public boolean onTouch(MotionEvent event) {
+        if (touch_xy == null) {
+            touch_xy = new float[2];
+        }
+        if (g_delta == null) {
+            g_delta = new int[2];
+        }
+        // should translate to keystrokes in sensible manner.
+        int action = event.getActionMasked();
+        switch(action) {
+        case ACTION_MOVE:
+            if (movecount > 0) {
+      int finger = event.findPointerIndex(pointerid);
+                float delta[] = new float[2];
+                delta[0] = event.getX(finger) - touch_xy[0];
+                delta[1] = event.getY(finger) - touch_xy[1];
+                int ndelta[] = GetBinaryDelta(delta);
+      long new_time = event.getEventTime();
+      long timedelta = new_time-move_time;
+      boolean dorepeat = false;
+      boolean largemove = (abs(delta[0])+abs(delta[1]) > 50);
+      boolean smallmove = (abs(delta[0])+abs(delta[1]) > 10);
+      if ((timedelta > 200)|| largemove)  { // restart
+          dorepeat = true;
+          move_time = new_time;
+      }
+      if (smallmove || dorepeat) {
+          g_delta[0] = apply_key_delta(g_delta[0], ndelta[0], true, dorepeat);
+          g_delta[1] = apply_key_delta(g_delta[1], ndelta[1], false, dorepeat);
+          if ((ndelta[0] != 0)||(ndelta[1] != 0)) { // otherwise not moved enough.
+         touch_xy[0] = event.getX(finger);
+         touch_xy[1] = event.getY(finger);
+          }
+      }
+            }
+            break;
+        case MotionEvent.ACTION_UP:
+            {
+      int finger = event.findPointerIndex(pointerid);
+                float delta[] = new float[2];
+                delta[0] = event.getX(finger) - touch_xy[0];
+                delta[1] = event.getY(finger) - touch_xy[1];
+                if ((delta[0] > 1) || (delta[1] > 1)) {
+                    int ndelta[] = GetBinaryDelta(delta);
+                    g_delta[0] = apply_key_delta(g_delta[0], ndelta[0], true , true);
+                    g_delta[1] = apply_key_delta(g_delta[1], ndelta[1], false, true);
+                }
+
+                // up no move, if  move first should apply that.
+                g_delta[0] = apply_key_delta(g_delta[0], -g_delta[0], true, true);
+                g_delta[1] = apply_key_delta(g_delta[1], -g_delta[1], false, true);
+                if (movecount == 1) {
+          long new_time = event.getEventTime();
+          long timedelta = new_time-move_time;
+          if (timedelta < 200) { // just a tap. Anoying otherwise.
+         SDLActivity.onNativeKeyDown(KEYCODE_D);
+         SDLActivity.onNativeKeyUp(KEYCODE_D);
+          }
+                }
+                movecount = 0;
+            }
+            break;
+        case MotionEvent.ACTION_POINTER_DOWN:
+       {
+       final int mypointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+       final int mypointerId = event.getPointerId(mypointerIndex);
+       if (mypointerId != pointerid){ // emit 'menu' keycode
+      SDLActivity.onNativeKeyDown(KEYCODE_D);
+      SDLActivity.onNativeKeyUp(KEYCODE_D);
+       }
+       }
+            break;
+        case MotionEvent.ACTION_POINTER_UP:
+       {
+       final int mypointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+       final int mypointerId = event.getPointerId(mypointerIndex);
+       if (mypointerId == pointerid){
+      // the main one going up !
+      // some other finger is still resting on the surface.
+                g_delta[0] = apply_key_delta(g_delta[0], -g_delta[0], true, true);
+                g_delta[1] = apply_key_delta(g_delta[1], -g_delta[1], false, true);
+      movecount = 0;
+       }
+       }
+            break;
+        case MotionEvent.ACTION_DOWN:
+       pointerid = event.getPointerId(0); // down is the first finger down
+       move_time = event.getEventTime();
+
+            touch_xy[0] = event.getX(0);
+            touch_xy[1] = event.getY(0);
+            g_delta[0] = 0;
+            g_delta[1] = 0;
+            movecount = 1; // if new pointerID == pointerid.
+            break;
+        }
+   return true;
+    }
+
+
     public void exit() {
         SDLActivity.mExitCalledFromJava = true;
         SDLActivity.nativeQuit();
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index c322a03..24b25c2 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -17,6 +17,13 @@
         <item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
     </style>

+    <style name="AppFullScreenTheme" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+    </style>
+
     <!-- Texts -->
     <style name="paragraph">
         <item name="android:textSize">@dimen/text_size</item>

Hi Karl,

Sorry to be rough about the git thing.
While your contribution is appreciated. Could you please put your patch in a code block ?
Also, I had in mind to have virtual buttons not triggering key events but SDL Joypad events. So that
Quests can view the android touch controls as a Joypad.

If you are willing to contribute we are really open to accept merge requests as long as the code
respects the style of the rest of the code base. Since you were able to hack this example so quickly,
consider contributing if you have the time.

Greg

Hi Greg,

I'll have to see about what I have to do to submit joypad inputs instead. I can see where you are coming from though - the quests can request any number of keys, whereas the joypad should only have a consistent and limited number of buttons.
I also made two improvements since yesterday:
1. hold the sword button , then move didn't move - now it does.
2. For return of the Hylian SE, at the very beginning, Link has to move fast ( with help of 'shift' key ) before the sword is obtained - otherwise, he is surrounded and can't escape the monsters.

- Karl


diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4a57f67..7a2454a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,7 +45,7 @@

         <activity android:name=".SolarusEngine"
             android:configChanges="orientation|screenSize"
-            android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+            android:theme="@style/AppFullScreenTheme">
         </activity>
         <activity
             android:name=".Solarus"
diff --git a/app/src/main/java/org/solarus_games/solarus/SolarusEngine.java b/app/src/main/java/org/solarus_games/solarus/SolarusEngine.java
index 4fa3d91..b484518 100644
--- a/app/src/main/java/org/solarus_games/solarus/SolarusEngine.java
+++ b/app/src/main/java/org/solarus_games/solarus/SolarusEngine.java
@@ -2,29 +2,100 @@ package org.solarus_games.solarus;

import android.app.Activity;
import android.content.Intent;
+import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
+import android.support.design.widget.FloatingActionButton;
import android.util.Log;
import android.view.KeyEvent;
+import android.view.MotionEvent;
import android.view.PixelCopy;
import android.view.View;
-import android.widget.Toast;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;

import org.libsdl.app.SDLActivity;

+import static android.view.KeyEvent.KEYCODE_C;
+import static android.view.KeyEvent.KEYCODE_D;
+import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
+import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
+import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
+import static android.view.KeyEvent.KEYCODE_DPAD_UP;
+import static android.view.KeyEvent.KEYCODE_ESCAPE;
+import static android.view.KeyEvent.KEYCODE_SHIFT_LEFT;
+import static android.view.KeyEvent.KEYCODE_SPACE;
+import static android.view.KeyEvent.KEYCODE_V;
+import static android.view.KeyEvent.KEYCODE_X;
+import static android.view.MotionEvent.ACTION_MOVE;
+import static java.lang.Math.abs;
+
public class SolarusEngine extends SDLActivity {
     public Quest quest;
     final private String TAG = "SolarusEngine";
     public Bitmap screenCapture;

+
+    protected static float touch_xy[]; // last tracked position of touch
+    protected static int g_delta[]; // computed binary delta for simulated cursor key press
+    protected static int pointerid;
+    protected static int movecount; // number of events in this touch sequence
+    protected static long  move_time;
+
+
+    FloatingActionButton createButton(final int keycode, int offset, int color, final int imageId)
+    {
+        FloatingActionButton floatingActionButton1 = new FloatingActionButton(this);
+        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                                                                                   ViewGroup.LayoutParams.WRAP_CONTENT);
+        layoutParams.setMargins(32, 32, 32, 32 + 128*offset);
+        layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT,RelativeLayout.TRUE);
+        layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM,RelativeLayout.TRUE);
+               
+        floatingActionButton1.setLayoutParams(layoutParams);
+        floatingActionButton1.setBackgroundTintList(ColorStateList.valueOf(color));
+        floatingActionButton1.setImageResource(imageId);
+        floatingActionButton1.setOnTouchListener(new View.OnTouchListener() {
+                @Override
+                public boolean onTouch(View view, MotionEvent event) {
+                    int action = event.getActionMasked();
+                    switch(action) {
+                    case MotionEvent.ACTION_UP:
+                        SDLActivity.onNativeKeyUp(keycode);
+                        break;
+                    case MotionEvent.ACTION_DOWN:
+                        SDLActivity.onNativeKeyDown(keycode);
+                        break;
+                    }
+                    return true;
+                }
+            });
+
+        mLayout.addView(floatingActionButton1);
+        return floatingActionButton1;
+    }
+
+
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         quest = Quest.fromPath(getIntent().getExtras().getString("quest_path"));
         SolarusApp.setCurrentQuest(this);
+        {
+            FloatingActionButton floatingActionButton1 = createButton(KEYCODE_SPACE,0, Color.parseColor("#00c0ff"),
+                                                                      android.R.drawable.ic_menu_agenda);
+            FloatingActionButton floatingActionButton2 = createButton(KEYCODE_C,1,Color.parseColor("#90ff90"),
+                                                                      android.R.drawable.ic_menu_compass);
+            FloatingActionButton floatingActionButton3 = createButton(KEYCODE_X,2,Color.parseColor("#999999"),
+                                                                      android.R.drawable.ic_menu_mylocation);
+            FloatingActionButton floatingActionButton4 = createButton(KEYCODE_V,3,Color.parseColor("#999999"),
+                                                                      android.R.drawable.ic_menu_add);
+        }
     }

     @Override
@@ -43,6 +114,73 @@ public class SolarusEngine extends SDLActivity {
                 || "google_sdk".equals(Build.PRODUCT);
     }

+    protected int apply_key_delta(int dx,int newdx,boolean hor, boolean repeat)
+    {
+        if (Math.signum(dx) == Math.signum(newdx)) {  // arrow repeat for intro screen
+            if (repeat) { // don't make it too finiky. Moving the hero doesn't rely on repeat anyway.
+                if (abs(dx) == 1) {
+                    SDLActivity.onNativeKeyDown( KEYCODE_SHIFT_LEFT );
+                    dx += newdx;
+                } else {
+                    if (newdx < 0) {
+                        SDLActivity.onNativeKeyUp( hor ? KEYCODE_DPAD_LEFT : KEYCODE_DPAD_UP);
+                        SDLActivity.onNativeKeyDown( hor ? KEYCODE_DPAD_LEFT : KEYCODE_DPAD_UP);
+                    } else if (newdx > 0) {
+                        SDLActivity.onNativeKeyUp( hor ? KEYCODE_DPAD_RIGHT : KEYCODE_DPAD_DOWN);
+                        SDLActivity.onNativeKeyDown( hor ? KEYCODE_DPAD_RIGHT : KEYCODE_DPAD_DOWN);
+                    }       
+                }
+            }
+            return dx;
+        }
+        if (newdx == 0) { return dx; }
+        if (dx == 0) {
+            if (newdx < 0) {
+                movecount += 1;
+                SDLActivity.onNativeKeyDown( hor ? KEYCODE_DPAD_LEFT : KEYCODE_DPAD_UP);
+            } else if (newdx > 0) {
+                movecount += 1;
+                SDLActivity.onNativeKeyDown( hor ? KEYCODE_DPAD_RIGHT : KEYCODE_DPAD_DOWN);
+            }
+            return newdx;
+        } else { // dx,newdx are opposite
+            if (abs(dx) > 1) { // first remove the 'fast' key
+                SDLActivity.onNativeKeyUp( KEYCODE_SHIFT_LEFT );
+            }
+            if ( (abs(dx) == 1)||(abs(newdx) > 1) ) {
+                if (dx < 0) {
+                    SDLActivity.onNativeKeyUp( hor ? KEYCODE_DPAD_LEFT : KEYCODE_DPAD_UP);
+                } else if (dx > 0) {
+                    SDLActivity.onNativeKeyUp( hor ? KEYCODE_DPAD_RIGHT : KEYCODE_DPAD_DOWN);
+                }
+            }
+            return dx+newdx;
+        }
+    }
+   
+    protected int[] GetBinaryDelta(float[] mdelta)
+    {
+        int res[] = new int[2];
+        final int limit = 5;
+        res[0] = 0;
+        res[1] = 0;
+        if ( (abs(mdelta[0]) < limit)&& (abs(mdelta[1]) < limit) ) {
+            return res;
+        }
+        if (abs(mdelta[0]) < 0.8 * abs(mdelta[1])){
+            res[0] = 0;
+            res[1] = mdelta[1] > 0 ? 1:-1;
+        } else if (abs(mdelta[1]) < (0.8 * abs(mdelta[0]))) {
+            res[0] = mdelta[0] > 0 ? 1:-1;
+            res[1] = 0;
+        } else {
+            res[0] = mdelta[0] > 0 ? 1:-1;
+            res[1] = mdelta[1] > 0 ? 1:-1;
+        }
+        return res;
+    }
+
+
     private void bringMainActivityToFront() {
         Intent i = new Intent(getApplicationContext(), Solarus.class);
         i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
@@ -70,6 +208,121 @@ public class SolarusEngine extends SDLActivity {
         return super.dispatchKeyEvent(event);
     }

+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        // turn certain touch events into keys so hero can move
+
+       // don't do anything with our floating action buttons or surrounds.
+       // because then we'd get touch-down events that were actually for the FAB.
+       // - but that is handled in onTouch now...
+        onTouch(event);
+        return super.dispatchTouchEvent(event);
+    }
+
+   
+    public boolean onTouch(MotionEvent event) {
+        View sv = mSurface;
+        int width = sv.getWidth();
+        int height = sv.getHeight();
+        int minw = width > height ? height:width;
+       
+        if (touch_xy == null) {
+            touch_xy = new float[2];
+        }
+        if (g_delta == null) {
+            g_delta = new int[2];
+        }
+        // should translate to keystrokes in sensible manner.
+        int action = event.getActionMasked();
+        int fullaction = event.getAction();
+        switch(action) {
+        case ACTION_MOVE:
+            if (movecount > 0) {
+                int finger = event.findPointerIndex(pointerid);
+                float delta[] = new float[2];
+                delta[0] = event.getX(finger) - touch_xy[0];
+                delta[1] = event.getY(finger) - touch_xy[1];
+                int ndelta[] = GetBinaryDelta(delta);
+                long new_time = event.getEventTime();
+                long timedelta = new_time-move_time;
+                boolean dorepeat = false;
+                boolean largemove = (abs(delta[0])+abs(delta[1]) > 50);
+                boolean smallmove = (abs(delta[0])+abs(delta[1]) > 10);
+                if ((timedelta > 200)|| largemove)  { // restart
+                    dorepeat = true;
+                    move_time = new_time;
+                }
+                if (smallmove || dorepeat) {
+                    g_delta[0] = apply_key_delta(g_delta[0], ndelta[0], true, dorepeat);
+                    g_delta[1] = apply_key_delta(g_delta[1], ndelta[1], false, dorepeat);
+                    if ((ndelta[0] != 0)||(ndelta[1] != 0)) { // otherwise not moved enough.
+                        touch_xy[0] = event.getX(finger);
+                        touch_xy[1] = event.getY(finger);
+                    }
+                }
+            }
+            break;
+        case MotionEvent.ACTION_POINTER_UP:
+        case MotionEvent.ACTION_UP:
+            {
+                int finger = event.findPointerIndex(pointerid);
+                if (finger >= 0) {
+                    final int mypointerId = event.getPointerId(finger);
+                    if (mypointerId == pointerid) {
+                        float delta[] = new float[2];
+                        delta[0] = event.getX(finger) - touch_xy[0];
+                        delta[1] = event.getY(finger) - touch_xy[1];
+                        if ((delta[0] > 1) || (delta[1] > 1)) {
+                            int ndelta[] = GetBinaryDelta(delta);
+                            g_delta[0] = apply_key_delta(g_delta[0], ndelta[0], true , true);
+                            g_delta[1] = apply_key_delta(g_delta[1], ndelta[1], false, true);
+                        }
+
+                        // up no move, if  move first should apply that.
+                        g_delta[0] = apply_key_delta(g_delta[0], -g_delta[0], true, true);
+                        g_delta[1] = apply_key_delta(g_delta[1], -g_delta[1], false, true);
+                        if (movecount == 1) {
+                            long new_time = event.getEventTime();
+                            long timedelta = new_time-move_time;
+                            if ((timedelta < 200)&&(event.getY(finger) < minw/4)) { // just a tap. Anoying otherwise.
+                                if (event.getX(finger) < minw/4) {
+                                    SDLActivity.onNativeKeyDown(KEYCODE_ESCAPE);
+                                    SDLActivity.onNativeKeyUp(KEYCODE_ESCAPE);
+                                } else {
+                                    SDLActivity.onNativeKeyDown(KEYCODE_D);
+                                    SDLActivity.onNativeKeyUp(KEYCODE_D);
+                                }
+                            }
+                        }
+                        movecount = 0;
+                    }
+                }
+            }
+            break;
+        case MotionEvent.ACTION_DOWN:
+        case MotionEvent.ACTION_POINTER_DOWN:
+            {
+                final int mypointerIndex = (fullaction & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+                final int mypointerId = event.getPointerId(mypointerIndex);
+                if (event.getX(mypointerIndex) < width-minw/4) { // else reserved for FABs
+                    if (movecount == 0) { // e.g. the sword was activated first
+                        pointerid = mypointerId;
+                        move_time = event.getEventTime();
+                       
+                        touch_xy[0] = event.getX(0);
+                        touch_xy[1] = event.getY(0);
+                        g_delta[0] = 0;
+                        g_delta[1] = 0;
+                        movecount = 1; // if new pointerID == pointerid.
+                    }
+                }
+            }
+            break;
+        }
+        return true;
+    }
+
+
     public void exit() {
         SDLActivity.mExitCalledFromJava = true;
         SDLActivity.nativeQuit();
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index c322a03..24b25c2 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -17,6 +17,13 @@
         <item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
     </style>

+    <style name="AppFullScreenTheme" parent="Theme.AppCompat.Light.NoActionBar">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowFullscreen">true</item>
+        <item name="android:windowContentOverlay">@null</item>
+    </style>
+
     <!-- Texts -->
     <style name="paragraph">
         <item name="android:textSize">@dimen/text_size</item>