diff --git a/forge-core/src/main/java/forge/util/ImageUtil.java b/forge-core/src/main/java/forge/util/ImageUtil.java index 5940df56280..9c4f9d0b5bb 100644 --- a/forge-core/src/main/java/forge/util/ImageUtil.java +++ b/forge-core/src/main/java/forge/util/ImageUtil.java @@ -22,26 +22,10 @@ public class ImageUtil { } if (imageKey.startsWith(ImageKeys.CARD_PREFIX)) key = imageKey.substring(ImageKeys.CARD_PREFIX.length()); - else if (imageKey.startsWith(ImageKeys.TOKEN_PREFIX)) - key = imageKey.substring(ImageKeys.TOKEN_PREFIX.length()); - else if (imageKey.startsWith(ImageKeys.ICON_PREFIX)) - key = imageKey.substring(ImageKeys.ICON_PREFIX.length()); - else if (imageKey.startsWith(ImageKeys.BOOSTER_PREFIX)) - key = imageKey.substring(ImageKeys.BOOSTER_PREFIX.length()); - else if (imageKey.startsWith(ImageKeys.FATPACK_PREFIX)) - key = imageKey.substring(ImageKeys.FATPACK_PREFIX.length()); - else if (imageKey.startsWith(ImageKeys.BOOSTERBOX_PREFIX)) - key = imageKey.substring(ImageKeys.BOOSTERBOX_PREFIX.length()); - else if (imageKey.startsWith(ImageKeys.PRECON_PREFIX)) - key = imageKey.substring(ImageKeys.PRECON_PREFIX.length()); - else if (imageKey.startsWith(ImageKeys.TOURNAMENTPACK_PREFIX)) - key = imageKey.substring(ImageKeys.TOURNAMENTPACK_PREFIX.length()); - else if (imageKey.startsWith(ImageKeys.ADVENTURECARD_PREFIX)) - key = imageKey.substring(ImageKeys.ADVENTURECARD_PREFIX.length()); - else if (imageKey.contains(".full")) {//no prefix found, construct a valid key if imageKey is art imagekey. - key = transformKey(imageKey); - } else //try anyway... - key = imageKey; + else + return null; + if (key.isEmpty()) + return null; CardDb db = StaticData.instance().getCommonCards(); PaperCard cp = null; diff --git a/forge-gui-android/libs/gdx-backend-android-sources.jar b/forge-gui-android/libs/gdx-backend-android-sources.jar index 5d44f0a0b9c..0f96bb27b4d 100644 Binary files a/forge-gui-android/libs/gdx-backend-android-sources.jar and b/forge-gui-android/libs/gdx-backend-android-sources.jar differ diff --git a/forge-gui-android/libs/gdx-backend-android.jar b/forge-gui-android/libs/gdx-backend-android.jar index da5c7c2bd25..f734d365cb4 100644 Binary files a/forge-gui-android/libs/gdx-backend-android.jar and b/forge-gui-android/libs/gdx-backend-android.jar differ diff --git a/forge-gui-android/res/drawable/design_ic_visibility.xml b/forge-gui-android/res/drawable/design_ic_visibility.xml new file mode 100644 index 00000000000..5b3c817a076 --- /dev/null +++ b/forge-gui-android/res/drawable/design_ic_visibility.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/forge-gui-android/res/drawable/design_ic_visibility_off.xml b/forge-gui-android/res/drawable/design_ic_visibility_off.xml new file mode 100644 index 00000000000..d79be979ef7 --- /dev/null +++ b/forge-gui-android/res/drawable/design_ic_visibility_off.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/forge-gui-android/src/com/badlogic/gdx/backends/android/ForgeAndroidApplication.java b/forge-gui-android/src/com/badlogic/gdx/backends/android/AndroidApplication.java similarity index 93% rename from forge-gui-android/src/com/badlogic/gdx/backends/android/ForgeAndroidApplication.java rename to forge-gui-android/src/com/badlogic/gdx/backends/android/AndroidApplication.java index f126aa4abde..75b84958fdd 100644 --- a/forge-gui-android/src/com/badlogic/gdx/backends/android/ForgeAndroidApplication.java +++ b/forge-gui-android/src/com/badlogic/gdx/backends/android/AndroidApplication.java @@ -31,18 +31,18 @@ import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; import com.badlogic.gdx.*; -/*import com.badlogic.gdx.backends.android.keyboardheight.AndroidRKeyboardHeightProvider; +import com.badlogic.gdx.backends.android.keyboardheight.AndroidRKeyboardHeightProvider; import com.badlogic.gdx.backends.android.keyboardheight.KeyboardHeightProvider; -import com.badlogic.gdx.backends.android.keyboardheight.StandardKeyboardHeightProvider;*/ +import com.badlogic.gdx.backends.android.keyboardheight.StandardKeyboardHeightProvider; import com.badlogic.gdx.backends.android.surfaceview.FillResolutionStrategy; import com.badlogic.gdx.utils.*; -/** A modified implementation of the {@link Application} interface for Android - Forge app. Create an {@link Activity} that derives from this class. +/** An implementation of the {@link Application} interface for Android. Create an {@link Activity} that derives from this class. * In the {@link Activity#onCreate(Bundle)} method call the {@link #initialize(ApplicationListener)} method specifying the * configuration for the GLSurfaceView. * * @author mzechner */ -public class ForgeAndroidApplication extends Activity implements AndroidApplicationBase { +public class AndroidApplication extends Activity implements AndroidApplicationBase { protected AndroidGraphics graphics; protected AndroidInput input; @@ -56,14 +56,14 @@ public class ForgeAndroidApplication extends Activity implements AndroidApplicat protected final Array runnables = new Array(); protected final Array executedRunnables = new Array(); protected final SnapshotArray lifecycleListeners = new SnapshotArray( - LifecycleListener.class); + LifecycleListener.class); private final Array androidEventListeners = new Array(); protected int logLevel = LOG_INFO; protected ApplicationLogger applicationLogger; protected boolean useImmersiveMode = false; private int wasFocusChanged = -1; private boolean isWaitingForAudio = false; - //private KeyboardHeightProvider keyboardHeightProvider; + private KeyboardHeightProvider keyboardHeightProvider; protected boolean renderUnderCutout = false; @@ -121,7 +121,7 @@ public class ForgeAndroidApplication extends Activity implements AndroidApplicat config.nativeLoader.load(); setApplicationLogger(new AndroidApplicationLogger()); graphics = new AndroidGraphics(this, config, - config.resolutionStrategy == null ? new FillResolutionStrategy() : config.resolutionStrategy); + config.resolutionStrategy == null ? new FillResolutionStrategy() : config.resolutionStrategy); input = createInput(this, this, graphics.view, config); audio = createAudio(this, config); files = createFiles(); @@ -171,26 +171,26 @@ public class ForgeAndroidApplication extends Activity implements AndroidApplicat createWakeLock(config.useWakelock); useImmersiveMode(this.useImmersiveMode); - /*if (this.useImmersiveMode) { + if (this.useImmersiveMode) { AndroidVisibilityListener vlistener = new AndroidVisibilityListener(); vlistener.createListener(this); - }*/ + } // detect an already connected bluetooth keyboardAvailable if (getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS) input.setKeyboardAvailable(true); setLayoutInDisplayCutoutMode(this.renderUnderCutout); - /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { keyboardHeightProvider = new AndroidRKeyboardHeightProvider(this); } else { keyboardHeightProvider = new StandardKeyboardHeightProvider(this); - }*/ + } } protected FrameLayout.LayoutParams createLayoutParams () { FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, - android.view.ViewGroup.LayoutParams.MATCH_PARENT); + android.view.ViewGroup.LayoutParams.MATCH_PARENT); layoutParams.gravity = Gravity.CENTER; return layoutParams; } @@ -226,13 +226,13 @@ public class ForgeAndroidApplication extends Activity implements AndroidApplicat @Override public void useImmersiveMode (boolean use) { - /*if (!use) return; + if (!use) return; View view = getWindow().getDecorView(); int code = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; - view.setSystemUiVisibility(code);*/ + view.setSystemUiVisibility(code); } @Override @@ -260,7 +260,7 @@ public class ForgeAndroidApplication extends Activity implements AndroidApplicat graphics.onPauseGLSurfaceView(); super.onPause(); - //keyboardHeightProvider.setKeyboardHeightObserver(null); + keyboardHeightProvider.setKeyboardHeightObserver(null); } @Override @@ -289,19 +289,19 @@ public class ForgeAndroidApplication extends Activity implements AndroidApplicat this.isWaitingForAudio = false; } super.onResume(); - /*keyboardHeightProvider.setKeyboardHeightObserver((DefaultAndroidInput)Gdx.input); + keyboardHeightProvider.setKeyboardHeightObserver((DefaultAndroidInput)Gdx.input); ((AndroidGraphics)getGraphics()).getView().post(new Runnable() { @Override public void run () { keyboardHeightProvider.start(); } - });*/ + }); } @Override protected void onDestroy () { super.onDestroy(); - //keyboardHeightProvider.close(); + keyboardHeightProvider.close(); } @Override @@ -385,7 +385,7 @@ public class ForgeAndroidApplication extends Activity implements AndroidApplicat handler.post(new Runnable() { @Override public void run () { - ForgeAndroidApplication.this.finish(); + AndroidApplication.this.finish(); } }); } @@ -528,7 +528,7 @@ public class ForgeAndroidApplication extends Activity implements AndroidApplicat return new DefaultAndroidFiles(this.getAssets(), this, true); } - /*public KeyboardHeightProvider getKeyboardHeightProvider () { + public KeyboardHeightProvider getKeyboardHeightProvider () { return keyboardHeightProvider; - }*/ + } } \ No newline at end of file diff --git a/forge-gui-android/src/com/badlogic/gdx/backends/android/DefaultAndroidInput.java b/forge-gui-android/src/com/badlogic/gdx/backends/android/DefaultAndroidInput.java new file mode 100644 index 00000000000..7828c995430 --- /dev/null +++ b/forge-gui-android/src/com/badlogic/gdx/backends/android/DefaultAndroidInput.java @@ -0,0 +1,1523 @@ +/******************************************************************************* + * Copyright 2011 See AUTHORS file. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +package com.badlogic.gdx.backends.android; + +import android.animation.Animator; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.graphics.Color; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.Build; +import android.os.Handler; +import android.text.*; +import android.text.InputFilter; +import android.text.InputFilter.LengthFilter; +import android.text.method.PasswordTransformationMethod; +import android.util.DisplayMetrics; +import android.view.*; +import android.view.View.OnGenericMotionListener; +import android.view.View.OnKeyListener; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputConnectionWrapper; +import android.view.inputmethod.InputMethodManager; +import android.widget.*; +import android.widget.TextView.OnEditorActionListener; +import android.window.OnBackInvokedCallback; +import android.window.OnBackInvokedDispatcher; + +import com.badlogic.gdx.AbstractInput; +import com.badlogic.gdx.Application; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Graphics.DisplayMode; +import com.badlogic.gdx.Input; +import com.badlogic.gdx.InputProcessor; +import com.badlogic.gdx.backends.android.keyboardheight.KeyboardHeightObserver; +import com.badlogic.gdx.backends.android.keyboardheight.KeyboardHeightProvider; +import com.badlogic.gdx.backends.android.keyboardheight.StandardKeyboardHeightProvider; +import com.badlogic.gdx.backends.android.surfaceview.GLSurfaceView20; +import com.badlogic.gdx.input.NativeInputConfiguration; +import com.badlogic.gdx.input.TextInputWrapper; +import com.badlogic.gdx.utils.GdxRuntimeException; +import com.badlogic.gdx.utils.Pool; + +import java.util.ArrayList; +import java.util.List; + +/** An implementation of the {@link Input} interface for Android. + * + * @author mzechner + * @author jshapcot */ +public class DefaultAndroidInput extends AbstractInput implements AndroidInput, KeyboardHeightObserver { + + static class KeyEvent { + static final int KEY_DOWN = 0; + static final int KEY_UP = 1; + static final int KEY_TYPED = 2; + + long timeStamp; + int type; + int keyCode; + char keyChar; + } + + static class TouchEvent { + static final int TOUCH_DOWN = 0; + static final int TOUCH_UP = 1; + static final int TOUCH_DRAGGED = 2; + static final int TOUCH_SCROLLED = 3; + static final int TOUCH_MOVED = 4; + static final int TOUCH_CANCELLED = 5; + + long timeStamp; + int type; + int x; + int y; + int scrollAmountX; + int scrollAmountY; + int button; + int pointer; + } + + Pool usedKeyEvents = new Pool(16, 1000) { + protected KeyEvent newObject () { + return new KeyEvent(); + } + }; + + Pool usedTouchEvents = new Pool(16, 1000) { + protected TouchEvent newObject () { + return new TouchEvent(); + } + }; + + public static final int NUM_TOUCHES = 20; + + ArrayList keyListeners = new ArrayList(); + ArrayList keyEvents = new ArrayList(); + ArrayList touchEvents = new ArrayList(); + int[] touchX = new int[NUM_TOUCHES]; + int[] touchY = new int[NUM_TOUCHES]; + int[] deltaX = new int[NUM_TOUCHES]; + int[] deltaY = new int[NUM_TOUCHES]; + boolean[] touched = new boolean[NUM_TOUCHES]; + int[] button = new int[NUM_TOUCHES]; + int[] realId = new int[NUM_TOUCHES]; + float[] pressure = new float[NUM_TOUCHES]; + final boolean hasMultitouch; + private boolean[] justPressedButtons = new boolean[NUM_TOUCHES]; + private SensorManager manager; + public boolean accelerometerAvailable = false; + protected final float[] accelerometerValues = new float[3]; + public boolean gyroscopeAvailable = false; + protected final float[] gyroscopeValues = new float[3]; + private Handler handle; + final Application app; + final Context context; + protected final AndroidTouchHandler touchHandler; + private int sleepTime = 0; + protected final AndroidHaptics haptics; + private boolean compassAvailable = false; + private boolean rotationVectorAvailable = false; + boolean keyboardAvailable; + protected final float[] magneticFieldValues = new float[3]; + protected final float[] rotationVectorValues = new float[3]; + private float azimuth = 0; + private float pitch = 0; + private float roll = 0; + private boolean justTouched = false; + private InputProcessor processor; + private final AndroidApplicationConfiguration config; + protected final Orientation nativeOrientation; + private long currentEventTimeStamp = 0; + private PredictiveBackHandler predictiveBackHandler; + + private SensorEventListener accelerometerListener; + private SensorEventListener gyroscopeListener; + private SensorEventListener compassListener; + private SensorEventListener rotationVectorListener; + + private final ArrayList genericMotionListeners = new ArrayList(); + private final AndroidMouseHandler mouseHandler; + + public DefaultAndroidInput (Application activity, Context context, Object view, AndroidApplicationConfiguration config) { + // we hook into View, for LWPs we call onTouch below directly from + // within the AndroidLivewallpaperEngine#onTouchEvent() method. + if (view instanceof View) { + View v = (View)view; + v.setOnKeyListener(this); + v.setOnTouchListener(this); + v.setFocusable(true); + v.setFocusableInTouchMode(true); + v.requestFocus(); + v.setOnGenericMotionListener(this); + } + this.config = config; + this.mouseHandler = new AndroidMouseHandler(); + + for (int i = 0; i < realId.length; i++) + realId[i] = -1; + handle = new Handler(); + this.app = activity; + this.context = context; + this.sleepTime = config.touchSleepTime; + touchHandler = new AndroidTouchHandler(); + hasMultitouch = touchHandler.supportsMultitouch(context); + + haptics = new AndroidHaptics(context); + + if (Build.VERSION.SDK_INT >= 33 && context instanceof Activity) { + this.predictiveBackHandler = new PredictiveBackHandler(); + } + + int rotation = getRotation(); + DisplayMode mode = app.getGraphics().getDisplayMode(); + if (((rotation == 0 || rotation == 180) && (mode.width >= mode.height)) + || ((rotation == 90 || rotation == 270) && (mode.width <= mode.height))) { + nativeOrientation = Orientation.Landscape; + } else { + nativeOrientation = Orientation.Portrait; + } + + // this is for backward compatibility: libGDX always caught the circle button, original comment: + // circle button on Xperia Play shouldn't need catchBack == true + setCatchKey(Keys.BUTTON_CIRCLE, true); + } + + @Override + public float getAccelerometerX () { + return accelerometerValues[0]; + } + + @Override + public float getAccelerometerY () { + return accelerometerValues[1]; + } + + @Override + public float getAccelerometerZ () { + return accelerometerValues[2]; + } + + @Override + public float getGyroscopeX () { + return gyroscopeValues[0]; + } + + @Override + public float getGyroscopeY () { + return gyroscopeValues[1]; + } + + @Override + public float getGyroscopeZ () { + return gyroscopeValues[2]; + } + + @Override + public void getTextInput (TextInputListener listener, String title, String text, String hint) { + getTextInput(listener, title, text, hint, OnscreenKeyboardType.Default); + } + + @Override + public void getTextInput (final TextInputListener listener, final String title, final String text, final String hint, + final OnscreenKeyboardType keyboardType) { + handle.post(new Runnable() { + public void run () { + AlertDialog.Builder alert = new AlertDialog.Builder(context); + alert.setTitle(title); + final EditText input = new EditText(context); + if (keyboardType != OnscreenKeyboardType.Default) { + input.setInputType(getAndroidInputType(keyboardType, false)); + } + input.setHint(hint); + input.setText(text); + input.setSingleLine(); + if (keyboardType == OnscreenKeyboardType.Password) { + input.setTransformationMethod(new PasswordTransformationMethod()); + } + alert.setView(input); + alert.setPositiveButton(context.getString(android.R.string.ok), new DialogInterface.OnClickListener() { + public void onClick (DialogInterface dialog, int whichButton) { + Gdx.app.postRunnable(new Runnable() { + @Override + public void run () { + listener.input(input.getText().toString()); + } + }); + } + }); + alert.setNegativeButton(context.getString(android.R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick (DialogInterface dialog, int whichButton) { + Gdx.app.postRunnable(new Runnable() { + @Override + public void run () { + listener.canceled(); + } + }); + } + }); + alert.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel (DialogInterface arg0) { + Gdx.app.postRunnable(new Runnable() { + @Override + public void run () { + listener.canceled(); + } + }); + } + }); + alert.show(); + } + }); + } + + public static int getAndroidInputType (OnscreenKeyboardType type, boolean defaultDisableAutocorrection) { + int inputType; + switch (type) { + case NumberPad: + inputType = InputType.TYPE_CLASS_NUMBER; + break; + case PhonePad: + inputType = InputType.TYPE_CLASS_PHONE; + break; + case Email: + inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + break; + case Password: + inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD; + break; + case URI: + inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI; + break; + default: + if (defaultDisableAutocorrection) { + inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; + } else { + inputType = InputType.TYPE_CLASS_TEXT; + } + break; + } + return inputType; + } + + @Override + public int getMaxPointers () { + return NUM_TOUCHES; + } + + @Override + public int getX () { + synchronized (this) { + return touchX[0]; + } + } + + @Override + public int getY () { + synchronized (this) { + return touchY[0]; + } + } + + @Override + public int getX (int pointer) { + synchronized (this) { + return touchX[pointer]; + } + } + + @Override + public int getY (int pointer) { + synchronized (this) { + return touchY[pointer]; + } + } + + public boolean isTouched (int pointer) { + synchronized (this) { + return touched[pointer]; + } + } + + @Override + public float getPressure () { + return getPressure(0); + } + + @Override + public float getPressure (int pointer) { + return pressure[pointer]; + } + + @Override + public void setKeyboardAvailable (boolean available) { + this.keyboardAvailable = available; + } + + @Override + public boolean isTouched () { + synchronized (this) { + if (hasMultitouch) { + for (int pointer = 0; pointer < NUM_TOUCHES; pointer++) { + if (touched[pointer]) { + return true; + } + } + } + return touched[0]; + } + } + + public void setInputProcessor (InputProcessor processor) { + synchronized (this) { + this.processor = processor; + } + } + + @Override + public void processEvents () { + synchronized (this) { + if (justTouched) { + justTouched = false; + for (int i = 0; i < justPressedButtons.length; i++) { + justPressedButtons[i] = false; + } + } + if (keyJustPressed) { + keyJustPressed = false; + for (int i = 0; i < justPressedKeys.length; i++) { + justPressedKeys[i] = false; + } + } + + if (processor != null) { + final InputProcessor processor = this.processor; + + int len = keyEvents.size(); + for (int i = 0; i < len; i++) { + KeyEvent e = keyEvents.get(i); + currentEventTimeStamp = e.timeStamp; + switch (e.type) { + case KeyEvent.KEY_DOWN: + processor.keyDown(e.keyCode); + keyJustPressed = true; + justPressedKeys[e.keyCode] = true; + break; + case KeyEvent.KEY_UP: + processor.keyUp(e.keyCode); + break; + case KeyEvent.KEY_TYPED: + processor.keyTyped(e.keyChar); + } + usedKeyEvents.free(e); + } + + len = touchEvents.size(); + for (int i = 0; i < len; i++) { + TouchEvent e = touchEvents.get(i); + currentEventTimeStamp = e.timeStamp; + switch (e.type) { + case TouchEvent.TOUCH_DOWN: + processor.touchDown(e.x, e.y, e.pointer, e.button); + justTouched = true; + justPressedButtons[e.button] = true; + break; + case TouchEvent.TOUCH_UP: + processor.touchUp(e.x, e.y, e.pointer, e.button); + break; + case TouchEvent.TOUCH_DRAGGED: + processor.touchDragged(e.x, e.y, e.pointer); + break; + case TouchEvent.TOUCH_CANCELLED: + processor.touchCancelled(e.x, e.y, e.pointer, e.button); + break; + case TouchEvent.TOUCH_MOVED: + processor.mouseMoved(e.x, e.y); + break; + case TouchEvent.TOUCH_SCROLLED: + processor.scrolled(e.scrollAmountX, e.scrollAmountY); + } + usedTouchEvents.free(e); + } + } else { + int len = touchEvents.size(); + for (int i = 0; i < len; i++) { + TouchEvent e = touchEvents.get(i); + if (e.type == TouchEvent.TOUCH_DOWN) justTouched = true; + usedTouchEvents.free(e); + } + + len = keyEvents.size(); + for (int i = 0; i < len; i++) { + usedKeyEvents.free(keyEvents.get(i)); + } + } + + if (touchEvents.isEmpty()) { + for (int i = 0; i < deltaX.length; i++) { + deltaX[0] = 0; + deltaY[0] = 0; + } + } + + keyEvents.clear(); + touchEvents.clear(); + } + } + + boolean requestFocus = true; + + @Override + public boolean onTouch (View view, MotionEvent event) { + if (requestFocus && view != null) { + view.setFocusableInTouchMode(true); + view.requestFocus(); + requestFocus = false; + } + + // synchronized in handler.postTouchEvent() + touchHandler.onTouch(event, this); + + if (sleepTime != 0) { + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + } + } + return true; + } + +// TODO Seems unused. Delete when confirmed. +// /** Called in {@link AndroidLiveWallpaperService} on tap +// * @param x +// * @param y */ +// public void onTap (int x, int y) { +// postTap(x, y); +// } +// +// /** Called in {@link AndroidLiveWallpaperService} on drop +// * @param x +// * @param y */ +// public void onDrop (int x, int y) { +// postTap(x, y); +// } +// +// protected void postTap (int x, int y) { +// synchronized (this) { +// TouchEvent event = usedTouchEvents.obtain(); +// event.timeStamp = System.nanoTime(); +// event.pointer = 0; +// event.x = x; +// event.y = y; +// event.type = TouchEvent.TOUCH_DOWN; +// touchEvents.add(event); +// +// event = usedTouchEvents.obtain(); +// event.timeStamp = System.nanoTime(); +// event.pointer = 0; +// event.x = x; +// event.y = y; +// event.type = TouchEvent.TOUCH_UP; +// touchEvents.add(event); +// } +// Gdx.app.getGraphics().requestRendering(); +// } + + @Override + public boolean onKey (View v, int keyCode, android.view.KeyEvent e) { + for (int i = 0, n = keyListeners.size(); i < n; i++) + if (keyListeners.get(i).onKey(v, keyCode, e)) return true; + + // If the key is held sufficiently long that it repeats, then the initial down is followed + // additional key events with ACTION_DOWN and a non-zero value for getRepeatCount(). + // We are only interested in the first key down event here and must ignore all others + if (e.getAction() == android.view.KeyEvent.ACTION_DOWN && e.getRepeatCount() > 0) return isCatchKey(keyCode); + + synchronized (this) { + KeyEvent event = null; + + if (e.getKeyCode() == android.view.KeyEvent.KEYCODE_UNKNOWN && e.getAction() == android.view.KeyEvent.ACTION_MULTIPLE) { + String chars = e.getCharacters(); + for (int i = 0; i < chars.length(); i++) { + event = usedKeyEvents.obtain(); + event.timeStamp = System.nanoTime(); + event.keyCode = 0; + event.keyChar = chars.charAt(i); + event.type = KeyEvent.KEY_TYPED; + keyEvents.add(event); + } + return false; + } + + char character = (char)e.getUnicodeChar(); + // Android doesn't report a unicode char for back space. hrm... + if (keyCode == 67) character = '\b'; + if (e.getKeyCode() < 0 || e.getKeyCode() > Keys.MAX_KEYCODE) { + return false; + } + + switch (e.getAction()) { + case android.view.KeyEvent.ACTION_DOWN: + event = usedKeyEvents.obtain(); + event.timeStamp = System.nanoTime(); + event.keyChar = 0; + event.keyCode = e.getKeyCode(); + event.type = KeyEvent.KEY_DOWN; + + // Xperia hack for circle key. gah... + if (keyCode == android.view.KeyEvent.KEYCODE_BACK && e.isAltPressed()) { + keyCode = Keys.BUTTON_CIRCLE; + event.keyCode = keyCode; + } + + keyEvents.add(event); + if (!pressedKeys[event.keyCode]) { + pressedKeyCount++; + pressedKeys[event.keyCode] = true; + } + break; + case android.view.KeyEvent.ACTION_UP: + long timeStamp = System.nanoTime(); + event = usedKeyEvents.obtain(); + event.timeStamp = timeStamp; + event.keyChar = 0; + event.keyCode = e.getKeyCode(); + event.type = KeyEvent.KEY_UP; + // Xperia hack for circle key. gah... + if (keyCode == android.view.KeyEvent.KEYCODE_BACK && e.isAltPressed()) { + keyCode = Keys.BUTTON_CIRCLE; + event.keyCode = keyCode; + } + keyEvents.add(event); + + event = usedKeyEvents.obtain(); + event.timeStamp = timeStamp; + event.keyChar = character; + event.keyCode = 0; + event.type = KeyEvent.KEY_TYPED; + keyEvents.add(event); + + if (keyCode == Keys.BUTTON_CIRCLE) { + if (pressedKeys[Keys.BUTTON_CIRCLE]) { + pressedKeyCount--; + pressedKeys[Keys.BUTTON_CIRCLE] = false; + } + } else { + if (pressedKeys[e.getKeyCode()]) { + pressedKeyCount--; + pressedKeys[e.getKeyCode()] = false; + } + } + } + app.getGraphics().requestRendering(); + } + + return isCatchKey(keyCode); + } + + @Override + public void setOnscreenKeyboardVisible (final boolean visible) { + setOnscreenKeyboardVisible(visible, OnscreenKeyboardType.Default); + } + + private boolean onscreenVisible = false; + + @Override + public void setOnscreenKeyboardVisible (final boolean visible, final OnscreenKeyboardType type) { + if (isNativeInputOpen()) throw new GdxRuntimeException("Can't open keyboard if already open"); + onscreenVisible = visible; + handle.post(new Runnable() { + public void run () { + InputMethodManager manager = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (visible) { + View view = ((AndroidGraphics)app.getGraphics()).getView(); + OnscreenKeyboardType tmp = type == null ? OnscreenKeyboardType.Default : type; + if (((GLSurfaceView20)view).onscreenKeyboardType != tmp) { + ((GLSurfaceView20)view).onscreenKeyboardType = tmp; + manager.restartInput(view); + } + + view.setFocusable(true); + view.setFocusableInTouchMode(true); + manager.showSoftInput(((AndroidGraphics)app.getGraphics()).getView(), 0); + } else { + manager.hideSoftInputFromWindow(((AndroidGraphics)app.getGraphics()).getView().getWindowToken(), 0); + } + } + }); + } + + private RelativeLayout relativeLayoutField = null; + private TextInputWrapper textInputWrapper; + + private int getSoftButtonsBarHeight () { + AndroidApplication androidApplication = (AndroidApplication)Gdx.app; + + DisplayMetrics metrics = new DisplayMetrics(); + androidApplication.getWindowManager().getDefaultDisplay().getMetrics(metrics); + int usableHeight = metrics.heightPixels; + int sdkVersion = android.os.Build.VERSION.SDK_INT; + if (sdkVersion < 17) return usableHeight; + + androidApplication.getWindowManager().getDefaultDisplay().getRealMetrics(metrics); + int realHeight = metrics.heightPixels; + + if (realHeight > usableHeight) { + return realHeight - usableHeight; + } + + return 0; + } + + @Override + public void onKeyboardHeightChanged (int height, int leftInset, int rightInset, int orientation) { + KeyboardHeightProvider keyboardHeightProvider = ((AndroidApplication)app).getKeyboardHeightProvider(); + boolean isStandardHeightProvider = keyboardHeightProvider instanceof StandardKeyboardHeightProvider; + if (config.useImmersiveMode && isStandardHeightProvider) { + height += getSoftButtonsBarHeight(); + } + + if (!isNativeInputOpen()) { + if (observer != null) observer.onKeyboardHeightChanged(height); + return; + } + + if (height == 0) { + // Don't close keyboard on floating keyboards + if (!isStandardHeightProvider && (keyboardHeightProvider.getKeyboardLandscapeHeight() != 0 + || keyboardHeightProvider.getKeyboardPortraitHeight() != 0)) { + closeTextInputField(false); + } + // What should I say at this point, everything is busted on android + if (isStandardHeightProvider && getEditTextForNativeInput().isPopupShowing()) { + return; + } + if (observer != null) observer.onKeyboardHeightChanged(0); + relativeLayoutField.setY(0); + return; + } + if (observer != null) observer.onKeyboardHeightChanged(height + getEditTextForNativeInput().getHeight()); + // This is weird, if I don't do that there is a weird scaling/position error after rotating the 2. time + relativeLayoutField.setX(0); + relativeLayoutField.setScaleX(1); + relativeLayoutField.setY(0); + // @off + if ((((Activity)context).getWindow().getAttributes().softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) != WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) { + height = 0; + } + relativeLayoutField.animate() + .y(-height) + .scaleX(((float) Gdx.graphics.getWidth() - rightInset - leftInset) / Gdx.graphics.getWidth()) + .x((float) (leftInset - rightInset) / 2) + .setDuration(100) + .setListener(new Animator.AnimatorListener() { + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) {} + + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationEnd(Animator animation) { + if (getEditTextForNativeInput().isPopupShowing()) { + // In case it gets reopened + getEditTextForNativeInput().showDropDown(); + } + } + }); + + // @on + } + + private void createDefaultEditText () { + // TODO: 07.10.2024 This should probably just get the content/root view instead + View view = ((AndroidGraphics)app.getGraphics()).getView(); + ViewGroup frameLayout = (ViewGroup)view.getParent(); + final RelativeLayout relativeLayout = new RelativeLayout(context); + relativeLayout.setGravity(Gravity.BOTTOM); + // Why? Why isn't it working without? + relativeLayout.setBackgroundColor(Color.TRANSPARENT); + + final AutoCompleteTextView editText = new AutoCompleteTextView(context) { + + private int count = 0; + + @Override + public void onFilterComplete (int count) { + this.count = count; + super.onFilterComplete(count); + } + + @Override + public void showDropDown () { + int size = 165 * count; + if (size > relativeLayout.getHeight() + relativeLayout.getY() - getHeight()) + size = (int)(relativeLayout.getHeight() + relativeLayout.getY() - getHeight()); + if (size > 0) setDropDownHeight(size); + setDropDownVerticalOffset(-getDropDownHeight() - getHeight()); + setDropDownWidth((int)(getWidth() * relativeLayout.getScaleX())); + super.showDropDown(); + } + + @Override + public boolean onKeyPreIme (int keyCode, android.view.KeyEvent event) { + if (event.getKeyCode() == android.view.KeyEvent.KEYCODE_BACK) { + Gdx.input.closeTextInputField(false); + } + return super.onKeyPreIme(keyCode, event); + } + + @Override + public InputConnection onCreateInputConnection (EditorInfo outAttrs) { + return new InputConnectionWrapper(super.onCreateInputConnection(outAttrs), true) { + + // Why? Is this correct handling? I mean, this can't be right! Why shouldn't it work out of the box? + // This is needed for multiline delete + @Override + public boolean sendKeyEvent (android.view.KeyEvent event) { + if (multiline && event.getAction() == android.view.KeyEvent.ACTION_DOWN) { + if (event.getKeyCode() == android.view.KeyEvent.KEYCODE_DEL) { + super.deleteSurroundingText(1, 0); + return true; + } else if (event.getKeyCode() == android.view.KeyEvent.KEYCODE_ENTER) { + commitText("\n", 0); + return true; + } + } + + return super.sendKeyEvent(event); + } + }; + } + }; + + RelativeLayout.LayoutParams editTextParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); + + editText.setLayoutParams(editTextParams); + + relativeLayout.setVisibility(View.INVISIBLE); + relativeLayout.addView(editText); + relativeLayout.requestLayout(); + + frameLayout.addView(relativeLayout); + relativeLayoutField = relativeLayout; + } + + private boolean isNativeInputOpen () { + return relativeLayoutField != null && relativeLayoutField.getVisibility() == View.VISIBLE; + } + + private AutoCompleteTextView getEditTextForNativeInput () { + return (AutoCompleteTextView)relativeLayoutField.getChildAt(0); + } + + private boolean multiline; + + @Override + public void openTextInputField (final NativeInputConfiguration configuration) { + configuration.validate(); + if (isNativeInputOpen()) { + if (closeTriggered) { + while (closeTriggered) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + return; + } + } + } else { + throw new GdxRuntimeException("Can't open keyboard if already open with openTextInputField"); + } + } + if (onscreenVisible) throw new GdxRuntimeException("Can't open keyboard if already open with setOnscreenKeyboardVisible"); + + textInputWrapper = configuration.getTextInputWrapper(); + multiline = configuration.isMultiLine(); + handle.post(new Runnable() { + public void run () { + if (relativeLayoutField == null) createDefaultEditText(); + final AutoCompleteTextView editText = getEditTextForNativeInput(); + if (isNativeInputOpen()) return; + + InputMethodManager manager = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + + // Potential cleanup + if (relativeLayoutField.getChildCount() > 1) + relativeLayoutField.removeViews(1, relativeLayoutField.getChildCount() - 1); + + editText.setOnEditorActionListener(new OnEditorActionListener() { + @Override + public boolean onEditorAction (TextView textView, int actionId, android.view.KeyEvent keyEvent) { + if (actionId == EditorInfo.IME_ACTION_DONE) { + Gdx.input.closeTextInputField(true); + return true; + } + return true; + } + }); + + // Needs to be done first, for some reason... + if (configuration.getType() != OnscreenKeyboardType.Password) { + editText.setTransformationMethod(null); + } + + editText.setInputType(getAndroidInputType(configuration.getType(), false)); + + if (configuration.isPreventCorrection()) { + editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + editText.setInputType(editText.getInputType() & ~InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); + } else { + editText.setInputType( + editText.getInputType() | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT); + } + + editText.setImeOptions(EditorInfo.IME_ACTION_DONE); + if (configuration.isMultiLine()) { + editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + editText.setImeOptions(editText.getImeOptions() | EditorInfo.IME_FLAG_NO_FULLSCREEN); + // For cursor control support + editText.setWidth(Gdx.graphics.getWidth()); + editText.setLines(3); + } else { + editText.setImeOptions(editText.getImeOptions() | EditorInfo.IME_FLAG_NO_FULLSCREEN); + editText.setSingleLine(); + } + // Reset filters to not run into a issue, where the max length filter messes with setText + // But, we can't set the correct filters here, because that leads to problems for some apparent reason nobody will + // ever understand + editText.setFilters(new InputFilter[] {}); + editText.setText(textInputWrapper.getText()); + editText.setHint(configuration.getPlaceholder()); + + InputFilter filter = new InputFilter() { + @Override + public CharSequence filter (CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + boolean keepOriginal = true; + StringBuilder sb = new StringBuilder(end - start); + for (int i = start; i < end; i++) { + char c = source.charAt(i); + // TODO: 02.08.2022 There is a backend incosistenty between iOS and android. On Autocomplete + // iOS would delete whole words, while android only deletes characters. We should make it + // consistent. However that seems not that trivial and it first needs to be decided, which + // behavior the correct one is. + if (configuration.getValidator() == null || configuration.getValidator().validate(c + "")) + sb.append(c); + else + keepOriginal = false; + } + if (keepOriginal) + return null; + else { + if (source instanceof Spanned) { + SpannableString sp = new SpannableString(sb); + TextUtils.copySpansFrom((Spanned)source, start, sb.length(), null, sp, 0); + return sp; + } else { + return sb; + } + } + } + }; + InputFilter[] filters = new InputFilter[] {filter}; + if (configuration.getMaxLength() != null) { + filters = new InputFilter[] {filter, new LengthFilter(configuration.getMaxLength())}; + } + + editText.setFilters(filters); + + if (configuration.getAutoComplete() != null) { + ArrayAdapter adapter = new ArrayAdapter<>(context, android.R.layout.simple_dropdown_item_1line, + configuration.getAutoComplete()); + editText.setAdapter(adapter); + } else { + editText.setAdapter(null); + } + + editText.setBackgroundColor(Color.WHITE); + + if (configuration.getType() == OnscreenKeyboardType.Password) { + // For some reason this needs to be done last, otherwise it won't work + editText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + if (configuration.isShowPasswordButton()) { + final ImageView imageView = new ImageView(context); + + imageView.setImageResource(context.getResources().getIdentifier("design_ic_visibility", "drawable", context.getPackageName())); + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); + params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + params.rightMargin = 10; + params.height = editText.getHeight(); + params.width = editText.getHeight(); + + imageView.setLayoutParams(params); + imageView.setOnClickListener(new View.OnClickListener() { + private boolean isHidding = true; + + @Override + public void onClick (View v) { + int start = editText.getSelectionStart(); + int end = editText.getSelectionStart(); + isHidding = !isHidding; + if (isHidding) { + editText.setTransformationMethod(PasswordTransformationMethod.getInstance()); + imageView.setImageResource(context.getResources().getIdentifier("design_ic_visibility", "drawable", context.getPackageName())); + } else { + editText.setTransformationMethod(null); + imageView.setImageResource(context.getResources().getIdentifier("design_ic_visibility_off", "drawable", context.getPackageName())); + } + // Seems to get reset by "setTransformationMethod" + editText.setSelection(start, end); + } + }); + imageView.setAlpha(0.5f); + imageView.setPadding(5, 5, 5, 5); + relativeLayoutField.addView(imageView); + } + } + + // One wonders why here? I don't know! + editText.setSelection(textInputWrapper.getSelectionStart(), textInputWrapper.getSelectionEnd()); + + relativeLayoutField.setVisibility(View.VISIBLE); + + editText.requestFocus(); + manager.showSoftInput(editText, 0); + } + }); + } + + // Due to the lots of threads in android, we need to use this as a lock to wait with openTextInputField until close has + // finished + boolean closeTriggered = false; + + @Override + public void closeTextInputField (final boolean sendReturn) { + if (closeTriggered) return; + if (!isNativeInputOpen()) return; + closeTriggered = true; + handle.post(new Runnable() { + @Override + public void run () { + if (!isNativeInputOpen()) { + closeTriggered = false; + return; + } + final View view = ((AndroidGraphics)app.getGraphics()).getView(); + view.requestFocus(); + EditText editText = getEditTextForNativeInput(); + final String text = editText.getText().toString(); + final int selection = editText.getSelectionStart(); + Gdx.app.postRunnable(new Runnable() { + TextInputWrapper wrapper = textInputWrapper; + + @Override + public void run () { + wrapper.setText(text); + wrapper.setPosition(selection); + if (sendReturn) { + getInputProcessor().keyDown(Keys.ENTER); + getInputProcessor().keyTyped((char)13); + } + + // This is getting ridiculous... + Gdx.app.postRunnable(new Runnable() { + @Override + public void run () { + if (wrapper.shouldClose()) { + handle.post(new Runnable() { + @Override + public void run () { + InputMethodManager manager = (InputMethodManager)context + .getSystemService(Context.INPUT_METHOD_SERVICE); + manager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + }); + } + } + + }); + } + }); + if (relativeLayoutField.getChildCount() > 1) + relativeLayoutField.removeViews(1, relativeLayoutField.getChildCount() - 1); + relativeLayoutField.setVisibility(View.INVISIBLE); + closeTriggered = false; + } + }); + } + + private KeyboardHeightObserver observer; + + @Override + public void setKeyboardHeightObserver (KeyboardHeightObserver observer) { + this.observer = observer; + } + + @Override + public void vibrate (int milliseconds) { + haptics.vibrate(milliseconds); + } + + @Override + public void vibrate (int milliseconds, boolean fallback) { + haptics.vibrate(milliseconds); + } + + @Override + public void vibrate (int milliseconds, int amplitude, boolean fallback) { + haptics.vibrate(milliseconds, amplitude, fallback); + } + + @Override + public void vibrate (VibrationType vibrationType) { + haptics.vibrate(vibrationType); + } + + @Override + public boolean justTouched () { + return justTouched; + } + + @Override + public boolean isButtonPressed (int button) { + synchronized (this) { + if (hasMultitouch) { + for (int pointer = 0; pointer < NUM_TOUCHES; pointer++) { + if (touched[pointer] && (this.button[pointer] == button)) { + return true; + } + } + } + return (touched[0] && (this.button[0] == button)); + } + } + + @Override + public boolean isButtonJustPressed (int button) { + if (button < 0 || button > NUM_TOUCHES) return false; + return justPressedButtons[button]; + } + + final float[] R = new float[9]; + final float[] orientation = new float[3]; + + private void updateOrientation () { + if (rotationVectorAvailable) { + SensorManager.getRotationMatrixFromVector(R, rotationVectorValues); + } else if (!SensorManager.getRotationMatrix(R, null, accelerometerValues, magneticFieldValues)) { + return; // compass + accelerometer in free fall + } + SensorManager.getOrientation(R, orientation); + azimuth = (float)Math.toDegrees(orientation[0]); + pitch = (float)Math.toDegrees(orientation[1]); + roll = (float)Math.toDegrees(orientation[2]); + } + + /** Returns the rotation matrix describing the devices rotation as per + * SensorManager#getRotationMatrix(float[], float[], float[], float[]). Does not manipulate the matrix + * if the platform does not have an accelerometer and compass, or a rotation vector sensor. + * @param matrix */ + public void getRotationMatrix (float[] matrix) { + if (rotationVectorAvailable) + SensorManager.getRotationMatrixFromVector(matrix, rotationVectorValues); + else // compass + accelerometer + SensorManager.getRotationMatrix(matrix, null, accelerometerValues, magneticFieldValues); + } + + @Override + public float getAzimuth () { + if (!compassAvailable && !rotationVectorAvailable) return 0; + + updateOrientation(); + return azimuth; + } + + @Override + public float getPitch () { + if (!compassAvailable && !rotationVectorAvailable) return 0; + + updateOrientation(); + return pitch; + } + + @Override + public float getRoll () { + if (!compassAvailable && !rotationVectorAvailable) return 0; + + updateOrientation(); + return roll; + } + + void registerSensorListeners () { + if (config.useAccelerometer) { + manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + if (manager.getSensorList(Sensor.TYPE_ACCELEROMETER).isEmpty()) { + accelerometerAvailable = false; + } else { + Sensor accelerometer = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0); + accelerometerListener = new SensorListener(); + accelerometerAvailable = manager.registerListener(accelerometerListener, accelerometer, config.sensorDelay); + } + } else + accelerometerAvailable = false; + + if (config.useGyroscope) { + manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + if (manager.getSensorList(Sensor.TYPE_GYROSCOPE).isEmpty()) { + gyroscopeAvailable = false; + } else { + Sensor gyroscope = manager.getSensorList(Sensor.TYPE_GYROSCOPE).get(0); + gyroscopeListener = new SensorListener(); + gyroscopeAvailable = manager.registerListener(gyroscopeListener, gyroscope, config.sensorDelay); + } + } else + gyroscopeAvailable = false; + + rotationVectorAvailable = false; + if (config.useRotationVectorSensor) { + if (manager == null) manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + List rotationVectorSensors = manager.getSensorList(Sensor.TYPE_ROTATION_VECTOR); + if (!rotationVectorSensors.isEmpty()) { + rotationVectorListener = new SensorListener(); + for (Sensor sensor : rotationVectorSensors) { // favor AOSP sensor + if (sensor.getVendor().equals("Google Inc.") && sensor.getVersion() == 3) { + rotationVectorAvailable = manager.registerListener(rotationVectorListener, sensor, config.sensorDelay); + break; + } + } + if (!rotationVectorAvailable) rotationVectorAvailable = manager.registerListener(rotationVectorListener, + rotationVectorSensors.get(0), config.sensorDelay); + } + } + + if (config.useCompass && !rotationVectorAvailable) { + if (manager == null) manager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE); + Sensor sensor = manager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + if (sensor != null) { + compassAvailable = accelerometerAvailable; + if (compassAvailable) { + compassListener = new SensorListener(); + compassAvailable = manager.registerListener(compassListener, sensor, config.sensorDelay); + } + } else { + compassAvailable = false; + } + } else + compassAvailable = false; + Gdx.app.log("AndroidInput", "sensor listener setup"); + } + + void unregisterSensorListeners () { + if (manager != null) { + if (accelerometerListener != null) { + manager.unregisterListener(accelerometerListener); + accelerometerListener = null; + } + if (gyroscopeListener != null) { + manager.unregisterListener(gyroscopeListener); + gyroscopeListener = null; + } + if (rotationVectorListener != null) { + manager.unregisterListener(rotationVectorListener); + rotationVectorListener = null; + } + if (compassListener != null) { + manager.unregisterListener(compassListener); + compassListener = null; + } + manager = null; + } + Gdx.app.log("AndroidInput", "sensor listener tear down"); + } + + @Override + public InputProcessor getInputProcessor () { + return this.processor; + } + + @Override + public boolean isPeripheralAvailable (Peripheral peripheral) { + if (peripheral == Peripheral.Accelerometer) return accelerometerAvailable; + if (peripheral == Peripheral.Gyroscope) return gyroscopeAvailable; + if (peripheral == Peripheral.Compass) return compassAvailable; + if (peripheral == Peripheral.HardwareKeyboard) return keyboardAvailable; + if (peripheral == Peripheral.OnscreenKeyboard) return true; + if (peripheral == Peripheral.Vibrator) return haptics.hasVibratorAvailable(); + if (peripheral == Peripheral.HapticFeedback) return haptics.hasHapticsSupport(); + if (peripheral == Peripheral.MultitouchScreen) return hasMultitouch; + if (peripheral == Peripheral.RotationVector) return rotationVectorAvailable; + if (peripheral == Peripheral.Pressure) return true; + return false; + } + + public int getFreePointerIndex () { + int len = realId.length; + for (int i = 0; i < len; i++) { + if (realId[i] == -1) return i; + } + + pressure = resize(pressure); + realId = resize(realId); + touchX = resize(touchX); + touchY = resize(touchY); + deltaX = resize(deltaX); + deltaY = resize(deltaY); + touched = resize(touched); + button = resize(button); + + return len; + } + + private int[] resize (int[] orig) { + int[] tmp = new int[orig.length + 2]; + System.arraycopy(orig, 0, tmp, 0, orig.length); + return tmp; + } + + private boolean[] resize (boolean[] orig) { + boolean[] tmp = new boolean[orig.length + 2]; + System.arraycopy(orig, 0, tmp, 0, orig.length); + return tmp; + } + + private float[] resize (float[] orig) { + float[] tmp = new float[orig.length + 2]; + System.arraycopy(orig, 0, tmp, 0, orig.length); + return tmp; + } + + public int lookUpPointerIndex (int pointerId) { + int len = realId.length; + for (int i = 0; i < len; i++) { + if (realId[i] == pointerId) return i; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + sb.append(i + ":" + realId[i] + " "); + } + Gdx.app.log("AndroidInput", "Pointer ID lookup failed: " + pointerId + ", " + sb.toString()); + return -1; + } + + @Override + public int getRotation () { + int orientation = 0; + + if (context instanceof Activity) { + orientation = ((Activity)context).getWindowManager().getDefaultDisplay().getRotation(); + } else { + orientation = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation(); + } + + switch (orientation) { + case Surface.ROTATION_0: + return 0; + case Surface.ROTATION_90: + return 90; + case Surface.ROTATION_180: + return 180; + case Surface.ROTATION_270: + return 270; + default: + return 0; + } + } + + @Override + public Orientation getNativeOrientation () { + return nativeOrientation; + } + + @Override + public void setCursorCatched (boolean catched) { + } + + @Override + public boolean isCursorCatched () { + return false; + } + + @Override + public int getDeltaX () { + return deltaX[0]; + } + + @Override + public int getDeltaX (int pointer) { + return deltaX[pointer]; + } + + @Override + public int getDeltaY () { + return deltaY[0]; + } + + @Override + public int getDeltaY (int pointer) { + return deltaY[pointer]; + } + + @Override + public void setCursorPosition (int x, int y) { + } + + @Override + public long getCurrentEventTime () { + return currentEventTimeStamp; + } + + @Override + public void addKeyListener (OnKeyListener listener) { + keyListeners.add(listener); + } + + @Override + public boolean onGenericMotion (View view, MotionEvent event) { + if (mouseHandler.onGenericMotion(event, this)) return true; + for (int i = 0, n = genericMotionListeners.size(); i < n; i++) + if (genericMotionListeners.get(i).onGenericMotion(view, event)) return true; + return false; + } + + @Override + public void addGenericMotionListener (OnGenericMotionListener listener) { + genericMotionListeners.add(listener); + } + + @Override + public void onPause () { + unregisterSensorListeners(); + } + + @Override + public void onResume () { + registerSensorListeners(); + } + + @Override + public void onDreamingStarted () { + registerSensorListeners(); + } + + @Override + public void onDreamingStopped () { + unregisterSensorListeners(); + } + + @Override + public void setCatchKey (int keycode, boolean catchKey) { + super.setCatchKey(keycode, catchKey); + if (keycode == Keys.BACK && predictiveBackHandler != null) { + if (catchKey) + predictiveBackHandler.register(); + else + predictiveBackHandler.unregister(); + } + } + + /** Handle predictive back gestures on Android 13 and newer, replacing the BACK key event for exiting the + * activity. + * @see Add support for the + * predictive back gesture - Android Developers */ + @TargetApi(33) + private class PredictiveBackHandler { + + private final OnBackInvokedDispatcher dispatcher = ((Activity)context).getOnBackInvokedDispatcher(); + private final OnBackInvokedCallback callback = new OnBackInvokedCallback() { + @Override + public void onBackInvoked () { + if (processor != null) { + processor.keyDown(Keys.BACK); + } + } + }; + + private void register () { + dispatcher.registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, callback); + } + + private void unregister () { + dispatcher.unregisterOnBackInvokedCallback(callback); + } + + } + + /** Our implementation of SensorEventListener. Because Android doesn't like it when we register more than one Sensor to a + * single SensorEventListener, we add one of these for each Sensor. Could use an anonymous class, but I don't see any harm in + * explicitly defining it here. Correct me if I am wrong. */ + private class SensorListener implements SensorEventListener { + + public SensorListener () { + + } + + @Override + public void onAccuracyChanged (Sensor arg0, int arg1) { + + } + + @Override + public void onSensorChanged (SensorEvent event) { + if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + if (nativeOrientation == Orientation.Portrait) { + System.arraycopy(event.values, 0, accelerometerValues, 0, accelerometerValues.length); + } else { + accelerometerValues[0] = event.values[1]; + accelerometerValues[1] = -event.values[0]; + accelerometerValues[2] = event.values[2]; + } + } + if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { + System.arraycopy(event.values, 0, magneticFieldValues, 0, magneticFieldValues.length); + } + if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { + if (nativeOrientation == Orientation.Portrait) { + System.arraycopy(event.values, 0, gyroscopeValues, 0, gyroscopeValues.length); + } else { + gyroscopeValues[0] = event.values[1]; + gyroscopeValues[1] = -event.values[0]; + gyroscopeValues[2] = event.values[2]; + } + } + if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { + if (nativeOrientation == Orientation.Portrait) { + System.arraycopy(event.values, 0, rotationVectorValues, 0, rotationVectorValues.length); + } else { + rotationVectorValues[0] = event.values[1]; + rotationVectorValues[1] = -event.values[0]; + rotationVectorValues[2] = event.values[2]; + } + } + } + } +} \ No newline at end of file diff --git a/forge-gui-android/src/com/badlogic/gdx/backends/android/surfaceview/GLSurfaceView20.java b/forge-gui-android/src/com/badlogic/gdx/backends/android/surfaceview/GLSurfaceView20.java new file mode 100644 index 00000000000..f250d8a5ef8 --- /dev/null +++ b/forge-gui-android/src/com/badlogic/gdx/backends/android/surfaceview/GLSurfaceView20.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" + * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +package com.badlogic.gdx.backends.android.surfaceview; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import android.content.Context; +import android.graphics.PixelFormat; +import android.opengl.GLSurfaceView; +import android.os.SystemClock; +import android.util.Log; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import com.badlogic.gdx.Input.OnscreenKeyboardType; +import com.badlogic.gdx.backends.android.DefaultAndroidInput; + +/** A simple GLSurfaceView sub-class that demonstrates how to perform OpenGL ES 2.0 rendering into a GL Surface. Note the + * following important details: + *

+ * - The class must use a custom context factory to enable 2.0 rendering. See ContextFactory class definition below. + *

+ * - The class must use a custom EGLConfigChooser to be able to select an EGLConfig that supports 2.0. This is done by providing a + * config specification to eglChooseConfig() that has the attribute EGL10.ELG_RENDERABLE_TYPE containing the EGL_OPENGL_ES2_BIT + * flag set. See ConfigChooser class definition below. + *

+ * - The class must select the surface's format, then choose an EGLConfig that matches it exactly (with regards to + * red/green/blue/alpha channels bit depths). Failure to do so would result in an EGL_BAD_MATCH error. */ +public class GLSurfaceView20 extends GLSurfaceView { + static String TAG = "GL2JNIView"; + private static final boolean DEBUG = false; + + final ResolutionStrategy resolutionStrategy; + static int targetGLESVersion; + public OnscreenKeyboardType onscreenKeyboardType = OnscreenKeyboardType.Default; + + public GLSurfaceView20 (Context context, ResolutionStrategy resolutionStrategy, int targetGLESVersion) { + super(context); + GLSurfaceView20.targetGLESVersion = targetGLESVersion; + this.resolutionStrategy = resolutionStrategy; + init(false, 16, 0); + } + + public GLSurfaceView20 (Context context, ResolutionStrategy resolutionStrategy) { + this(context, resolutionStrategy, 2); + } + + public GLSurfaceView20 (Context context, boolean translucent, int depth, int stencil, ResolutionStrategy resolutionStrategy) { + super(context); + this.resolutionStrategy = resolutionStrategy; + init(translucent, depth, stencil); + + } + + @Override + protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { + ResolutionStrategy.MeasuredDimension measures = resolutionStrategy.calcMeasures(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(measures.width, measures.height); + } + + @Override + public InputConnection onCreateInputConnection (EditorInfo outAttrs) { + + // add this line, the IME can show the selectable words when use chinese input method editor. + if (outAttrs != null) { + outAttrs.imeOptions = outAttrs.imeOptions | EditorInfo.IME_FLAG_NO_EXTRACT_UI; + outAttrs.inputType = DefaultAndroidInput.getAndroidInputType(onscreenKeyboardType, true); + } + + BaseInputConnection connection = new BaseInputConnection(this, false) { + @Override + public boolean deleteSurroundingText (int beforeLength, int afterLength) { + /* + * In Jelly Bean, they don't send key events for delete. Instead, they send beforeLength = 1, afterLength = 0. So, + * we'll just simulate what it used to do. + */ + if (beforeLength == 1 && afterLength == 0) { + sendDownUpKeyEventForBackwardCompatibility(KeyEvent.KEYCODE_DEL); + return true; + } + return super.deleteSurroundingText(beforeLength, afterLength); + } + + private void sendDownUpKeyEventForBackwardCompatibility (final int code) { + final long eventTime = SystemClock.uptimeMillis(); + super.sendKeyEvent(new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, code, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); + super.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, code, 0, 0, + KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); + } + }; + return connection; + } + + @Override + public void onDetachedFromWindow () { + super.onDetachedFromWindow(); + } + + private void init (boolean translucent, int depth, int stencil) { + + /* + * By default, GLSurfaceView() creates a RGB_888 opaque surface. If we want a translucent one, we should change the + * surface's format here, using PixelFormat.TRANSLUCENT for GL Surfaces is interpreted as any 32-bit surface with alpha by + * SurfaceFlinger. + */ + if (translucent) { + this.getHolder().setFormat(PixelFormat.TRANSLUCENT); + } + + /* + * Setup the context factory for 2.0 rendering. See ContextFactory class definition below + */ + setEGLContextFactory(new ContextFactory()); + + /* + * We need to choose an EGLConfig that matches the format of our surface exactly. This is going to be done in our custom + * config chooser. See ConfigChooser class definition below. + */ + setEGLConfigChooser( + translucent ? new ConfigChooser(8, 8, 8, 8, depth, stencil) : new ConfigChooser(8, 8, 8, 0, depth, stencil)); + + /* Set the renderer responsible for frame rendering */ + } + + static class ContextFactory implements GLSurfaceView.EGLContextFactory { + private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + + public EGLContext createContext (EGL10 egl, EGLDisplay display, EGLConfig eglConfig) { + Log.w(TAG, "creating OpenGL ES " + GLSurfaceView20.targetGLESVersion + ".0 context"); + checkEglError("Before eglCreateContext " + targetGLESVersion, egl); + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, GLSurfaceView20.targetGLESVersion, EGL10.EGL_NONE}; + EGLContext context = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + boolean success = checkEglError("After eglCreateContext " + targetGLESVersion, egl); + + if ((!success || context == null) && GLSurfaceView20.targetGLESVersion > 2) { + Log.w(TAG, "Falling back to GLES 2"); + GLSurfaceView20.targetGLESVersion = 2; + return createContext(egl, display, eglConfig); + } + Log.w(TAG, "Returning a GLES " + targetGLESVersion + " context"); + return context; + } + + public void destroyContext (EGL10 egl, EGLDisplay display, EGLContext context) { + egl.eglDestroyContext(display, context); + } + } + + static boolean checkEglError (String prompt, EGL10 egl) { + int error; + boolean result = true; + while ((error = egl.eglGetError()) != EGL10.EGL_SUCCESS) { + result = false; + Log.e(TAG, String.format("%s: EGL error: 0x%x", prompt, error)); + } + return result; + } + + private static class ConfigChooser implements GLSurfaceView.EGLConfigChooser { + + public ConfigChooser (int r, int g, int b, int a, int depth, int stencil) { + mRedSize = r; + mGreenSize = g; + mBlueSize = b; + mAlphaSize = a; + mDepthSize = depth; + mStencilSize = stencil; + } + + /* + * This EGL config specification is used to specify 2.0 rendering. We use a minimum size of 4 bits for red/green/blue, but + * will perform actual matching in chooseConfig() below. + */ + private static int EGL_OPENGL_ES2_BIT = 4; + private static int[] s_configAttribs2 = {EGL10.EGL_RED_SIZE, 4, EGL10.EGL_GREEN_SIZE, 4, EGL10.EGL_BLUE_SIZE, 4, + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL10.EGL_NONE}; + + public EGLConfig chooseConfig (EGL10 egl, EGLDisplay display) { + + /* + * Get the number of minimally matching EGL configurations + */ + int[] num_config = new int[1]; + egl.eglChooseConfig(display, s_configAttribs2, null, 0, num_config); + + int numConfigs = num_config[0]; + + if (numConfigs <= 0) { + throw new IllegalArgumentException("No configs match configSpec"); + } + + /* + * Allocate then read the array of minimally matching EGL configs + */ + EGLConfig[] configs = new EGLConfig[numConfigs]; + egl.eglChooseConfig(display, s_configAttribs2, configs, numConfigs, num_config); + + if (DEBUG) { + printConfigs(egl, display, configs); + } + /* + * Now return the "best" one + */ + return chooseConfig(egl, display, configs); + } + + public EGLConfig chooseConfig (EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); + + // We need at least mDepthSize and mStencilSize bits + if (d < mDepthSize || s < mStencilSize) continue; + + // We want an *exact* match for red/green/blue/alpha + int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); + int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); + + if (r == mRedSize && g == mGreenSize && b == mBlueSize && a == mAlphaSize) return config; + } + return null; + } + + private int findConfigAttrib (EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) { + + if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { + return mValue[0]; + } + return defaultValue; + } + + private void printConfigs (EGL10 egl, EGLDisplay display, EGLConfig[] configs) { + int numConfigs = configs.length; + Log.w(TAG, String.format("%d configurations", numConfigs)); + for (int i = 0; i < numConfigs; i++) { + Log.w(TAG, String.format("Configuration %d:\n", i)); + printConfig(egl, display, configs[i]); + } + } + + private void printConfig (EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attributes = {EGL10.EGL_BUFFER_SIZE, EGL10.EGL_ALPHA_SIZE, EGL10.EGL_BLUE_SIZE, EGL10.EGL_GREEN_SIZE, + EGL10.EGL_RED_SIZE, EGL10.EGL_DEPTH_SIZE, EGL10.EGL_STENCIL_SIZE, EGL10.EGL_CONFIG_CAVEAT, EGL10.EGL_CONFIG_ID, + EGL10.EGL_LEVEL, EGL10.EGL_MAX_PBUFFER_HEIGHT, EGL10.EGL_MAX_PBUFFER_PIXELS, EGL10.EGL_MAX_PBUFFER_WIDTH, + EGL10.EGL_NATIVE_RENDERABLE, EGL10.EGL_NATIVE_VISUAL_ID, EGL10.EGL_NATIVE_VISUAL_TYPE, 0x3030, // EGL10.EGL_PRESERVED_RESOURCES, + EGL10.EGL_SAMPLES, EGL10.EGL_SAMPLE_BUFFERS, EGL10.EGL_SURFACE_TYPE, EGL10.EGL_TRANSPARENT_TYPE, + EGL10.EGL_TRANSPARENT_RED_VALUE, EGL10.EGL_TRANSPARENT_GREEN_VALUE, EGL10.EGL_TRANSPARENT_BLUE_VALUE, 0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB, + 0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA, + 0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL, + 0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL, + EGL10.EGL_LUMINANCE_SIZE, EGL10.EGL_ALPHA_MASK_SIZE, EGL10.EGL_COLOR_BUFFER_TYPE, EGL10.EGL_RENDERABLE_TYPE, 0x3042 // EGL10.EGL_CONFORMANT + }; + String[] names = {"EGL_BUFFER_SIZE", "EGL_ALPHA_SIZE", "EGL_BLUE_SIZE", "EGL_GREEN_SIZE", "EGL_RED_SIZE", + "EGL_DEPTH_SIZE", "EGL_STENCIL_SIZE", "EGL_CONFIG_CAVEAT", "EGL_CONFIG_ID", "EGL_LEVEL", "EGL_MAX_PBUFFER_HEIGHT", + "EGL_MAX_PBUFFER_PIXELS", "EGL_MAX_PBUFFER_WIDTH", "EGL_NATIVE_RENDERABLE", "EGL_NATIVE_VISUAL_ID", + "EGL_NATIVE_VISUAL_TYPE", "EGL_PRESERVED_RESOURCES", "EGL_SAMPLES", "EGL_SAMPLE_BUFFERS", "EGL_SURFACE_TYPE", + "EGL_TRANSPARENT_TYPE", "EGL_TRANSPARENT_RED_VALUE", "EGL_TRANSPARENT_GREEN_VALUE", "EGL_TRANSPARENT_BLUE_VALUE", + "EGL_BIND_TO_TEXTURE_RGB", "EGL_BIND_TO_TEXTURE_RGBA", "EGL_MIN_SWAP_INTERVAL", "EGL_MAX_SWAP_INTERVAL", + "EGL_LUMINANCE_SIZE", "EGL_ALPHA_MASK_SIZE", "EGL_COLOR_BUFFER_TYPE", "EGL_RENDERABLE_TYPE", "EGL_CONFORMANT"}; + int[] value = new int[1]; + for (int i = 0; i < attributes.length; i++) { + int attribute = attributes[i]; + String name = names[i]; + if (egl.eglGetConfigAttrib(display, config, attribute, value)) { + Log.w(TAG, String.format(" %s: %d\n", name, value[0])); + } else { + // Log.w(TAG, String.format(" %s: failed\n", name)); + while (egl.eglGetError() != EGL10.EGL_SUCCESS) + ; + } + } + } + + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + private int[] mValue = new int[1]; + } +} \ No newline at end of file diff --git a/forge-gui-android/src/forge/app/Main.java b/forge-gui-android/src/forge/app/Main.java index a405d43a124..725c1c86285 100644 --- a/forge-gui-android/src/forge/app/Main.java +++ b/forge-gui-android/src/forge/app/Main.java @@ -51,7 +51,7 @@ import android.widget.TableRow; import android.widget.TextView; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Version; -import com.badlogic.gdx.backends.android.ForgeAndroidApplication; +import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import com.badlogic.gdx.backends.android.AndroidAudio; import com.badlogic.gdx.backends.android.AsynchronousAndroidAudio; @@ -73,7 +73,7 @@ import java.text.Normalizer; import java.util.ArrayList; import java.util.Date; -public class Main extends ForgeAndroidApplication { +public class Main extends AndroidApplication { private AndroidAdapter Gadapter; private ArrayList gamepads; private AndroidClipboard androidClipboard; diff --git a/forge-gui/src/main/java/forge/util/ImageFetcher.java b/forge-gui/src/main/java/forge/util/ImageFetcher.java index 2ec71bc3025..516988793c2 100644 --- a/forge-gui/src/main/java/forge/util/ImageFetcher.java +++ b/forge-gui/src/main/java/forge/util/ImageFetcher.java @@ -105,6 +105,8 @@ public abstract class ImageFetcher { setupObserver(destFile.getAbsolutePath(), callback, downloadUrls); return; } + if (imageKey.equalsIgnoreCase("t:null")) + return; boolean useArtCrop = "Crop".equals(FModel.getPreferences().getPref(ForgePreferences.FPref.UI_CARD_ART_FORMAT)); final String prefix = imageKey.substring(0, 2); @@ -195,14 +197,10 @@ public abstract class ImageFetcher { for (PaperCard pc : clones) { if (clones.size() > 1) {//clones only if (!paperCard.getEdition().equalsIgnoreCase(pc.getEdition())) { - StringBuilder set = new StringBuilder(ForgeConstants.URL_PIC_DOWNLOAD); - set.append(ImageUtil.getDownloadUrl(pc, face)); - downloadUrls.add(set.toString()); + downloadUrls.add(ForgeConstants.URL_PIC_DOWNLOAD + ImageUtil.getDownloadUrl(pc, face)); } } else {// original from set - StringBuilder set = new StringBuilder(ForgeConstants.URL_PIC_DOWNLOAD); - set.append(ImageUtil.getDownloadUrl(pc, face)); - downloadUrls.add(set.toString()); + downloadUrls.add(ForgeConstants.URL_PIC_DOWNLOAD + ImageUtil.getDownloadUrl(pc, face)); } } } @@ -228,6 +226,8 @@ public abstract class ImageFetcher { CardEdition E = StaticData.instance().getEditions().get(tempdata[1]); if (E != null && E.getType() == CardEdition.Type.CUSTOM_SET) return; //Custom set token, skip fetching. } + if (filename.equalsIgnoreCase("null.jpg")) + return; System.err.println("No specified file for '" + filename + "'.. Attempting to download from default Url"); tokenUrl = String.format("%s%s", ForgeConstants.URL_TOKEN_DOWNLOAD, filename); }