From 0cfc7847c58b51c9a26b50d905b592d1fc4c8578 Mon Sep 17 00:00:00 2001
From: Sven Gothel <sgothel@jausoft.com>
Date: Wed, 14 Mar 2012 23:07:21 +0100
Subject: Android: New ActivityLauncher (jogamp.android-launcher.apk)

ActivityLauncher provides delegating Activities, allowing the user to:

  - daisy chain custom APK classes and native libraries to the classpath

  - name one custom activity which gets delegated to, the downstream activity

Overview:

  [User:a1] -- (usr-data) --> [Launcher] -> [User:a2] + using [other packages..]

[User APK]   - The user provided APK
[JogAmp APK] - JogAmp APKs

[User:a1]    - The initial user activity, which starts the [Launcher].
               Providing data to [Launcher]: [User:a2], [User APK]
               Resides in [User APK]

[User:a2]    - The actual downstream 'real' activity, spoiled w/ full fledged ClassLoader
               having access to all packages as requested, ie. [User APK], ..
               Resides in [User APK]

[Launcher]   - The launcher activity.
               Gets called by [User:a1].
               Creates a new ClassLoader, daisy chainging all requested APKs.
               Instantiates [User:a2] w/ new ClassLoader.
               Delegates all calls to [User:a2].
               Resides in [JogAmp APK].
---
 .../jogamp/android/launcher/ActivityLauncher.java  | 231 ++++++++++
 .../jogamp/android/launcher/ClassLoaderUtil.java   | 193 +++++++++
 src/java/jogamp/android/launcher/LauncherMain.java |  63 +++
 .../android/launcher/LauncherTempFileCache.java    | 477 +++++++++++++++++++++
 src/java/jogamp/android/launcher/LauncherUtil.java | 320 ++++++++++++++
 5 files changed, 1284 insertions(+)
 create mode 100644 src/java/jogamp/android/launcher/ActivityLauncher.java
 create mode 100644 src/java/jogamp/android/launcher/ClassLoaderUtil.java
 create mode 100644 src/java/jogamp/android/launcher/LauncherMain.java
 create mode 100644 src/java/jogamp/android/launcher/LauncherTempFileCache.java
 create mode 100644 src/java/jogamp/android/launcher/LauncherUtil.java

(limited to 'src/java/jogamp/android/launcher')

