DEPRECATE


En Android, gracias a la técnica de programación Java conocida como reflection, es posible cargar código nativo Java y C/C++ dinámicamente en tiempo de ejecución.

Esto permite que el partner que lo desee pueda crear un plugin para XOne que interactúe con el framework mientras incluya la librería de interfaces xoneinterfaces.jar y las utilidades de desarrollo xoneutils.jar, disponible en xonesupport.
Antes de comenzar con el siguiente manual, se debe tener en cuenta lo siguiente.

  • Se asume que se tienen sólidos conocimientos de programación en Android.
  • Se asume que se conoce la técnica de reflection, aunque sea en las variantes Java de PC, y que se sabe que al ejecutarse en una ruta distinta a la que el compilador se esperaba puede darse algún error en runtime que el compilador no va a detectar a la hora de construir el plugin.
  • Entorno de desarrollo Eclipse. Concretamente la versión Eclipse Juno 4.2 SR2. Las opciones pueden cambiar de lugar y el comportamiento puede ser otro en otras versiones.
  • Android SDK revisión 22 ó superior.


  • La clase reflejada se cargará en el contexto del framework, por lo tanto tendrá los permisos de éste. Los permisos del androidmanifest.xml del plugin sólo son efectivos con las actividades del plugin que lancemos mediante startActivity(), que al fin y al cabo, son un proceso aparte, independiente del framework.
  • Los plugins viven durante toda la aplicación. Es decir, si se hacen nuevas llamadas de script createobject a nuestro objeto plugin, éste devuelve el mismo objeto cacheado. Si se necesita, crear un método que reinicialice las variables necesarias.
  • Si se incluyen librerías nativas C/C++, se debe incluir en la medida de lo posible versiones para el máximo número de procesadores distintos, como vienen siendo ARMv6, ARMv7 y x86.
  • Como target de nuestra aplicación, seleccionamos Android 2.1. Podemos seleccionar un target más alto, pero entonces debemos tener en cuenta que esa llamada/constante/motivo por el cual seleccionamos dicho target puede no existir en versiones previas, y como el framework sí se puede ejecutar en versiones de Android >= 2.1, entonces la aplicación puede explotar al llegar a ese punto. Se puede evitar que se ejecuten llamadas a APIs no existentes mediante una condición que compruebe en qué versión de Android se ejecuta.

Comenzamos creando un nuevo proyecto de Android en el Eclipse. Le damos un nombre interno, seleccionamos un target, añadimos iconos, no es necesario que creemos ninguna actividad.

  • Si se añade una actividad y la lanzamos desde la clase reflejada, esta ya tendrá su propio contexto.

Hacemos click derecho sobre el proyecto, pinchamos sobre Properties > Java Build Path. En la pestaña Order and Export, marcamos el checkbox Android Private Libraries y le damos a OK.

Ahora, en la pestaña Libraries, pinchamos en Add External JARs y seleccionamos el jar de las interfaces xoneinterfaces.jar y xoneutils.jar. Es importante añadirlas así, o de lo contrario la máquina Dalvik puede confundirse con las librerías internas del framework.

Ya casi estamos listos. Ahora, se puede interactuar con el framework de dos formas. Una es mediante código VBScript, y la otra es creando un prop de type=“IMG” que incrustará el objeto View de nuestro plugin. La primera forma es la más sencilla y comenzaremos por ahí.


Comenzamos creando en el plugin una nueva clase testclassscripting.java que extienda a :iruntimeobject, y que siga este esquema.

package com.xone.pluginpackage;
 
import java.sql.Date;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Hashtable;
import java.util.Locale;
 
import xone.interfaces.IRuntimeObject;
import xone.interfaces.IRuntimeScope;
import xone.interfaces.IRuntimeTypeInfo;
import xone.interfaces.RuntimeParameterType;
import xone.interfaces.RuntimeTypeInfoType;
import xone.interfaces.XoneScriptException;
import xone.runtime.scripting.XoneVBSTypeInfoHolder;
import android.content.Context;
 
import com.xone.android.utils.Utils;
import com.xone.interfaces.IXoneApp;
 
public class TestClassScripting implements IRuntimeObject {
 
	//Del runtime
	private XoneVBSTypeInfoHolder ti = null;
	private Hashtable<String, IRuntimeTypeInfo> m_lstTypeInfoList = null;
	private IXoneApp _app = null;
	private Context _context = null;
 
	public TestClassScripting(Context context, IXoneApp app) {
		m_lstTypeInfoList = new Hashtable<String, IRuntimeTypeInfo>();
		_app = app;
		_context = context;
		crearTipos();
	}
 
	private void crearTipos() {
		ti = new XoneVBSTypeInfoHolder("ShowInConsole", RuntimeTypeInfoType.RTTI_FUNCTION);
		ti.AddParam("textparameter", RuntimeParameterType.RTTI_PARAM_STRING, false);
		m_lstTypeInfoList.put(ti.getName().toLowerCase(Locale.US), ti);
	}
 
