/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2013 - Raw Material Software Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ package com.juce; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.view.*; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.graphics.*; import android.opengl.*; import android.text.ClipboardManager; import android.text.InputType; import android.util.DisplayMetrics; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.net.HttpURLConnection; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import android.media.AudioManager; import android.media.MediaScannerConnection; import android.media.MediaScannerConnection.MediaScannerConnectionClient; //============================================================================== public final class JuceAppActivity extends Activity { //============================================================================== static { System.loadLibrary ("juce_jni"); } @Override public final void onCreate (Bundle savedInstanceState) { super.onCreate (savedInstanceState); viewHolder = new ViewHolder (this); setContentView (viewHolder); setVolumeControlStream (AudioManager.STREAM_MUSIC); } @Override protected final void onDestroy() { quitApp(); super.onDestroy(); } @Override protected final void onPause() { if (viewHolder != null) viewHolder.onPause(); suspendApp(); super.onPause(); } @Override protected final void onResume() { super.onResume(); if (viewHolder != null) viewHolder.onResume(); resumeApp(); } @Override public void onConfigurationChanged (Configuration cfg) { super.onConfigurationChanged (cfg); setContentView (viewHolder); } private void callAppLauncher() { launchApp (getApplicationInfo().publicSourceDir, getApplicationInfo().dataDir); } //============================================================================== private native void launchApp (String appFile, String appDataDir); private native void quitApp(); private native void suspendApp(); private native void resumeApp(); private native void setScreenSize (int screenWidth, int screenHeight, int dpi); //============================================================================== public native void deliverMessage (long value); private android.os.Handler messageHandler = new android.os.Handler(); public final void postMessage (long value) { messageHandler.post (new MessageCallback (value)); } private final class MessageCallback implements Runnable { public MessageCallback (long value_) { value = value_; } public final void run() { deliverMessage (value); } private long value; } //============================================================================== private ViewHolder viewHolder; public final ComponentPeerView createNewView (boolean opaque, long host) { ComponentPeerView v = new ComponentPeerView (this, opaque, host); viewHolder.addView (v); return v; } public final void deleteView (ComponentPeerView view) { ViewGroup group = (ViewGroup) (view.getParent()); if (group != null) group.removeView (view); } final class ViewHolder extends ViewGroup { public ViewHolder (Context context) { super (context); setDescendantFocusability (ViewGroup.FOCUS_AFTER_DESCENDANTS); setFocusable (false); } protected final void onLayout (boolean changed, int left, int top, int right, int bottom) { setScreenSize (getWidth(), getHeight(), getDPI()); if (isFirstResize) { isFirstResize = false; callAppLauncher(); } } public final void onPause() { for (int i = getChildCount(); --i >= 0;) { View v = getChildAt (i); if (v instanceof ComponentPeerView) ((ComponentPeerView) v).onPause(); } } public final void onResume() { for (int i = getChildCount(); --i >= 0;) { View v = getChildAt (i); if (v instanceof ComponentPeerView) ((ComponentPeerView) v).onResume(); } } private final int getDPI() { DisplayMetrics metrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics (metrics); return metrics.densityDpi; } private boolean isFirstResize = true; } public final void excludeClipRegion (android.graphics.Canvas canvas, float left, float top, float right, float bottom) { canvas.clipRect (left, top, right, bottom, android.graphics.Region.Op.DIFFERENCE); } //============================================================================== public final String getClipboardContent() { ClipboardManager clipboard = (ClipboardManager) getSystemService (CLIPBOARD_SERVICE); return clipboard.getText().toString(); } public final void setClipboardContent (String newText) { ClipboardManager clipboard = (ClipboardManager) getSystemService (CLIPBOARD_SERVICE); clipboard.setText (newText); } //============================================================================== public final void showMessageBox (String title, String message, final long callback) { AlertDialog.Builder builder = new AlertDialog.Builder (this); builder.setTitle (title) .setMessage (message) .setCancelable (true) .setPositiveButton ("OK", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceAppActivity.this.alertDismissed (callback, 0); } }); builder.create().show(); } public final void showOkCancelBox (String title, String message, final long callback) { AlertDialog.Builder builder = new AlertDialog.Builder (this); builder.setTitle (title) .setMessage (message) .setCancelable (true) .setPositiveButton ("OK", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceAppActivity.this.alertDismissed (callback, 1); } }) .setNegativeButton ("Cancel", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceAppActivity.this.alertDismissed (callback, 0); } }); builder.create().show(); } public final void showYesNoCancelBox (String title, String message, final long callback) { AlertDialog.Builder builder = new AlertDialog.Builder (this); builder.setTitle (title) .setMessage (message) .setCancelable (true) .setPositiveButton ("Yes", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceAppActivity.this.alertDismissed (callback, 1); } }) .setNegativeButton ("No", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceAppActivity.this.alertDismissed (callback, 2); } }) .setNeutralButton ("Cancel", new DialogInterface.OnClickListener() { public void onClick (DialogInterface dialog, int id) { dialog.cancel(); JuceAppActivity.this.alertDismissed (callback, 0); } }); builder.create().show(); } public native void alertDismissed (long callback, int id); //============================================================================== public final class ComponentPeerView extends ViewGroup implements View.OnFocusChangeListener { public ComponentPeerView (Context context, boolean opaque_, long host) { super (context); this.host = host; setWillNotDraw (false); opaque = opaque_; setFocusable (true); setFocusableInTouchMode (true); setOnFocusChangeListener (this); requestFocus(); } //============================================================================== private native void handlePaint (long host, Canvas canvas); @Override public void onDraw (Canvas canvas) { handlePaint (host, canvas); } @Override public boolean isOpaque() { return opaque; } private boolean opaque; private long host; //============================================================================== private native void handleMouseDown (long host, int index, float x, float y, long time); private native void handleMouseDrag (long host, int index, float x, float y, long time); private native void handleMouseUp (long host, int index, float x, float y, long time); @Override public boolean onTouchEvent (MotionEvent event) { int action = event.getAction(); long time = event.getEventTime(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: handleMouseDown (host, event.getPointerId(0), event.getX(), event.getY(), time); return true; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: handleMouseUp (host, event.getPointerId(0), event.getX(), event.getY(), time); return true; case MotionEvent.ACTION_MOVE: { int n = event.getPointerCount(); for (int i = 0; i < n; ++i) handleMouseDrag (host, event.getPointerId(i), event.getX(i), event.getY(i), time); return true; } case MotionEvent.ACTION_POINTER_UP: { int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; handleMouseUp (host, event.getPointerId(i), event.getX(i), event.getY(i), time); return true; } case MotionEvent.ACTION_POINTER_DOWN: { int i = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; handleMouseDown (host, event.getPointerId(i), event.getX(i), event.getY(i), time); return true; } default: break; } return false; } //============================================================================== private native void handleKeyDown (long host, int keycode, int textchar); private native void handleKeyUp (long host, int keycode, int textchar); public void showKeyboard (String type) { InputMethodManager imm = (InputMethodManager) getSystemService (Context.INPUT_METHOD_SERVICE); if (imm != null) { if (type.length() > 0) { imm.showSoftInput (this, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); imm.setInputMethod (getWindowToken(), type); } else { imm.hideSoftInputFromWindow (getWindowToken(), 0); } } } @Override public boolean onKeyDown (int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: return super.onKeyDown (keyCode, event); default: break; } handleKeyDown (host, keyCode, event.getUnicodeChar()); return true; } @Override public boolean onKeyUp (int keyCode, KeyEvent event) { handleKeyUp (host, keyCode, event.getUnicodeChar()); return true; } // this is here to make keyboard entry work on a Galaxy Tab2 10.1 @Override public InputConnection onCreateInputConnection (EditorInfo outAttrs) { outAttrs.actionLabel = ""; outAttrs.hintText = ""; outAttrs.initialCapsMode = 0; outAttrs.initialSelEnd = outAttrs.initialSelStart = -1; outAttrs.label = ""; outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE | EditorInfo.IME_FLAG_NO_EXTRACT_UI; outAttrs.inputType = InputType.TYPE_NULL; return new BaseInputConnection (this, false); } //============================================================================== @Override protected void onSizeChanged (int w, int h, int oldw, int oldh) { super.onSizeChanged (w, h, oldw, oldh); viewSizeChanged (host); } @Override protected void onLayout (boolean changed, int left, int top, int right, int bottom) { for (int i = getChildCount(); --i >= 0;) requestTransparentRegion (getChildAt (i)); } private native void viewSizeChanged (long host); @Override public void onFocusChange (View v, boolean hasFocus) { if (v == this) focusChanged (host, hasFocus); } private native void focusChanged (long host, boolean hasFocus); public void setViewName (String newName) {} public boolean isVisible() { return getVisibility() == VISIBLE; } public void setVisible (boolean b) { setVisibility (b ? VISIBLE : INVISIBLE); } public boolean containsPoint (int x, int y) { return true; //xxx needs to check overlapping views } public final void onPause() { for (int i = getChildCount(); --i >= 0;) { View v = getChildAt (i); if (v instanceof OpenGLView) ((OpenGLView) v).onPause(); } } public final void onResume() { for (int i = getChildCount(); --i >= 0;) { View v = getChildAt (i); if (v instanceof OpenGLView) ((OpenGLView) v).onResume(); } } public OpenGLView createGLView() { OpenGLView glView = new OpenGLView (getContext()); addView (glView); return glView; } } //============================================================================== public final class OpenGLView extends GLSurfaceView implements GLSurfaceView.Renderer { OpenGLView (Context context) { super (context); setEGLContextClientVersion (2); setRenderer (this); setRenderMode (RENDERMODE_WHEN_DIRTY); } @Override public void onSurfaceCreated (GL10 unused, EGLConfig config) { contextCreated(); } @Override public void onSurfaceChanged (GL10 unused, int width, int height) { contextChangedSize(); } @Override public void onDrawFrame (GL10 unused) { render(); } private native void contextCreated(); private native void contextChangedSize(); private native void render(); } //============================================================================== public final int[] renderGlyph (char glyph, Paint paint, android.graphics.Matrix matrix, Rect bounds) { Path p = new Path(); paint.getTextPath (String.valueOf (glyph), 0, 1, 0.0f, 0.0f, p); RectF boundsF = new RectF(); p.computeBounds (boundsF, true); matrix.mapRect (boundsF); boundsF.roundOut (bounds); bounds.left--; bounds.right++; final int w = bounds.width(); final int h = Math.max (1, bounds.height()); Bitmap bm = Bitmap.createBitmap (w, h, Bitmap.Config.ARGB_8888); Canvas c = new Canvas (bm); matrix.postTranslate (-bounds.left, -bounds.top); c.setMatrix (matrix); c.drawPath (p, paint); final int sizeNeeded = w * h; if (cachedRenderArray.length < sizeNeeded) cachedRenderArray = new int [sizeNeeded]; bm.getPixels (cachedRenderArray, 0, w, 0, 0, w, h); bm.recycle(); return cachedRenderArray; } private int[] cachedRenderArray = new int [256]; //============================================================================== public static class HTTPStream { public HTTPStream (HttpURLConnection connection_, int[] statusCode, StringBuffer responseHeaders) throws IOException { connection = connection_; try { inputStream = new BufferedInputStream (connection.getInputStream()); } catch (IOException e) { if (connection.getResponseCode() < org.apache.http.HttpStatus.SC_BAD_REQUEST) throw e; } finally { statusCode[0] = connection.getResponseCode(); } if (statusCode[0] >= org.apache.http.HttpStatus.SC_BAD_REQUEST) inputStream = connection.getErrorStream(); else inputStream = connection.getInputStream(); for (java.util.Map.Entry> entry : connection.getHeaderFields().entrySet()) if (entry.getKey() != null && entry.getValue() != null) responseHeaders.append (entry.getKey() + ": " + android.text.TextUtils.join (",", entry.getValue()) + "\n"); } public final void release() { try { inputStream.close(); } catch (IOException e) {} connection.disconnect(); } public final int read (byte[] buffer, int numBytes) { int num = 0; try { num = inputStream.read (buffer, 0, numBytes); } catch (IOException e) {} if (num > 0) position += num; return num; } public final long getPosition() { return position; } public final long getTotalLength() { return -1; } public final boolean isExhausted() { return false; } public final boolean setPosition (long newPos) { return false; } private HttpURLConnection connection; private InputStream inputStream; private long position; } public static final HTTPStream createHTTPStream (String address, boolean isPost, byte[] postData, String headers, int timeOutMs, int[] statusCode, StringBuffer responseHeaders) { try { HttpURLConnection connection = (HttpURLConnection) (new URL(address) .openConnection()); if (connection != null) { try { if (isPost) { connection.setRequestMethod("POST"); connection.setConnectTimeout(timeOutMs); connection.setDoOutput(true); connection.setChunkedStreamingMode(0); OutputStream out = connection.getOutputStream(); out.write(postData); out.flush(); } return new HTTPStream (connection, statusCode, responseHeaders); } catch (Throwable e) { connection.disconnect(); } } } catch (Throwable e) {} return null; } public final void launchURL (String url) { startActivity (new Intent (Intent.ACTION_VIEW, Uri.parse (url))); } public static final String getLocaleValue (boolean isRegion) { java.util.Locale locale = java.util.Locale.getDefault(); return isRegion ? locale.getDisplayCountry (java.util.Locale.US) : locale.getDisplayLanguage (java.util.Locale.US); } //============================================================================== private final class SingleMediaScanner implements MediaScannerConnectionClient { public SingleMediaScanner (Context context, String filename) { file = filename; msc = new MediaScannerConnection (context, this); msc.connect(); } @Override public void onMediaScannerConnected() { msc.scanFile (file, null); } @Override public void onScanCompleted (String path, Uri uri) { msc.disconnect(); } private MediaScannerConnection msc; private String file; } public final void scanFile (String filename) { new SingleMediaScanner (this, filename); } }