commit e4237c21e523563a0d8305ec08557c27df439fe6
Author: Leszek Koltunski <leszek@distoretedandroid.org>
Date:   Wed Jun 15 16:14:07 2016 +0100

    Move the 'Save' effect to the Application and remove the 'OTHER' effect queue

diff --git a/src/main/java/org/distorted/examples/save/SaveActivity.java b/src/main/java/org/distorted/examples/save/SaveActivity.java
index 4f1b53f..6fc9f75 100644
--- a/src/main/java/org/distorted/examples/save/SaveActivity.java
+++ b/src/main/java/org/distorted/examples/save/SaveActivity.java
@@ -57,6 +57,8 @@ public class SaveActivity extends Activity implements SeekBar.OnSeekBarChangeLis
   @Override
   protected void onPause() 
     {
+    SaveRenderer.stopThread();
+
     GLSurfaceView mView = (GLSurfaceView) this.findViewById(R.id.saveSurfaceView);
     mView.onPause();
       
@@ -72,6 +74,8 @@ public class SaveActivity extends Activity implements SeekBar.OnSeekBarChangeLis
       
     GLSurfaceView mView = (GLSurfaceView) this.findViewById(R.id.saveSurfaceView);
     mView.onResume();
+
+    SaveRenderer.startThread(this);
     }
  
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/examples/save/SaveRenderer.java b/src/main/java/org/distorted/examples/save/SaveRenderer.java
index 52d7357..a5e062f 100644
--- a/src/main/java/org/distorted/examples/save/SaveRenderer.java
+++ b/src/main/java/org/distorted/examples/save/SaveRenderer.java
@@ -22,7 +22,8 @@ package org.distorted.examples.save;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Vector;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
@@ -31,8 +32,6 @@ import org.distorted.examples.R;
 
 import org.distorted.library.Distorted;
 import org.distorted.library.DistortedBitmap;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.message.EffectMessage;
 import org.distorted.library.EffectTypes;
 import org.distorted.library.type.Float1D;
 import org.distorted.library.type.Float2D;
@@ -45,11 +44,10 @@ import android.graphics.BitmapFactory;
 import android.opengl.GLES20;
 import android.opengl.GLSurfaceView;
 import android.os.Environment;