	@Override
	public String getName() {
		return "TestClassScripting";
	}
 
	@Override
	public String getDefaultMethod() {
		return Utils.EMPTY_STRING;
	}
 
	@Override
	public IRuntimeScope getScope() {
		return null;
	}
 
	@Override
	public IRuntimeTypeInfo GetTypeInfo(String ElementName) {
		String str = ElementName.toLowerCase(Locale.US);
		if (m_lstTypeInfoList.containsKey(str))
			return (IRuntimeTypeInfo) m_lstTypeInfoList.get(str);
		return null;
	}
 
	@Override
	public Object GetPropertyManager(String PropertyName,
			Object[] PropertyParams) throws XoneScriptException {
		return null;
	}
 
	@Override
	public Object Invoke(String FunctionName, int FunctionType, Object[] FunctionParams) throws Exception {
		String str = FunctionName.toLowerCase(Locale.US);
		if(str.equals("showinconsole")) {
			return ShowInConsole(FunctionParams);
		} else {
			throw new Exception("Método " + FunctionName + " no implementado");
		}
	}
 
	private Object ShowInConsole(Object[] FunctionParams) throws Exception {
		if(FunctionParams != null) {
			if(FunctionParams.length != 1) {
				throw new Exception("Error de ejecución, número incorrecto de parámetros");
			}
			String param = SafeToString(FunctionParams[0]);
			System.out.println("El mensaje que me ha llegado es: " + param);
		}
		return "";
	}
 
	public static String SafeToString(Object Source) {
 		try {
			if (Source == null)
				return Utils.EMPTY_STRING;
			if (Source instanceof Calendar){
				SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                		return dateformat.format(((Calendar)Source).getTime().getTime());
            		}
            		if (Source instanceof Date){
                		SimpleDateFormat dateformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                		return dateformat.format(((Date)Source).getTime());
            		}
            		if (Source instanceof Object[]) {
            			StringBuilder bld=new StringBuilder();
            			try {
					Object[] arr=(Object[]) Source;
	            			for (int i=0;i<arr.length;i++) {
	            				if (bld.length()>0)
	            					bld.append("\r\n");
	            				bld.append(arr[i]);
	        	    		}
            			} catch (Exception ex) {
					ex.printStackTrace();
				}
            			return bld.toString();
            		}
            		return String.valueOf(Source);
		} catch (Exception ex) {
			return "";
		}
	}
}


Añadimos los imports que nos pida el Eclipse pulsando Ctrl + Mayús + O.

En este ejemplo sencillo de plugin XOne, lo que hemos hecho ha sido crear un método llamado Create, que acepta una cadena de texto como parámetro y la imprime por la consola de debug de Android.
Compilamos este proyecto y le damos a ejecutar en el Eclipse. Nos advertirá de que no va a lanzar nada, pues el proyecto no tiene Activities, y que sólo instalará el .APK. Para el ejemplo nos vale con eso.

Nótese los métodos @Override, son parte esencial de la maquinaria de script y bastante descriptivos por sí solos. Son generalmente transparentes al usuario, pero hay tres métodos a tener en cuenta, pues el framework no sabe con qué clase de objetos trabajará:

getName Éste le devuelve al framework el nombre del plugin externo.
gettypeinfo En este método debemos devolver el Hashtable que contiene la definición de todos los métodos que vamos a exponer. En el método crearTipos() vemos un ejemplo de cómo debemos definir nuestros métodos. Es absolutamente imprescindible rellenar el xonevbstypeinfoholder si se quiere que las expresiones sean asignables.Cada addparam añade un nuevo parámetro, define si es opcional (tener en cuenta que por definición VBScript no permite sobrecarga de funciones), y de qué tipo es, si cadena, entero, booleano…
Invoke Este método será invocado por el framework cada vez que se invoque un método del objeto plugin que hemos creado. Nos devuelve el nombre de la función invocada. Tal y como se muestra en el ejemplo podemos usarlo para saber qué método interno invocar. Se deben llamar igual tanto en el xonevbstypeinfoholder como en nuestro script, y por convención, también en los métodos internos.


Ahora, en nuestra aplicación XOne, ejecutamos este script.

  Set obj01 = CreateObject("com.xone.testplugin,com.xone.pluginpackage.TestClassScripting")
  'En este momento ya está creado el objeto, si esta llamada ya ha ocurrido devolverá el mismo objeto y no invocará al constructor de nuevo.
  obj01.ShowInConsole "Hola mundo, saludos desde mi plugin XOne!"
  'Ahora nuestro método interno ShowInConsole (que por convención lo hemos llamado igual) ha sido invocado y la cadena "hola que tal" se muestra en el log de Android.
  Set obj01 = nothing


Como podemos ver, hemos especificado al createobject el nombre interno de nuestro paquete plugin y el nombre completo cualificado de la clase que vamos a cargar.

Para esta forma de plugin, la estructura de la clase debe seguir más o menos este esquema. Creamos la siguiente clase testclass.java para el ejemplo.

