|
@@ -15,13 +15,9 @@ import android.content.pm.PackageManager;
|
|
|
import android.content.res.Configuration;
|
|
|
import android.graphics.Bitmap;
|
|
|
import android.graphics.Color;
|
|
|
-import android.graphics.PixelFormat;
|
|
|
import android.graphics.PorterDuff;
|
|
|
import android.graphics.drawable.Drawable;
|
|
|
import android.hardware.Sensor;
|
|
|
-import android.hardware.SensorEvent;
|
|
|
-import android.hardware.SensorEventListener;
|
|
|
-import android.hardware.SensorManager;
|
|
|
import android.net.Uri;
|
|
|
import android.os.Build;
|
|
|
import android.os.Bundle;
|
|
@@ -37,11 +33,8 @@ import android.view.Display;
|
|
|
import android.view.Gravity;
|
|
|
import android.view.InputDevice;
|
|
|
import android.view.KeyEvent;
|
|
|
-import android.view.MotionEvent;
|
|
|
import android.view.PointerIcon;
|
|
|
import android.view.Surface;
|
|
|
-import android.view.SurfaceHolder;
|
|
|
-import android.view.SurfaceView;
|
|
|
import android.view.View;
|
|
|
import android.view.ViewGroup;
|
|
|
import android.view.Window;
|
|
@@ -314,6 +307,10 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|
|
mNextNativeState = NativeState.INIT;
|
|
|
mCurrentNativeState = NativeState.INIT;
|
|
|
}
|
|
|
+
|
|
|
+ protected SDLSurface createSDLSurface(Context context) {
|
|
|
+ return new SDLSurface(context);
|
|
|
+ }
|
|
|
|
|
|
// Setup
|
|
|
@Override
|
|
@@ -382,7 +379,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
|
|
|
mHIDDeviceManager = HIDDeviceManager.acquire(this);
|
|
|
|
|
|
// Set up the surface
|
|
|
- mSurface = new SDLSurface(getApplication());
|
|
|
+ mSurface = createSDLSurface(getApplication());
|
|
|
|
|
|
mLayout = new RelativeLayout(this);
|
|
|
mLayout.addView(mSurface);
|
|
@@ -1809,455 +1806,6 @@ class SDLMain implements Runnable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-/**
|
|
|
- SDLSurface. This is what we draw on, so we need to know when it's created
|
|
|
- in order to do anything useful.
|
|
|
-
|
|
|
- Because of this, that's where we set up the SDL thread
|
|
|
-*/
|
|
|
-class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
|
|
- View.OnKeyListener, View.OnTouchListener, SensorEventListener {
|
|
|
-
|
|
|
- // Sensors
|
|
|
- protected SensorManager mSensorManager;
|
|
|
- protected Display mDisplay;
|
|
|
-
|
|
|
- // Keep track of the surface size to normalize touch events
|
|
|
- protected float mWidth, mHeight;
|
|
|
-
|
|
|
- // Is SurfaceView ready for rendering
|
|
|
- public boolean mIsSurfaceReady;
|
|
|
-
|
|
|
- // Startup
|
|
|
- public SDLSurface(Context context) {
|
|
|
- super(context);
|
|
|
- getHolder().addCallback(this);
|
|
|
-
|
|
|
- setFocusable(true);
|
|
|
- setFocusableInTouchMode(true);
|
|
|
- requestFocus();
|
|
|
- setOnKeyListener(this);
|
|
|
- setOnTouchListener(this);
|
|
|
-
|
|
|
- mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
|
|
- mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
|
|
-
|
|
|
- setOnGenericMotionListener(SDLActivity.getMotionListener());
|
|
|
-
|
|
|
- // Some arbitrary defaults to avoid a potential division by zero
|
|
|
- mWidth = 1.0f;
|
|
|
- mHeight = 1.0f;
|
|
|
-
|
|
|
- mIsSurfaceReady = false;
|
|
|
- }
|
|
|
-
|
|
|
- public void handlePause() {
|
|
|
- enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
|
|
- }
|
|
|
-
|
|
|
- public void handleResume() {
|
|
|
- setFocusable(true);
|
|
|
- setFocusableInTouchMode(true);
|
|
|
- requestFocus();
|
|
|
- setOnKeyListener(this);
|
|
|
- setOnTouchListener(this);
|
|
|
- enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
|
|
- }
|
|
|
-
|
|
|
- public Surface getNativeSurface() {
|
|
|
- return getHolder().getSurface();
|
|
|
- }
|
|
|
-
|
|
|
- // Called when we have a valid drawing surface
|
|
|
- @Override
|
|
|
- public void surfaceCreated(SurfaceHolder holder) {
|
|
|
- Log.v("SDL", "surfaceCreated()");
|
|
|
- SDLActivity.onNativeSurfaceCreated();
|
|
|
- }
|
|
|
-
|
|
|
- // Called when we lose the surface
|
|
|
- @Override
|
|
|
- public void surfaceDestroyed(SurfaceHolder holder) {
|
|
|
- Log.v("SDL", "surfaceDestroyed()");
|
|
|
-
|
|
|
- // Transition to pause, if needed
|
|
|
- SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
|
|
- SDLActivity.handleNativeState();
|
|
|
-
|
|
|
- mIsSurfaceReady = false;
|
|
|
- SDLActivity.onNativeSurfaceDestroyed();
|
|
|
- }
|
|
|
-
|
|
|
- // Called when the surface is resized
|
|
|
- @Override
|
|
|
- public void surfaceChanged(SurfaceHolder holder,
|
|
|
- int format, int width, int height) {
|
|
|
- Log.v("SDL", "surfaceChanged()");
|
|
|
-
|
|
|
- if (SDLActivity.mSingleton == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- mWidth = width;
|
|
|
- mHeight = height;
|
|
|
- int nDeviceWidth = width;
|
|
|
- int nDeviceHeight = height;
|
|
|
- try
|
|
|
- {
|
|
|
- if (Build.VERSION.SDK_INT >= 17) {
|
|
|
- DisplayMetrics realMetrics = new DisplayMetrics();
|
|
|
- mDisplay.getRealMetrics( realMetrics );
|
|
|
- nDeviceWidth = realMetrics.widthPixels;
|
|
|
- nDeviceHeight = realMetrics.heightPixels;
|
|
|
- }
|
|
|
- } catch(Exception ignored) {
|
|
|
- }
|
|
|
-
|
|
|
- synchronized(SDLActivity.getContext()) {
|
|
|
- // In case we're waiting on a size change after going fullscreen, send a notification.
|
|
|
- SDLActivity.getContext().notifyAll();
|
|
|
- }
|
|
|
-
|
|
|
- Log.v("SDL", "Window size: " + width + "x" + height);
|
|
|
- Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
|
|
- SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
|
|
|
- SDLActivity.onNativeResize();
|
|
|
-
|
|
|
- // Prevent a screen distortion glitch,
|
|
|
- // for instance when the device is in Landscape and a Portrait App is resumed.
|
|
|
- boolean skip = false;
|
|
|
- int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
|
|
-
|
|
|
- if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
|
|
- if (mWidth > mHeight) {
|
|
|
- skip = true;
|
|
|
- }
|
|
|
- } else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
|
|
- if (mWidth < mHeight) {
|
|
|
- skip = true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Special Patch for Square Resolution: Black Berry Passport
|
|
|
- if (skip) {
|
|
|
- double min = Math.min(mWidth, mHeight);
|
|
|
- double max = Math.max(mWidth, mHeight);
|
|
|
-
|
|
|
- if (max / min < 1.20) {
|
|
|
- Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
|
|
- skip = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Don't skip in MultiWindow.
|
|
|
- if (skip) {
|
|
|
- if (Build.VERSION.SDK_INT >= 24) {
|
|
|
- if (SDLActivity.mSingleton.isInMultiWindowMode()) {
|
|
|
- Log.v("SDL", "Don't skip in Multi-Window");
|
|
|
- skip = false;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (skip) {
|
|
|
- Log.v("SDL", "Skip .. Surface is not ready.");
|
|
|
- mIsSurfaceReady = false;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- /* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
|
|
- SDLActivity.onNativeSurfaceChanged();
|
|
|
-
|
|
|
- /* Surface is ready */
|
|
|
- mIsSurfaceReady = true;
|
|
|
-
|
|
|
- SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
|
|
- SDLActivity.handleNativeState();
|
|
|
- }
|
|
|
-
|
|
|
- // Key events
|
|
|
- @Override
|
|
|
- public boolean onKey(View v, int keyCode, KeyEvent event) {
|
|
|
-
|
|
|
- int deviceId = event.getDeviceId();
|
|
|
- int source = event.getSource();
|
|
|
-
|
|
|
- if (source == InputDevice.SOURCE_UNKNOWN) {
|
|
|
- InputDevice device = InputDevice.getDevice(deviceId);
|
|
|
- if (device != null) {
|
|
|
- source = device.getSources();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-// if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
|
-// Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source);
|
|
|
-// } else if (event.getAction() == KeyEvent.ACTION_UP) {
|
|
|
-// Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source);
|
|
|
-// }
|
|
|
-
|
|
|
- // Dispatch the different events depending on where they come from
|
|
|
- // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD
|
|
|
- // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD
|
|
|
- //
|
|
|
- // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and
|
|
|
- // SOURCE_JOYSTICK, while its key events arrive from the keyboard source
|
|
|
- // So, retrieve the device itself and check all of its sources
|
|
|
- if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) {
|
|
|
- // Note that we process events with specific key codes here
|
|
|
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
|
- if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- } else if (event.getAction() == KeyEvent.ACTION_UP) {
|
|
|
- if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if ((source & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) {
|
|
|
- if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
|
- if (SDLActivity.isTextInputEvent(event)) {
|
|
|
- SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1);
|
|
|
- }
|
|
|
- SDLActivity.onNativeKeyDown(keyCode);
|
|
|
- return true;
|
|
|
- } else if (event.getAction() == KeyEvent.ACTION_UP) {
|
|
|
- SDLActivity.onNativeKeyUp(keyCode);
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
|
|
|
- // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses
|
|
|
- // they are ignored here because sending them as mouse input to SDL is messy
|
|
|
- if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) {
|
|
|
- switch (event.getAction()) {
|
|
|
- case KeyEvent.ACTION_DOWN:
|
|
|
- case KeyEvent.ACTION_UP:
|
|
|
- // mark the event as handled or it will be handled by system
|
|
|
- // handling KEYCODE_BACK by system will call onBackPressed()
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // Touch events
|
|
|
- @Override
|
|
|
- public boolean onTouch(View v, MotionEvent event) {
|
|
|
- /* Ref: http://developer.android.com/training/gestures/multi.html */
|
|
|
- int touchDevId = event.getDeviceId();
|
|
|
- final int pointerCount = event.getPointerCount();
|
|
|
- int action = event.getActionMasked();
|
|
|
- int pointerFingerId;
|
|
|
- int i = -1;
|
|
|
- float x,y,p;
|
|
|
-
|
|
|
- /*
|
|
|
- * Prevent id to be -1, since it's used in SDL internal for synthetic events
|
|
|
- * Appears when using Android emulator, eg:
|
|
|
- * adb shell input mouse tap 100 100
|
|
|
- * adb shell input touchscreen tap 100 100
|
|
|
- */
|
|
|
- if (touchDevId < 0) {
|
|
|
- touchDevId -= 1;
|
|
|
- }
|
|
|
-
|
|
|
- // 12290 = Samsung DeX mode desktop mouse
|
|
|
- // 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
|
|
|
- // 0x2 = SOURCE_CLASS_POINTER
|
|
|
- if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
|
|
|
- int mouseButton = 1;
|
|
|
- try {
|
|
|
- Object object = event.getClass().getMethod("getButtonState").invoke(event);
|
|
|
- if (object != null) {
|
|
|
- mouseButton = (Integer) object;
|
|
|
- }
|
|
|
- } catch(Exception ignored) {
|
|
|
- }
|
|
|
-
|
|
|
- // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
|
|
- // if we are. We'll leverage our existing mouse motion listener
|
|
|
- SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
|
|
|
- x = motionListener.getEventX(event);
|
|
|
- y = motionListener.getEventY(event);
|
|
|
-
|
|
|
- SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
|
|
|
- } else {
|
|
|
- switch(action) {
|
|
|
- case MotionEvent.ACTION_MOVE:
|
|
|
- for (i = 0; i < pointerCount; i++) {
|
|
|
- pointerFingerId = event.getPointerId(i);
|
|
|
- x = event.getX(i) / mWidth;
|
|
|
- y = event.getY(i) / mHeight;
|
|
|
- p = event.getPressure(i);
|
|
|
- if (p > 1.0f) {
|
|
|
- // may be larger than 1.0f on some devices
|
|
|
- // see the documentation of getPressure(i)
|
|
|
- p = 1.0f;
|
|
|
- }
|
|
|
- SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- case MotionEvent.ACTION_UP:
|
|
|
- case MotionEvent.ACTION_DOWN:
|
|
|
- // Primary pointer up/down, the index is always zero
|
|
|
- i = 0;
|
|
|
- /* fallthrough */
|
|
|
- case MotionEvent.ACTION_POINTER_UP:
|
|
|
- case MotionEvent.ACTION_POINTER_DOWN:
|
|
|
- // Non primary pointer up/down
|
|
|
- if (i == -1) {
|
|
|
- i = event.getActionIndex();
|
|
|
- }
|
|
|
-
|
|
|
- pointerFingerId = event.getPointerId(i);
|
|
|
- x = event.getX(i) / mWidth;
|
|
|
- y = event.getY(i) / mHeight;
|
|
|
- p = event.getPressure(i);
|
|
|
- if (p > 1.0f) {
|
|
|
- // may be larger than 1.0f on some devices
|
|
|
- // see the documentation of getPressure(i)
|
|
|
- p = 1.0f;
|
|
|
- }
|
|
|
- SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
|
|
|
- break;
|
|
|
-
|
|
|
- case MotionEvent.ACTION_CANCEL:
|
|
|
- for (i = 0; i < pointerCount; i++) {
|
|
|
- pointerFingerId = event.getPointerId(i);
|
|
|
- x = event.getX(i) / mWidth;
|
|
|
- y = event.getY(i) / mHeight;
|
|
|
- p = event.getPressure(i);
|
|
|
- if (p > 1.0f) {
|
|
|
- // may be larger than 1.0f on some devices
|
|
|
- // see the documentation of getPressure(i)
|
|
|
- p = 1.0f;
|
|
|
- }
|
|
|
- SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
|
|
|
- }
|
|
|
- break;
|
|
|
-
|
|
|
- default:
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // Sensor events
|
|
|
- public void enableSensor(int sensortype, boolean enabled) {
|
|
|
- // TODO: This uses getDefaultSensor - what if we have >1 accels?
|
|
|
- if (enabled) {
|
|
|
- mSensorManager.registerListener(this,
|
|
|
- mSensorManager.getDefaultSensor(sensortype),
|
|
|
- SensorManager.SENSOR_DELAY_GAME, null);
|
|
|
- } else {
|
|
|
- mSensorManager.unregisterListener(this,
|
|
|
- mSensorManager.getDefaultSensor(sensortype));
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
|
|
- // TODO
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void onSensorChanged(SensorEvent event) {
|
|
|
- if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
|
|
-
|
|
|
- // Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
|
|
- // We thus should check here.
|
|
|
- int newOrientation;
|
|
|
-
|
|
|
- float x, y;
|
|
|
- switch (mDisplay.getRotation()) {
|
|
|
- case Surface.ROTATION_90:
|
|
|
- x = -event.values[1];
|
|
|
- y = event.values[0];
|
|
|
- newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
|
|
|
- break;
|
|
|
- case Surface.ROTATION_270:
|
|
|
- x = event.values[1];
|
|
|
- y = -event.values[0];
|
|
|
- newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
|
|
|
- break;
|
|
|
- case Surface.ROTATION_180:
|
|
|
- x = -event.values[0];
|
|
|
- y = -event.values[1];
|
|
|
- newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
|
|
|
- break;
|
|
|
- case Surface.ROTATION_0:
|
|
|
- default:
|
|
|
- x = event.values[0];
|
|
|
- y = event.values[1];
|
|
|
- newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
|
|
|
- break;
|
|
|
- }
|
|
|
-
|
|
|
- if (newOrientation != SDLActivity.mCurrentOrientation) {
|
|
|
- SDLActivity.mCurrentOrientation = newOrientation;
|
|
|
- SDLActivity.onNativeOrientationChanged(newOrientation);
|
|
|
- }
|
|
|
-
|
|
|
- SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
|
|
- y / SensorManager.GRAVITY_EARTH,
|
|
|
- event.values[2] / SensorManager.GRAVITY_EARTH);
|
|
|
-
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Captured pointer events for API 26.
|
|
|
- public boolean onCapturedPointerEvent(MotionEvent event)
|
|
|
- {
|
|
|
- int action = event.getActionMasked();
|
|
|
-
|
|
|
- float x, y;
|
|
|
- switch (action) {
|
|
|
- case MotionEvent.ACTION_SCROLL:
|
|
|
- x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
|
|
|
- y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
|
|
|
- SDLActivity.onNativeMouse(0, action, x, y, false);
|
|
|
- return true;
|
|
|
-
|
|
|
- case MotionEvent.ACTION_HOVER_MOVE:
|
|
|
- case MotionEvent.ACTION_MOVE:
|
|
|
- x = event.getX(0);
|
|
|
- y = event.getY(0);
|
|
|
- SDLActivity.onNativeMouse(0, action, x, y, true);
|
|
|
- return true;
|
|
|
-
|
|
|
- case MotionEvent.ACTION_BUTTON_PRESS:
|
|
|
- case MotionEvent.ACTION_BUTTON_RELEASE:
|
|
|
-
|
|
|
- // Change our action value to what SDL's code expects.
|
|
|
- if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
|
|
- action = MotionEvent.ACTION_DOWN;
|
|
|
- } else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
|
|
- action = MotionEvent.ACTION_UP;
|
|
|
- }
|
|
|
-
|
|
|
- x = event.getX(0);
|
|
|
- y = event.getY(0);
|
|
|
- int button = event.getButtonState();
|
|
|
-
|
|
|
- SDLActivity.onNativeMouse(button, action, x, y, true);
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
-}
|
|
|
-
|
|
|
/* This is a fake invisible editor view that receives the input and defines the
|
|
|
* pan&scan region
|
|
|
*/
|