diff --git a/src/java/jogamp/android/launcher/ActivityLauncher.java b/src/java/jogamp/android/launcher/ActivityLauncher.java
new file mode 100644
index 0000000..04c898e
--- /dev/null
+++ b/src/java/jogamp/android/launcher/ActivityLauncher.java
@@ -0,0 +1,231 @@
+/**
+ * Copyright 2011 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ * 
+ *    1. Redistributions of source code must retain the above copyright notice, this list of
+ *       conditions and the following disclaimer.
+ * 
+ *    2. Redistributions in binary form must reproduce the above copyright notice, this list
+ *       of conditions and the following disclaimer in the documentation and/or other materials
+ *       provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * 
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.android.launcher;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import android.app.Activity;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.TextView;
+import android.util.Log;
+
+public class ActivityLauncher extends Activity {
+   static final String TAG = "NEWTLauncherActivity";
+   TextView tv = null;
+   Method mOnCreate, mOnDestroy, mOnPause, mOnRestart, mOnResume, 
+          mOnStart, mOnStop, mSetIsInvokedByExternalActivity;
+   Class<?> activityClazz = null;
+   Object activityObject  = null;
+   
+   @Override
+   public void onCreate(Bundle savedInstanceState) {
+       Log.d(TAG, "onCreate - S");
+       super.onCreate(savedInstanceState);
+       
+       final Uri uri = getIntent().getData();
+       final LauncherUtil.DataSet data = LauncherUtil.DataSet.create(uri);
+       data.setSystemProperties();
+
+       ClassLoader cl = ClassLoaderUtil.createJogampClassLoaderSingleton(this, data.getPackages());
+       if(null != cl) {
+           try {
+               activityClazz = Class.forName(data.getActivityName(), true, cl);
+               Log.d(TAG, "Activity Clazz "+activityClazz);
+               activityObject = createInstance(activityClazz, null);
+               Log.d(TAG, "Activity Object "+activityObject);
+               mOnCreate = activityClazz.getMethod("onCreate", Bundle.class);
+               mOnDestroy = activityClazz.getMethod("onDestroy");
+               mOnPause = activityClazz.getMethod("onPause");
+               mOnRestart = activityClazz.getMethod("onRestart");
+               mOnResume = activityClazz.getMethod("onResume");
+               mOnStart = activityClazz.getMethod("onStart");
+               mOnStop = activityClazz.getMethod("onStop");
+               mSetIsInvokedByExternalActivity = activityClazz.getMethod("setIsInvokedByExternalActivity", Activity.class);
+           } catch (Exception e) {
+               Log.d(TAG, "error: "+e, e);
+               throw new RuntimeException(e);
+           }
+       }
+
+       if( null == mOnCreate || null == mOnDestroy || null == mOnPause ||
+           null == mOnRestart || null == mOnResume ||
+           null == mSetIsInvokedByExternalActivity ) {
+           RuntimeException e = new RuntimeException("XXX - incomplete method set");
+           Log.d(TAG, "error: "+e, e);
+           throw e;
+       }
+       
+       callMethod(activityObject, mSetIsInvokedByExternalActivity, this);
+       
+       callMethod(activityObject, mOnCreate, savedInstanceState);
+       Log.d(TAG, "onCreate - X");
+   }
+   
+   @Override
+   public void onStart() {
+     Log.d(TAG, "onStart - S");
+     callMethod(activityObject, mOnStart);
+     super.onStart();
+     Log.d(TAG, "onStart - X");
+   }
+     
+   @Override
+   public void onRestart() {
+     Log.d(TAG, "onRestart - S");
+     callMethod(activityObject, mOnRestart);
+     super.onRestart();
+     Log.d(TAG, "onRestart - X");
+   }
+
+   @Override
+   public void onResume() {
+     Log.d(TAG, "onResume - S");
+     callMethod(activityObject, mOnResume);
+     super.onResume();
+     Log.d(TAG, "onResume - X");
+   }
+
+   @Override
+   public void onPause() {
+     Log.d(TAG, "onPause - S");
+     callMethod(activityObject, mOnPause);
+     super.onPause();
+     Log.d(TAG, "onPause - X");
+   }
+
+   @Override
+   public void onStop() {
+     Log.d(TAG, "onStop - S");
+     callMethod(activityObject, mOnStop);
+     super.onStop();  
+     Log.d(TAG, "onStop - X");
+   }
+
+   @Override
+   public void onDestroy() {
+     Log.d(TAG, "onDestroy - S");
+     callMethod(activityObject, mOnDestroy);
+     super.onDestroy();  
+     finish();
+     Log.d(TAG, "onDestroy - X");
+   }   
+
+   @Override
+   public void finish() {
+     Log.d(TAG, "finish - S");
+     super.finish();  
+     Log.d(TAG, "finish - X");
+   }   
+
+  /**
+   * @throws JogampRuntimeException if the instance can not be created.
+   */
+  public static final Object createInstance(Class<?> clazz, Class<?>[] cstrArgTypes, Object ... cstrArgs) 
+      throws RuntimeException
+  {
+    return createInstance(getConstructor(clazz, cstrArgTypes), cstrArgs);
+  }
+
+  public static final Object createInstance(Constructor<?> cstr, Object ... cstrArgs) 
+      throws RuntimeException
+  {
+    try {
+        return cstr.newInstance(cstrArgs);
+    } catch (Exception e) {
+      Throwable t = e;
+      if (t instanceof InvocationTargetException) {
+        t = ((InvocationTargetException) t).getTargetException();
+      }
+      if (t instanceof Error) {
+        throw (Error) t;
+      }
+      if (t instanceof RuntimeException) {
+        throw (RuntimeException) t;
+      }
+      throw new RuntimeException("can not create instance of "+cstr.getName(), t);
+    }
+  }
+  
+    /**
+     * @throws JogampRuntimeException if the constructor can not be delivered.
+     */
+    protected static final Constructor<?> getConstructor(Class<?> clazz, Class<?> ... cstrArgTypes) 
+        throws RuntimeException {
+        try {
+            if(null == cstrArgTypes) {
+                cstrArgTypes = zeroTypes;
+            }
+            return clazz.getDeclaredConstructor(cstrArgTypes);
+        } catch (NoSuchMethodException ex) {
+            throw new RuntimeException("Constructor: '" + clazz + "(" + asString(cstrArgTypes) + ")' not found", ex);
+        }
+    }
+    
+    protected static final Class<?>[] zeroTypes = new Class[0];
+
+    protected static final String asString(Class<?>[] argTypes) {
+        StringBuffer args = new StringBuffer();
+        boolean coma = false;
+        if(null != argTypes) {
+            for (int i = 0; i < argTypes.length; i++) {
+                if(coma) {
+                     args.append(", ");
+                }
+                args.append(argTypes[i].getName());
+                coma = true;
+            }
+        }
+        return args.toString();
+    }
+    
+  protected static final Object callMethod(Object instance, Method method, Object ... args)
+      throws RuntimeException
+  {
+    try {
+        return method.invoke(instance, args);
+    } catch (Exception e) {
+      Throwable t = e;
+      if (t instanceof InvocationTargetException) {
+        t = ((InvocationTargetException) t).getTargetException();
+      }
+      if (t instanceof Error) {
+        throw (Error) t;
+      }
+      if (t instanceof RuntimeException) {
+        throw (RuntimeException) t;
+      }
+      throw new RuntimeException("calling "+method+" failed", t);
+    }
+  }
+
+
+}
diff --git a/src/java/jogamp/android/launcher/ClassLoaderUtil.java b/src/java/jogamp/android/launcher/ClassLoaderUtil.java
new file mode 100644
index 0000000..319a0fd
--- /dev/null
+++ b/src/java/jogamp/android/launcher/ClassLoaderUtil.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright 2011 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ * 
+ *    1. Redistributions of source code must retain the above copyright notice, this list of
+ *       conditions and the following disclaimer.
+ * 
+ *    2. Redistributions in binary form must reproduce the above copyright notice, this list
+ *       of conditions and the following disclaimer in the documentation and/or other materials
+ *       provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * 
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+
+package jogamp.android.launcher;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
+import dalvik.system.DexClassLoader;
+
+public class ClassLoaderUtil {
+   private static final String TAG = "JogampClassLoader";
+   
+   public static final String packageGlueGen = "com.jogamp.common";       
+   public static final String packageJogl = "javax.media.opengl";   // FIXME: a 'performance' hack
+   
+   public static final String dexPathName= "jogampDex";
+   
+   private static LauncherTempFileCache tmpFileCache = null;
+   private static ClassLoader jogAmpClassLoader = null;
+   
+   /**
+    * 
+    * @param ctx
+    * @param userPackageNames list of user package names, the last entry shall reflect the Activity
+    * @return
+    */
+   public static synchronized ClassLoader createJogampClassLoaderSingleton(Context ctx, List<String> userPackageNames) {
+       if(null==jogAmpClassLoader) {
+           if(null!=tmpFileCache) {
+               throw new InternalError("XXX0");
+           }
+           if(!LauncherTempFileCache.initSingleton(ctx)) {
+               throw new InternalError("TempFileCache initialization error");
+           }
+           tmpFileCache = new LauncherTempFileCache();
+           if(!tmpFileCache.isValid()) {
+               throw new InternalError("TempFileCache instantiation error");                
+           }
+           final ApplicationInfo ai = ctx.getApplicationInfo();
+           Log.d(TAG, "S: userPackageName: "+userPackageNames+", dataDir: "+ai.dataDir+", nativeLibraryDir: "+ai.nativeLibraryDir);
+    
+           final String appDir = new File(ai.dataDir).getParent();
+           final String libSub = ai.nativeLibraryDir.substring(ai.nativeLibraryDir.lastIndexOf('/')+1);
+           Log.d(TAG, "S: appDir: "+appDir+", libSub: "+libSub);
+           
+           final String libPathName = appDir + "/" + packageGlueGen + "/" + libSub + "/:" +
+                                      appDir + "/" + packageJogl + "/" + libSub + "/" ;
+           Log.d(TAG, "S: libPath: "+libPathName);
+                   
+           String apkGlueGen = null;
+           String apkJogl = null;
+       
+           try {
+               apkGlueGen = ctx.getPackageManager().getApplicationInfo(packageGlueGen,0).sourceDir;
+               apkJogl = ctx.getPackageManager().getApplicationInfo(packageJogl,0).sourceDir;
+           } catch (PackageManager.NameNotFoundException e) {
+               Log.d(TAG, "error: "+e, e);
+           }
+           if(null == apkGlueGen || null == apkJogl) {
+               Log.d(TAG, "not found: gluegen <"+apkGlueGen+">, jogl <"+apkJogl+">");
+               return null;
+           }
+           
+           final String cp = apkGlueGen + ":" + apkJogl ;
+           Log.d(TAG, "jogamp cp: " + cp);
+       
+           final File dexPath = new File(tmpFileCache.getTempDir(), dexPathName);
+           Log.d(TAG, "jogamp dexPath: " + dexPath.getAbsolutePath());
+           dexPath.mkdir();
+           jogAmpClassLoader = new DexClassLoader(cp, dexPath.getAbsolutePath(), libPathName, ctx.getClassLoader());
+       } else {
+           if(null==tmpFileCache) {
+               throw new InternalError("XXX1");
+           }           
+       }
+       
+       StringBuilder userAPKs = new StringBuilder();
+       int numUserAPKs = 0;
+       String lastUserPackageName = null; // the very last one reflects the Activity
+       for(Iterator<String> i=userPackageNames.iterator(); i.hasNext(); ) {
+           lastUserPackageName = i.next();
+           String userAPK = null;
+           try {
+               userAPK = ctx.getPackageManager().getApplicationInfo(lastUserPackageName,0).sourceDir;
+           } catch (PackageManager.NameNotFoundException e) {
+               Log.d(TAG, "error: "+e, e);
+           }
+           if(null != userAPK) {
+               numUserAPKs++;
+               if(numUserAPKs>0) {
+                   userAPKs.append(":");
+               }
+               userAPKs.append(userAPK);
+               Log.d(TAG, "APK found: <"+lastUserPackageName+"> -> <"+userAPK+">");
+           } else {
+               Log.d(TAG, "APK not found: <"+lastUserPackageName+">");
+           }
+       }
+       if( userPackageNames.size()!=numUserAPKs ) {
+           Log.d(TAG, "APKs incomplete, abort");
+           return null;
+       }
+       
+       final String userAPKs_s = userAPKs.toString();
+       Log.d(TAG, "user cp: " + userAPKs_s);
+       final File dexPath = new File(tmpFileCache.getTempDir(), lastUserPackageName);
+       Log.d(TAG, "user dexPath: " + dexPath.getAbsolutePath());
+       dexPath.mkdir();
+       ClassLoader cl = new DexClassLoader(userAPKs_s, dexPath.getAbsolutePath(), null, jogAmpClassLoader);
+       Log.d(TAG, "cl: " + cl);
+       
+       return cl;
+   }
+   
+   public boolean setAPKClassLoader(String activityPackageName, ClassLoader classLoader)
+   {
+       try {
+           Field mMainThread = getField(Activity.class, "mMainThread");
+           Object mainThread = mMainThread.get(this);
+           Class<?> threadClass = mainThread.getClass();
+           Field mPackages = getField(threadClass, "mPackages");
+
+           @SuppressWarnings("unchecked")
+           HashMap<String,?> map = (HashMap<String,?>) mPackages.get(mainThread);
+           WeakReference<?> ref = (WeakReference<?>) map.get(activityPackageName);
+           Object apk = ref.get();
+           Class<?> apkClass = apk.getClass();
+           Field mClassLoader = getField(apkClass, "mClassLoader");
+
+           mClassLoader.set(apk, classLoader);
+           
+           Log.d(TAG, "setAPKClassLoader: OK");
+           
+           return true;
+       } catch (IllegalArgumentException e) {
+           e.printStackTrace();
+       } catch (IllegalAccessException e) {
+           e.printStackTrace();
+       }
+       Log.d(TAG, "setAPKClassLoader: FAILED");
+       return false;
+   }
+
+   private Field getField(Class<?> cls, String name)
+   {
+       for (Field field: cls.getDeclaredFields())
+       {
+           if (!field.isAccessible()) {
+               field.setAccessible(true);
+           }
+           if (field.getName().equals(name)) {
+               return field;
+           }
+       }
+       return null;
+   }   
+
+}
diff --git a/src/java/jogamp/android/launcher/LauncherMain.java b/src/java/jogamp/android/launcher/LauncherMain.java
new file mode 100644
index 0000000..bbdee1d
--- /dev/null
+++ b/src/java/jogamp/android/launcher/LauncherMain.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2011 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ * 
+ *    1. Redistributions of source code must retain the above copyright notice, this list of
+ *       conditions and the following disclaimer.
+ * 
+ *    2. Redistributions in binary form must reproduce the above copyright notice, this list
+ *       of conditions and the following disclaimer in the documentation and/or other materials
+ *       provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * 
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.android.launcher;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.app.ActivityGroup;
+import android.content.Intent;
+import android.os.Looper;
+import android.util.Log;
+
+public class LauncherMain {
+   static final String TAG = "NEWTLauncherMain";
+   
+   static String userActivityName = null;
+   static ArrayList<String> userPackageNames = new ArrayList<String>();
+   static boolean setAPKClasspath = false; 
+   
+   public static String getUserActivityName() { return userActivityName; }
+   public static List<String> getUserPackageNames()  { return userPackageNames; }
+   
+   public static void main(String args[]) throws IOException, ClassNotFoundException {
+       userActivityName = "com.jogamp.opengl.test.android.NEWTGearsES2Activity";
+       userPackageNames.add("com.jogamp.opengl.test");
+       Looper.prepareMainLooper();
+       ActivityGroup activityGroup = new ActivityGroup(true);
+       ClassLoader cl = ClassLoaderUtil.createJogampClassLoaderSingleton(activityGroup, getUserPackageNames());
+       if(null != cl) {
+           Class<?>  activityClazz = Class.forName(getUserActivityName(), true, cl);
+           Intent intent = new Intent(activityGroup, activityClazz);
+           Log.d(TAG, "Launching Activity: "+activityClazz+", "+intent);
+           android.view.Window activityWindow = activityGroup.getLocalActivityManager().startActivity ("ID001", intent);
+           // activityGroup.addContentView(view, params)
+       }
+   }
+}
diff --git a/src/java/jogamp/android/launcher/LauncherTempFileCache.java b/src/java/jogamp/android/launcher/LauncherTempFileCache.java
new file mode 100644
index 0000000..06b8516
--- /dev/null
+++ b/src/java/jogamp/android/launcher/LauncherTempFileCache.java
@@ -0,0 +1,477 @@
+/**
+ * Copyright 2011 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ *
+ *    1. Redistributions of source code must retain the above copyright notice, this list of
+ *       conditions and the following disclaimer.
+ *
+ *    2. Redistributions in binary form must reproduce the above copyright notice, this list
+ *       of conditions and the following disclaimer in the documentation and/or other materials
+ *       provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.android.launcher;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+
+import android.content.Context;
+
+public class LauncherTempFileCache {
+    private static final boolean DEBUG = true;
+
+    // Lifecycle: For all JVMs, ClassLoader and times.
+    private static final String tmpDirPrefix = "jogamp.tmp.cache";
+    
+    // Get the value of the tmproot system property
+    // Lifecycle: For all JVMs and ClassLoader
+    /* package */ static final String tmpRootPropName = "jnlp.jogamp.tmp.cache.root";
+
+    // Flag indicating that we got a fatal error in the static initializer.
+    private static boolean staticInitError = false;
+    
+    private static File tmpBaseDir;
+
+    // String representing the name of the temp root directory relative to the
+    // tmpBaseDir. Its value is "jlnNNNNN", which is the unique filename created
+    // by File.createTempFile() without the ".tmp" extension.
+    //
+    // Lifecycle: For all JVMs and ClassLoader
+    //
+    private static String tmpRootPropValue;
+
+    private static File tmpRootDir;
+
+    // Flag indicating that we got a fatal error in the initializer.
+    private boolean initError = false;
+
+    private File individualTmpDir;
+    
+    /**
+     * Documented way to kick off static initialization
+     * @return true is static initialization was successful
+     */
+    public static synchronized boolean initSingleton(Context ctx) {
+        if(null == tmpRootDir && !staticInitError) {
+            // Create / initialize the temp root directory, starting the Reaper
+            // thread to reclaim old installations if necessary. If we get an
+            // exception, set an error code.
+            try {
+                initTmpRoot(ctx);
+            } catch (Exception ex) {
+                ex.printStackTrace();
+                staticInitError = true;
+            }
+        }
+        return !staticInitError;        
+    }
+    
+    /**
+     * This method is called by the static initializer to create / initialize
+     * the temp root directory that will hold the temp directories for this
+     * instance of the JVM. This is done as follows:
+     *
+     *     1. Synchronize on a global lock. Note that for this purpose we will
+     *        use System.out in the absence of a true global lock facility.
+     *        We are careful not to hold this lock too long.
+     *
+     *     2. Check for the existence of the "jnlp.applet.launcher.tmproot"
+     *        system property.
+     *
+     *         a. If set, then some other thread in a different ClassLoader has
+     *            already created the tmprootdir, so we just need to
+     *            use it. The remaining steps are skipped.
+     *
+     *         b. If not set, then we are the first thread in this JVM to run,
+     *            and we need to create the the tmprootdir.
+     *
+     *     3. Create the tmprootdir, along with the appropriate locks.
+     *        Note that we perform the operations in the following order,
+     *        prior to creating tmprootdir itself, to work around the fact that
+     *        the file creation and file lock steps are not atomic, and we need
+     *        to ensure that a newly-created tmprootdir isn't reaped by a
+     *        concurrently running JVM.
+     *
+     *            create jlnNNNN.tmp using File.createTempFile()
+     *            lock jlnNNNN.tmp
+     *            create jlnNNNN.lck while holding the lock on the .tmp file
+     *            lock jlnNNNN.lck
+     *
+     *        Since the Reaper thread will enumerate the list of *.lck files
+     *        before starting, we can guarantee that if there exists a *.lck file
+     *        for an active process, then the corresponding *.tmp file is locked
+     *        by that active process. This guarantee lets us avoid reaping an
+     *        active process' files.
+     *
+     *     4. Set the "jnlp.applet.launcher.tmproot" system property.
+     *
+     *     5. Add a shutdown hook to cleanup jlnNNNN.lck and jlnNNNN.tmp. We
+     *        don't actually expect that this shutdown hook will ever be called,
+     *        but the act of doing this, ensures that the locks never get
+     *        garbage-collected, which is necessary for correct behavior when
+     *        the first ClassLoader is later unloaded, while subsequent Applets
+     *        are still running.
+     *
+     *     6. Start the Reaper thread to cleanup old installations.
+     */
+    private static void initTmpRoot(Context ctx) throws IOException {
+        if (DEBUG) {
+            System.err.println("TempFileCache: Static Initialization ----------------------------------------------");
+            System.err.println("TempFileCache: Thread: "+Thread.currentThread().getName()+", CL 0x"+Integer.toHexString(LauncherTempFileCache.class.getClassLoader().hashCode()));
+        }
+
+        synchronized (System.out) {
+            // Get the name of the tmpbase directory.
+            {
+                final File tmpRoot = ctx.getDir("temp", Context.MODE_WORLD_READABLE);
+                tmpBaseDir = new File(tmpRoot, tmpDirPrefix);
+            }
+            tmpRootPropValue = System.getProperty(tmpRootPropName);
+
+            if (tmpRootPropValue == null) {
+                // Create the tmpbase directory if it doesn't already exist
+                tmpBaseDir.mkdir();
+                if (!tmpBaseDir.isDirectory()) {
+                    throw new IOException("Cannot create directory " + tmpBaseDir);
+                }
+
+                // Create ${tmpbase}/jlnNNNN.tmp then lock the file
+                File tmpFile = File.createTempFile("jln", ".tmp", tmpBaseDir);
+                if (DEBUG) {
+                    System.err.println("TempFileCache: tmpFile = " + tmpFile.getAbsolutePath());
+                }
+                final FileOutputStream tmpOut = new FileOutputStream(tmpFile);
+                final FileChannel tmpChannel = tmpOut.getChannel();
+                final FileLock tmpLock = tmpChannel.lock();
+
+                // Strip off the ".tmp" to get the name of the tmprootdir
+                String tmpFileName = tmpFile.getAbsolutePath();
+                String tmpRootName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".tmp"));
+
+                // create ${tmpbase}/jlnNNNN.lck then lock the file
+                String lckFileName = tmpRootName + ".lck";
+                File lckFile = new File(lckFileName);
+                if (DEBUG) {
+                    System.err.println("TempFileCache: lckFile = " + lckFile.getAbsolutePath());
+                }
+                lckFile.createNewFile();
+                final FileOutputStream lckOut = new FileOutputStream(lckFile);
+                final FileChannel lckChannel = lckOut.getChannel();
+                final FileLock lckLock = lckChannel.lock();
+
+                // Create tmprootdir
+                tmpRootDir = new File(tmpRootName);
+                if (DEBUG) {
+                    System.err.println("TempFileCache: tmpRootDir = " + tmpRootDir.getAbsolutePath());
+                }
+                if (!tmpRootDir.mkdir()) {
+                    throw new IOException("Cannot create " + tmpRootDir);
+                }
+
+                // Add shutdown hook to cleanup the OutputStream, FileChannel,
+                // and FileLock for the jlnNNNN.lck and jlnNNNN.lck files.
+                // We do this so that the locks never get garbage-collected.
+                Runtime.getRuntime().addShutdownHook(new Thread() {
+                    /* @Override */
+                    public void run() {
+                        // NOTE: we don't really expect that this code will ever
+                        // be called. If it does, we will close the output
+                        // stream, which will in turn close the channel.
+                        // We will then release the lock.
+                        try {
+                            tmpOut.close();
+                            tmpLock.release();
+                            lckOut.close();
+                            lckLock.release();
+                        } catch (IOException ex) {
+                            // Do nothing
+                        }
+                    }
+                });
+
+                // Set the system property...
+                tmpRootPropValue = tmpRootName.substring(tmpRootName.lastIndexOf(File.separator) + 1);
+                System.setProperty(tmpRootPropName, tmpRootPropValue);
+                if (DEBUG) {
+                    System.err.println("TempFileCache: Setting " + tmpRootPropName + "=" + tmpRootPropValue);
+                }
+
+                // Start a new Reaper thread to do stuff...
+                Thread reaperThread = new Thread() {
+                    /* @Override */
+                    public void run() {
+                        deleteOldTempDirs();
+                    }
+                };
+                reaperThread.setName("TempFileCache-Reaper");
+                reaperThread.start();
+            } else {
+                // Make sure that the property is not set to an illegal value
+                if (tmpRootPropValue.indexOf('/') >= 0 ||
+                        tmpRootPropValue.indexOf(File.separatorChar) >= 0) {
+                    throw new IOException("Illegal value of: " + tmpRootPropName);
+                }
+
+                // Set tmpRootDir = ${tmpbase}/${jnlp.applet.launcher.tmproot}
+                if (DEBUG) {
+                    System.err.println("TempFileCache: Using existing value of: " +
+                            tmpRootPropName + "=" + tmpRootPropValue);
+                }
+                tmpRootDir = new File(tmpBaseDir, tmpRootPropValue);
+                if (DEBUG) {
+                    System.err.println("TempFileCache: tmpRootDir = " + tmpRootDir.getAbsolutePath());
+                }
+                if (!tmpRootDir.isDirectory()) {
+                    throw new IOException("Cannot access " + tmpRootDir);
+                }
+            }
+        }
+        if (DEBUG) {
+            System.err.println("------------------------------------------------------------------ (static ok: "+(!staticInitError)+")");
+        }
+    }
+
+    /**
+     * Called by the Reaper thread to delete old temp directories
+     * Only one of these threads will run per JVM invocation.
+     */
+    private static void deleteOldTempDirs() {
+        if (DEBUG) {
+            System.err.println("TempFileCache: *** Reaper: deleteOldTempDirs in " +
+                    tmpBaseDir.getAbsolutePath());
+        }
+
+        // enumerate list of jnl*.lck files, ignore our own jlnNNNN file
+        final String ourLockFile = tmpRootPropValue + ".lck";
+        FilenameFilter lckFilter = new FilenameFilter() {
+            /* @Override */
+            public boolean accept(File dir, String name) {
+                return name.endsWith(".lck") && !name.equals(ourLockFile);
+            }
+        };
+
+        // For each file <file>.lck in the list we will first try to lock
+        // <file>.tmp if that succeeds then we will try to lock <file>.lck
+        // (which should always succeed unless there is a problem). If we can
+        // get the lock on both files, then it must be an old installation, and
+        // we will delete it.
+        String[] fileNames = tmpBaseDir.list(lckFilter);
+        if (fileNames != null) {
+            for (int i = 0; i < fileNames.length; i++) {
+                String lckFileName = fileNames[i];
+                String tmpDirName = lckFileName.substring(0, lckFileName.lastIndexOf(".lck"));
+                String tmpFileName = tmpDirName + ".tmp";
+
+                File lckFile = new File(tmpBaseDir, lckFileName);
+                File tmpFile = new File(tmpBaseDir, tmpFileName);
+                File tmpDir = new File(tmpBaseDir, tmpDirName);
+
+                if (lckFile.exists() && tmpFile.exists() && tmpDir.isDirectory()) {
+                    FileOutputStream tmpOut = null;
+                    FileChannel tmpChannel = null;
+                    FileLock tmpLock = null;
+
+                    try {
+                        tmpOut = new FileOutputStream(tmpFile);
+                        tmpChannel = tmpOut.getChannel();
+                        tmpLock = tmpChannel.tryLock();
+                    } catch (Exception ex) {
+                        // Ignore exceptions
+                        if (DEBUG) {
+                            ex.printStackTrace();
+                        }
+                    }
+
+                    if (tmpLock != null) {
+                        FileOutputStream lckOut = null;
+                        FileChannel lckChannel = null;
+                        FileLock lckLock = null;
+
+                        try {
+                            lckOut = new FileOutputStream(lckFile);
+                            lckChannel = lckOut.getChannel();
+                            lckLock = lckChannel.tryLock();
+                        } catch (Exception ex) {
+                            if (DEBUG) {
+                                ex.printStackTrace();
+                            }
+                        }
+
+                        if (lckLock != null) {
+                            // Recursively remove the old tmpDir and all of
+                            // its contents
+                            removeAll(tmpDir);
+
+                            // Close the streams and delete the .lck and .tmp
+                            // files. Note that there is a slight race condition
+                            // in that another process could open a stream at
+                            // the same time we are trying to delete it, which will
+                            // prevent deletion, but we won't worry about it, since
+                            // the worst that will happen is we might have an
+                            // occasional 0-byte .lck or .tmp file left around
+                            try {
+                                lckOut.close();
+                            } catch (IOException ex) {
+                            }
+                            lckFile.delete();
+                            try {
+                                tmpOut.close();
+                            } catch (IOException ex) {
+                            }
+                            tmpFile.delete();
+                        } else {
+                            try {
+                                // Close the file and channel for the *.lck file
+                                if (lckOut != null) {
+                                    lckOut.close();
+                                }
+                                // Close the file/channel and release the lock
+                                // on the *.tmp file
+                                tmpOut.close();
+                                tmpLock.release();
+                            } catch (IOException ex) {
+                                if (DEBUG) {
+                                    ex.printStackTrace();
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    if (DEBUG) {
+                        System.err.println("TempFileCache: Skipping: " + tmpDir.getAbsolutePath());
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Remove the specified file or directory. If "path" is a directory, then
+     * recursively remove all entries, then remove the directory itself.
+     */
+    private static void removeAll(File path) {
+        if (DEBUG) {
+            System.err.println("TempFileCache: removeAll(" + path + ")");
+        }
+
+        if (path.isDirectory()) {
+            // Recursively remove all files/directories in this directory
+            File[] list = path.listFiles();
+            if (list != null) {
+                for (int i = 0; i < list.length; i++) {
+                    removeAll(list[i]);
+                }
+            }
+        }
+
+        path.delete();
+    }
+
+    public LauncherTempFileCache () {
+        if (DEBUG) {
+            System.err.println("TempFileCache: new TempFileCache() --------------------- (static ok: "+(!staticInitError)+")");
+            System.err.println("TempFileCache: Thread: "+Thread.currentThread().getName()+", CL 0x"+Integer.toHexString(LauncherTempFileCache.class.getClassLoader().hashCode())+", this 0x"+Integer.toHexString(hashCode()));
+        }
+        if(!staticInitError) { 
+            try {
+                createTmpDir();
+            } catch (Exception ex) {
+                ex.printStackTrace();
+                initError = true;
+            }
+        }
+        if (DEBUG) {
+            System.err.println("tempDir: "+individualTmpDir+" (ok: "+(!initError)+")");
+            System.err.println("----------------------------------------------------------");
+        }        
+    }
+    
+    /**
+     * @return true is static and object initialization was successful
+     */
+    public boolean isValid() { return !staticInitError && !initError; }
+    
+    /**
+     * Base temp directory used by TempFileCache. 
+     * Lifecycle: For all JVMs, ClassLoader and times.
+     * 
+     * This is set to:
+     *
+     * ${java.io.tmpdir}/<tmpDirPrefix>
+     *
+     * 
+     * @return
+     */
+    public File getBaseDir() { return tmpBaseDir; }
+
+    /**
+     * Root temp directory for this JVM instance. Used to store individual
+     * directories.
+     *
+     * Lifecycle: For all JVMs and ClassLoader
+     *
+     * <tmpBaseDir>/<tmpRootPropValue>
+     *
+     * Use Case: Per ClassLoader files, eg. native libraries. 
+     *
+     * Old temp directories are cleaned up the next time a JVM is launched that
+     * uses TempFileCache.
+     *
+     * 
+     * @return
+     */
+    public File getRootDir() { return tmpRootDir; }
+    
+    /**
+     * Temporary directory for individual files (eg. native libraries of one ClassLoader instance).
+     * The directory name is:
+     *
+     * Lifecycle: Within each JVM .. use case dependent, ie. per ClassLoader
+     *
+     * <tmpRootDir>/jlnMMMMM
+     *
+     * where jlnMMMMM is the unique filename created by File.createTempFile()
+     * without the ".tmp" extension.
+     *
+     * 
+     * @return
+     */
+    public File getTempDir() { return individualTmpDir; }
+    
+    
+    /**
+     * Create the temp directory in tmpRootDir. To do this, we create a temp
+     * file with a ".tmp" extension, and then create a directory of the
+     * same name but without the ".tmp". The temp file, directory, and all
+     * files in the directory will be reaped the next time this is started.
+     * We avoid deleteOnExit, because it doesn't work reliably.
+     */
+    private void createTmpDir() throws IOException {
+        File tmpFile = File.createTempFile("jln", ".tmp", tmpRootDir);
+        String tmpFileName = tmpFile.getAbsolutePath();
+        String tmpDirName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".tmp"));
+        individualTmpDir = new File(tmpDirName);
+        if (!individualTmpDir.mkdir()) {
+            throw new IOException("Cannot create " + individualTmpDir);
+        }
+    }
+}
diff --git a/src/java/jogamp/android/launcher/LauncherUtil.java b/src/java/jogamp/android/launcher/LauncherUtil.java
new file mode 100644
index 0000000..63452b7
--- /dev/null
+++ b/src/java/jogamp/android/launcher/LauncherUtil.java
@@ -0,0 +1,320 @@
+/**
+ * Copyright 2012 JogAmp Community. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification, are
+ * permitted provided that the following conditions are met:
+ * 
+ *    1. Redistributions of source code must retain the above copyright notice, this list of
+ *       conditions and the following disclaimer.
+ * 
+ *    2. Redistributions in binary form must reproduce the above copyright notice, this list
+ *       of conditions and the following disclaimer in the documentation and/or other materials
+ *       provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * 
+ * The views and conclusions contained in the software and documentation are those of the
+ * authors and should not be interpreted as representing official policies, either expressed
+ * or implied, of JogAmp Community.
+ */
+package jogamp.android.launcher;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Helper class to parse Uri's and programmatically add package names and properties to create an Uri or Intend.
+ * <p>
+ * The order of the Uri segments (any arguments) is preserved.
+ * </p>  
+ */
+public class LauncherUtil {
+    
+   /** Default launch mode. */
+   public static final String LAUNCH_ACTIVITY_NORMAL = "org.jogamp.launcher.action.LAUNCH_ACTIVITY_NORMAL";
+   
+   /** Transparent launch mode. Note: This seems to be required to achieve translucency, since setTheme(..) doesn't work. */
+   public static final String LAUNCH_ACTIVITY_TRANSPARENT = "org.jogamp.launcher.action.LAUNCH_ACTIVITY_TRANSPARENT";
+   
+   /** FIXME: TODO */
+   public static final String LAUNCH_MAIN = "org.jogamp.launcher.action.LAUNCH_MAIN";
+   
+   /** FIXME: TODO */
+   public static final String LAUNCH_JUNIT = "org.jogamp.launcher.action.LAUNCH_JUNIT";
+   
+   /** The protocol <code>launch</code> */
+   public static final String SCHEME = "launch";
+   
+   /** The host <code>jogamp.org</code> */
+   public static final String HOST = "jogamp.org";
+   
+   static final String PKG = "pkg";
+   
+   public static abstract class BaseActivityLauncher extends Activity {
+       final OrderedProperties props = new OrderedProperties();
+       
+       /** 
+        * Returns the default {@link LauncherUtil#LAUNCH_ACTIVITY_NORMAL} action.
+        * <p>
+        * Should be overridden for other action, eg.  {@link LauncherUtil#LAUNCH_ACTIVITY_TRANSPARENT}.
+        * </p>
+        */
+       public String getAction() { return LAUNCH_ACTIVITY_NORMAL; }
+       
+       /**
+        * Returns the properties, which are being propagated to the target activity.
+        * <p>
+        * Maybe be used to set custom properties.
+        * </p>
+        */
+       public final OrderedProperties getProperties() { return props; }
+       
+       /** Custom initialization hook which can be overriden to setup data, e.g. fill the properties retrieved by {@link #getProperties()}. */
+       public void init() { }
+       
+       /** Returns true if this launcher activity shall end after starting the downstream activity. Defaults to <code>true</code>, override to change behavior. */
+       public boolean finishAfterDelegate() { return true; }
+       
+       /** Must return the downstream Activity class name */
+       public abstract String getActivityName();
+       
+       /** Must return a list of required packages, at least one. */
+       public abstract List<String> getPackages();
+
+       @Override
+       public void onCreate(Bundle savedInstanceState) {
+           super.onCreate(savedInstanceState);
+           
+           init();
+           
+           final DataSet data = new DataSet();
+           data.setActivityName(getActivityName());
+           data.addAllPackages(getPackages());
+           data.addAllProperties(props);
+           
+           final Intent intent = LauncherUtil.getIntent(getAction(), data);
+           Log.d(getClass().getSimpleName(), "Launching Activity: "+intent);
+           startActivity (intent);
+           
+           if(finishAfterDelegate()) {
+               finish(); // done
+           }
+       }        
+   }
+   
+   public static class OrderedProperties {
+       HashMap<String, String> map = new HashMap<String, String>();
+       ArrayList<String> keyList = new ArrayList<String>(); 
+              
+       public final void setProperty(String key, String value) { 
+           if(key.equals(PKG)) {
+               throw new IllegalArgumentException("Illegal property key, '"+PKG+"' is reserved");
+           }
+           final String oval = map.put(key, value);
+           if(null != oval) {
+               map.put(key, oval); // restore
+               throw new IllegalArgumentException("Property overwriting not allowed: "+key+": "+oval+" -> "+value);
+           }
+           keyList.add(key); // new key
+       }
+       
+       public final void addAll(OrderedProperties props) {
+           Iterator<String> argKeys = props.keyList.iterator();
+           while(argKeys.hasNext()) {
+                   final String key = argKeys.next();
+                   setProperty(key, props.map.get(key));
+           }           
+       }
+       
+       public final void setSystemProperties() {
+           Iterator<String> argKeys = keyList.iterator();
+           while(argKeys.hasNext()) {
+                   final String key = argKeys.next();
+                   System.setProperty(key, map.get(key));
+           }
+       }
+       
+       public final String getProperty(String key) { return map.get(key); }
+       public final Map<String, String> getProperties() { return map; }
+           
+       /** Returns the list of property keys in the order, as they were added. */
+       public final List<String> getPropertyKeys() { return keyList; }       
+   }
+   
+   public static class DataSet {
+       static final char SLASH = '/';
+       static final char QMARK = '?';
+       static final char AMPER = '&';
+       static final char ASSIG = '=';
+       static final String COLSLASH2 = "://";
+       static final String EMPTY = "";
+       
+       String activityName = null;
+       ArrayList<String> packages = new ArrayList<String>();
+       OrderedProperties properties = new OrderedProperties();
+       
+       public final void setActivityName(String name) { activityName = name; }
+       public final String getActivityName() { return activityName; }
+       
+       public final void addPackage(String p) { 
+           packages.add(p); 
+       }   
+       public final void addAllPackages(List<String> plist) { 
+           packages.addAll(plist);
+       }   
+       public final List<String> getPackages()  { return packages; }
+       
+       public final void setProperty(String key, String value) {
+           properties.setProperty(key, value);
+       }
+       public final void addAllProperties(OrderedProperties props) {
+           properties.addAll(props);
+       }
+       public final void setSystemProperties() {
+           properties.setSystemProperties();
+       }   
+       public final String getProperty(String key) { return properties.getProperty(key); }
+       public final OrderedProperties getProperties() { return properties; }
+       public final List<String> getPropertyKeys() { return properties.getPropertyKeys(); }       
+       
+       public final Uri getUri() {
+           StringBuilder sb = new StringBuilder();
+           sb.append(SCHEME).append(COLSLASH2).append(HOST).append(SLASH).append(getActivityName());
+           boolean needsSep = false;
+           if(packages.size()>0) {
+               sb.append(QMARK);
+               for(int i=0; i<packages.size(); i++) {
+                   if(needsSep) {
+                       sb.append(AMPER);
+                   }
+                   sb.append(PKG).append(ASSIG).append(packages.get(i));
+                   needsSep = true;
+               }
+           }
+           Iterator<String> argKeys = properties.keyList.iterator();
+           while(argKeys.hasNext()) {
+                   if(needsSep) {
+                       sb.append(AMPER);
+                   }
+                   final String key = argKeys.next();
+                   sb.append(key).append(ASSIG).append(properties.map.get(key));
+                   needsSep = true;
+           }
+           return Uri.parse(sb.toString());
+       }
+       
+       public static final DataSet create(Uri uri) {
+           if(!uri.getScheme().equals(SCHEME)) {
+               return null;
+           }
+           if(!uri.getHost().equals(HOST)) {
+               return null;
+           }
+           DataSet data = new DataSet();
+           {
+               String an =  uri.getPath();
+               if(SLASH == an.charAt(0)) {
+                   an = an.substring(1);
+               }
+               if(SLASH == an.charAt(an.length()-1)) {
+                   an = an.substring(0, an.length()-1);
+               }
+               data.setActivityName(an);
+           }
+           
+           final String q = uri.getQuery();
+           final int q_l = q.length();
+           int q_e = -1;
+           while(q_e < q_l) {
+               int q_b = q_e + 1; // next term
+               q_e = q.indexOf(AMPER, q_b);
+               if(0 == q_e) {
+                   // single seperator
+                   continue; 
+               }
+               if(0 > q_e) {
+                   // end
+                   q_e = q_l;
+               }
+               // n-part
+               final String part = q.substring(q_b, q_e);
+               final int assignment = part.indexOf(ASSIG);
+               if(0 < assignment) {
+                   // assignment
+                   final String k = part.substring(0, assignment);
+                   final String v = part.substring(assignment+1);
+                   if(k.equals(PKG)) {
+                       if(v.length()==0) {
+                           throw new IllegalArgumentException("Empty package name: part <"+part+">, query <"+q+"> of "+uri);
+                       }
+                       data.addPackage(v);
+                   } else {
+                       data.setProperty(k, v);
+                   }
+               } else {
+                   // property key only
+                   if(part.equals(PKG)) {
+                       throw new IllegalArgumentException("Empty package name: part <"+part+">, query <"+q+"> of "+uri);
+                   }
+                   data.setProperty(part, EMPTY);
+               }
+           }
+           data.validate();
+           return data;
+       }
+       
+       public final void validate() {
+           if(null == activityName) {
+               throw new RuntimeException("Activity is not NULL");
+           }
+           if(packages.size() == 0) {
+               throw new RuntimeException("Empty package list");
+           }
+       }
+   }
+   
+   public final static Intent getIntent(String action, DataSet data) {
+       data.validate();
+       return new Intent(action, data.getUri());
+   }
+   
+   public static void main(String[] args) {
+       if(args.length==0) {
+           args = new String[] {
+               SCHEME+"://"+HOST+"/com.jogamp.TestActivity?"+PKG+"=jogamp.pack1&"+PKG+"=javax.pack2&"+PKG+"=com.jogamp.pack3&jogamp.common.debug=true&com.jogamp.test=false",   
+               SCHEME+"://"+HOST+"/com.jogamp.TestActivity?"+PKG+"=jogamp.pack1&jogamp.common.debug=true&com.jogamp.test=false",   
+               SCHEME+"://"+HOST+"/com.jogamp.TestActivity?"+PKG+"=jogamp.pack1"   
+           };
+       }
+       for(int i=0; i<args.length; i++) {
+           String uri_s = args[i];
+           Uri uri0 = Uri.parse(uri_s);
+           DataSet data = DataSet.create(uri0);
+           if(null == data) {
+               System.err.println("Error: NULL JogAmpLauncherUtil: <"+uri_s+"> -> "+uri0+" -> NULL");
+           }
+           Uri uri1 = data.getUri();
+           if(!uri0.equals(uri1)) {
+               System.err.println("Error: Not equal: <"+uri_s+"> -> "+uri0+" -> "+uri1);
+           }
+       }
+   }
+   
+}
-- 
cgit v1.2.3