package com.xone.pluginpackage;
 
import android.content.Context;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.EditText;
import android.widget.LinearLayout;
 
import com.xone.interfaces.IEditBaseContent;
import com.xone.interfaces.IXoneAndroidApp;
import com.xone.interfaces.IXoneObject;
import com.xone.properties.PropData;
 
public class TestClass {
 
	private static View viewToAdd = null;
	private static LinearLayout newLinearLayout = null;
	private static Context context = null;
 
	public TestClass() {
		super();
	}
 
	public LinearLayout createView(
			Context _context,
			IXoneAndroidApp app,
			IEditBaseContent parent,
			IXoneObject objItem, 
			PropData prop,
			Boolean bStart,
			Boolean bEnd,
			Boolean bLast, 
			Boolean bDisableEdit,
			Object cssItem,
			Integer parentWidth,
			Integer parentHeight,
			Integer screenWidth,
			Integer screenHeight,
			Integer factor,
			Integer ZoomX,
			Integer ZoomY) {
		try {
			context = _context;
			newLinearLayout = new LinearLayout(context);
 
			String sProp = prop.getPropName();
			int nWidth = getDimensionFromString(context,
	    			objItem.FieldPropertyValue(sProp, "width"),
	    			app.getBaseWidth(),
	    			parentWidth,
	    			screenWidth);
			int nHeight = getDimensionFromString(context,
					objItem.FieldPropertyValue(sProp, "height"),
	    			app.getBaseHeight(),
	    			parentHeight,
	    			screenHeight);
 
			//Trabajar aquí con la view viewToAdd y editarla como se quiera.
			//No es necesario que sea una view, pero sí una clase que herede de ella.
 
			//Por ejemplo esto.
			EditText txt01 = new EditText(context);
			viewToAdd = txt01;
 
			newLinearLayout.addView(viewToAdd, nWidth, nHeight);
			StringBuilder sTag = new StringBuilder("##ITEM##");
			sTag.append(sProp);
			newLinearLayout.setTag(sTag.toString());
		} catch (Exception ex) {
			ex.printStackTrace();
			return null;
		}
		return newLinearLayout;
	}
 
	public static int getDimensionFromString(Context context,String value,int baseSize,int relativeSize,int maxSize) {
		try
		{
			if (TextUtils.isEmpty(value))
				return -1;
			int n=-1;
			n=value.indexOf("dp");
			if (n>=0)
				return convertFromDIPtoPixel(context,getRelativeDimension(Integer.valueOf(value.substring(0,n)),baseSize,maxSize));
			n=value.indexOf("p");
			if (n>=0)
				return getRelativeDimension(Integer.valueOf(value.substring(0,n)),baseSize,maxSize);
			n=value.indexOf("%");
			if (n>=0)
				if (maxSize>=0)
					if (relativeSize<0)
						return (int) (maxSize * ((float) Integer.valueOf(value.substring(0,n))/100));
					else
						return  (int) (relativeSize * ((float) Integer.valueOf(value.substring(0,n))/100)); //getRelativeDimension(Integer.valueOf((relativeSize * Integer.valueOf(value.substring(0,n)))/100),baseSize,maxSize);
				else
					return LayoutParams.WRAP_CONTENT;
			int val=Integer.valueOf(value);
			if (val>=0)
				if (baseSize>0)
					return getRelativeDimension(val,baseSize,maxSize);
				else
					return convertFromDIPtoPixel(context,getRelativeDimension(val,baseSize,maxSize));
			else
				return val;
		} catch (Exception e) {
			return -1;
		}
	}
 
	public static int getRelativeDimension(int value,int base,int maxSize) {
		if (base<0)
			return value;
		return Integer.valueOf(((value*maxSize)/base));
	}
 
	public static int convertFromDIPtoPixel(Context context,float dip)
	{
		int nPixel = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, context.getResources().getDisplayMetrics());
		return nPixel;
	}
}
<prop name="MAP_TESTPLUGIN" classid="com.xone.testplugin:com.xone.pluginpackage.TestClass" title="Test Plugin" labelwidth="0" type="IMG" visible="1" group="1" width="300" height="300" />

A la hora de desarrollar el plugin, podemos cometer el error de usar los recursos de nuestro paquete directamente, estilo R.string.loquesea, sin tener en cuenta que estamos en una clase reflejada y que los recursos locales son los del framework, no nuestro plugin. Esto es muy habitual en la programación Android y no es posible usarlo directamente con los plugin.

Para solucionar esto, hay que darle la vuelta con algo así:

	private Resources XOnePluginResources = null;
	try {
		XOnePluginResources = _context.getPackageManager().getResourcesForApplication("com.xone.android.plugintest");
	} catch (Exception ex) {
		ex.printStackTrace();
	}
	int resourceID = _context.getResources().getIdentifier(nombreDrawable, "drawable", "com.xone.android.plugintest");
	XOnePluginResources.getDrawable(resourceID);