-import android.widget.Toast;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class SaveRenderer implements GLSurfaceView.Renderer ,EffectListener
+class SaveRenderer implements GLSurfaceView.Renderer
   {
   private GLSurfaceView mView;
   private static DistortedBitmap mGirl;
@@ -61,26 +59,10 @@ class SaveRenderer implements GLSurfaceView.Renderer ,EffectListener
   private int bmpHeight, bmpWidth;
   private static int scrHeight, scrWidth;
   private static float boobsSink;
+  private static boolean isSaving = false;
+  private static String mPath;
 
-  private static class SaveInfo
-    {
-    private long id;
-    private String path;
-
-    SaveInfo(long id, String path)
-      {
-      this.id   =   id;
-      this.path = path;
-      }
-    }
-
-  private static Vector<SaveInfo> mSaveInfo;  // Vector keeping all the information about the PATH
-                                              // where current 'SAVE' effects are saving to and also
-                                              // the IDs of the effects.
-                                              // We need to have a whole Vector because there can be
-                                              // many concurrent 'SAVE' operations (click 'SAVE',
-                                              // don't wait for the Toast to appear, immediately
-                                              // click again! )
+  private static SaveWorkerThread mWorkerThread =null;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -101,89 +83,52 @@ class SaveRenderer implements GLSurfaceView.Renderer ,EffectListener
     diSink.setCount(0.5f);
     diSink.setDuration(0);
     diSink.add(s0);
-
-    mSaveInfo = new Vector<>();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public static void setSize(float s)
+  public static void startThread(Activity act)
     {
-    boobsSink = s;
-    s0.set(boobsSink);
+    if( mWorkerThread ==null ) mWorkerThread = new SaveWorkerThread();
+
+    mWorkerThread.startSending(act);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public static void Save()
+  public static void stopThread()
     {
-    File file;
-    int lowestNotFound = 1;
-    String path;
+    mWorkerThread.stopSending();
+    mWorkerThread = null;
+    }
 
-    while(true)
-      {
-      path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/girl" + lowestNotFound +".png";
-      file = new File(path);
+///////////////////////////////////////////////////////////////////////////////////////////////////
 
-      if( !file.exists() )
-        {
-        long id= mGirl.savePNG(file.getAbsolutePath(),0,0,scrWidth,scrHeight);
-        mSaveInfo.add(new SaveInfo(id,path));
-        break;
-        }
-      lowestNotFound++;
-      }
+  public static void setSize(float s)
+    {
+    boobsSink = s;
+    s0.set(boobsSink);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
-// the library sending messages to us. This does not run on this UI thread!
 
-  public void effectMessage(final EffectMessage em, final long effectID, final int effectName, final long bitmapID, final String message)
+  public static void Save()
     {
-    int numSaves = mSaveInfo.size();
-    int found = -1;
-
-    for(int i=0; i<numSaves; i++)           // doesn't need to be synchronized with Save()
-      {                                     // because over there we only add() and here we only
-      if( effectID == mSaveInfo.get(i).id ) // remove() - and even if we add() in between of
-        {                                   // computing the 'numSaves' above and this loop,
-        found = i;                          // it still doesn't matter :)
-        break;
-        }
-      }
-
-    if( found>=0 )  // the message really is about one of the 'SAVE' effects in execution now
+    if( isSaving==false )
       {
-      Activity act = (Activity)mView.getContext();
-      final String path = mSaveInfo.get(found).path;
-      mSaveInfo.remove(found);
+      isSaving = true;
 
-      switch(em)
+      File file;
+      int lowestNotFound = 1;
+
+      while(true)
         {
-        case EFFECT_FINISHED: act.runOnUiThread(new Runnable()
-                                {
-                                public void run()
-                                  {
-                                  Toast.makeText(mView.getContext(),
-                                      "Saving to \n\n"+path+"\n\nsuccessful" ,
-                                      Toast.LENGTH_LONG).show();
-                                  }
-                                });
-                              break;
-
-        case EFFECT_FAILED  : act.runOnUiThread(new Runnable()
-                                {
-                                public void run()
-                                  {
-                                  Toast.makeText(mView.getContext(),
-                                      "Saving to \n\n"+path+"\n\n failed: "+"\n\n"+message ,
-                                      Toast.LENGTH_LONG).show();
-                                  }
-                                });
-                              break;
-
-        default             : break;
+        mPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/girl" + lowestNotFound +".png";
+        file = new File(mPath);
+
+        if( !file.exists() ) break;
+
+        lowestNotFound++;
         }
       }
     }
@@ -196,6 +141,17 @@ class SaveRenderer implements GLSurfaceView.Renderer ,EffectListener
     GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
       
     mGirl.draw(System.currentTimeMillis());
+
+    if( isSaving )  // we should save the buffer to location pointed at by mPath
+      {
+      ByteBuffer buf = ByteBuffer.allocateDirect( scrWidth*scrHeight*4 );
+      buf.order(ByteOrder.LITTLE_ENDIAN);
+      GLES20.glReadPixels( 0, 0, scrWidth, scrHeight , GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
+
+      mWorkerThread.newBuffer(buf,scrWidth,scrHeight,mPath);
+
+      isSaving = false;
+      }
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -247,7 +203,6 @@ class SaveRenderer implements GLSurfaceView.Renderer ,EffectListener
     bmpWidth  = bitmap.getWidth();
       
     mGirl = new DistortedBitmap(bitmap, 30);
-    mGirl.addEventListener(this);
 
     mGirl.sink( diSink, sinkRegion, pLeft );
     mGirl.sink( diSink, sinkRegion, pRight);
diff --git a/src/main/java/org/distorted/examples/save/SaveWorkerThread.java b/src/main/java/org/distorted/examples/save/SaveWorkerThread.java
new file mode 100644
index 0000000..d95f568
--- /dev/null
+++ b/src/main/java/org/distorted/examples/save/SaveWorkerThread.java
@@ -0,0 +1,193 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted is free software: you can redistribute it and/or modify                             //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.examples.save;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.widget.Toast;
+
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.nio.ByteBuffer;
+import java.util.Vector;
+
+public class SaveWorkerThread extends Thread
+  {
+  private static Vector<WorkLoad> mBuffers;
+  private static SaveWorkerThread mThis=null;
+  private static volatile boolean mPaused;
+  private static WeakReference<Activity> mWeakAct;
+
+  private static class WorkLoad
+    {
+    ByteBuffer buffer;
+    int width;
+    int height;
+    String filename;
+
+    WorkLoad(ByteBuffer buf, int w, int h, String name)
+      {
+      buffer   = buf;
+      width    = w;
+      height   = h;
+      filename = name;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public SaveWorkerThread()
+    {
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void startSending(Activity act)
+    {
+    mWeakAct = new WeakReference(act);
+
+    mPaused = false;
+
+    if( mThis==null )
+      {
+      mBuffers = new Vector<>();
+      mThis = new SaveWorkerThread();
+      mThis.start();
+      }
+    else
+      {
+      synchronized(mThis)
+        {
+        mThis.notify();
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void stopSending()
+    {
+    mPaused = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void run()
+    {
+    WorkLoad load;
+
+    while(true)
+      {
+      if( mBuffers.size()>0 )
+        {
+        load = mBuffers.remove(0);
+        process(load);
+        }
+
+      if( mPaused )
+        {
+        synchronized(mThis)
+          {
+          try  { mThis.wait(); }
+          catch(InterruptedException ex) { }
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void newBuffer(ByteBuffer buffer, int width, int height, String filename)
+    {
+    WorkLoad load = new WorkLoad(buffer,width,height,filename);
+    mBuffers.add(load);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void process(WorkLoad load)
+    {
+    ByteBuffer buf = load.buffer;
+    int width  = load.width;
+    int height = load.height;
+    final String filename = load.filename;
+
+    byte[] tmp1 = new byte[width*4];
+    byte[] tmp2 = new byte[width*4];
+
+    for(int i=0; i<height/2; i++)
+      {
+      buf.position((         i)*width*4);  // GL uses a coordinate system from mathematics; i.e.
+      buf.get(tmp1);                       // (0,0) is in the lower-left corner. 2D stuff has
+      buf.position((height-1-i)*width*4);  // the origin on the upper-left corner; we have to flip
+      buf.get(tmp2);                       // out bitmap upside down!
+
+      buf.position((         i)*width*4);
+      buf.put(tmp2);
+      buf.position((height-1-i)*width*4);
+      buf.put(tmp1);
+      }
+
+    buf.rewind();
+    BufferedOutputStream bos =null;
+    final Activity act = mWeakAct.get();
+
+    try
+      {
+      bos = new BufferedOutputStream(new FileOutputStream(filename));
+      Bitmap bmp = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888);
+      bmp.copyPixelsFromBuffer(buf);
+      bmp.compress(Bitmap.CompressFormat.PNG, 90, bos);
+      bmp.recycle();
+
+      act.runOnUiThread(new Runnable()
+        {
+        public void run()
+          {
+          Toast.makeText(act,
+              "Saving to \n\n"+filename+"\n\nsuccessful" ,
+              Toast.LENGTH_LONG).show();
+          }
+        });
+      }
+    catch(final Exception ex)
+      {
+      act.runOnUiThread(new Runnable()
+        {
+        public void run()
+          {
+          Toast.makeText(act,
+              "Saving to \n\n"+filename+"\n\n failed: "+"\n\n"+ex.getMessage() ,
+              Toast.LENGTH_LONG).show();
+          }
+        });
+      }
+    finally
+      {
+      if(bos!=null)
+        {
+        try { bos.close(); }
+        catch(IOException io) {}
+        }
+      }
+    }
+  }
