diff --git a/app/src/main/assets/android/ELVA.lua b/app/src/main/assets/android/ELVA.lua new file mode 100644 index 0000000..b1a1bde --- /dev/null +++ b/app/src/main/assets/android/ELVA.lua @@ -0,0 +1,106 @@ +-- ELVA.lua +-- Expandable List View Adapter using a Lua table. +require 'android.import' + +return function(groups,overrides) + local ELA = {} + local my_observable = bind 'android.database.DataSetObservable'() + + function ELA.areAllItemsEnabled () + return true + end + + function ELA.getGroup (groupPos) + return groups[groupPos+1].group + end + + function ELA.getGroupCount () + return #groups + end + + function ELA.getChild (groupPos,childPos) + return groups[groupPos+1][childPos+1] + end + + function ELA.getChildrenCount (groupPos) + return #groups[groupPos+1] + end + + function ELA.getChildId (groupPos,childPos) + return childPos+1 + end + + function ELA.getCombinedChildId (groupPos,childPos) + return 1000*groupPos + childPos + end + + function ELA.getCombinedGroupId (groupPos) + return groupPos+1 + end + + function ELA.getGroupId (groupPos) + return groupPos+1 + end + + function ELA.hasStableIds () + return false + end + + function ELA.isChildSelectable (groupPos,childPos) + return true + end + + function ELA.isEmpty () + return ELA.getGroupCount() == 0 + end + + function ELA.onGroupCollapsed (groupPos) + --print('collapse',groupPos) + end + + function ELA.onGroupExpanded (groupPos) + --print('expand',groupPos) + end + + function ELA.registerDataSetObserver (observer) + my_observable:registerObserver(observer) + end + + function ELA.unregisterDataSetObserver (observer) + my_observable:unregisterObserver(observer) + end + + function ELA.notifyDataSetChanged() + my_observable:notifyChanged() + end + + function ELA.notifyDataSetInvalidated() + my_observable:notifyInvalidated() + end + + local getGroupView, getChildView = overrides.getGroupView, overrides.getChildView + if not getGroupView or not getChildView then + error('must override getGroupView and getChildView') + else + overrides.getGroupView = nil + overrides.getChildView = nil + getGroupView = android.safe(getGroupView) + getChildView = android.safe(getChildView) + end + + function ELA.getGroupView (groupPos,expanded,view,parent) + return getGroupView(ELA.getGroup(groupPos),groupPos,expanded,view,parent) + end + + function ELA.getChildView (groupPos,childPos,lastChild,view,parent) + return getChildView (ELA.getChild(groupPos,childPos),groupPos,childPos,lastChild,view,parent) + end + + -- allow for overriding any of the others... + for k,v in pairs(overrides) do + ELA[k] = v + end + + return proxy('android.widget.ExpandableListAdapter,sk.kottman.androlua.NotifyInterface',ELA) + +end diff --git a/app/src/main/assets/android/array.lua b/app/src/main/assets/android/array.lua new file mode 100644 index 0000000..cf8f410 --- /dev/null +++ b/app/src/main/assets/android/array.lua @@ -0,0 +1,297 @@ +--- Array class. +-- Useful array object specialized for numerical values, although most +-- operations work with arbitrary values as well. Functions taking functions +-- may accept _string lambdas_, which have either a placeholder '_' or two +-- placeholders '_1' or '_2'. As a special case, if the expression has no +-- identifier chars, it's assumed to be a binary operator. So '+' is equivalent +-- to '_1+_2' +-- The '+','-','*','/' operators are overloaded, so expressions like `2*x+1` or 'x+y' +-- work as expected. With two array arguments, '*' and '/' mean element-wise operations. +-- @module android.array + +local array = {} +array.__index = array + +local function _array (a) + return setmetatable(a,array) +end + +--- array constructor. +-- Useful for generating a set of values between `x1` and `x2`. +-- @param x1 initial value in range, or a table of values. (If that table +-- is itself an `array` then this acts like a copy constructor) +-- @param x2 final value in range +-- @param dx interval. +-- @treturn array +function array.new (x1,x2,dx) + local xvalues = {} + if x1 ~= nil then + if type(x1) == 'table' then + if getmetatable(x1) == array then + return x1:sub() + else + return _array(x1) + end + end + local i = 1 + for x = x1, x2 , dx do + xvalues[i] = x + i = i + 1 + end + end + return _array(xvalues) +end + +local _function_cache = {} + +local function _function_arg (f) + if type(f) == 'string' then + if _function_cache[f] then return _function_cache[f] end + if not f:match '[_%a]' then f = '_1'..f..'_2' end + local args = f:match '_2' and '_1,_2' or '_' + local chunk,err = loadstring('return function('..args..') return '..f..' end',f) + if err then error("bad function argument "..err,3) end + local fn = chunk() + _function_cache[f] = fn + return fn + end + return f +end + +local function _map (src,i1,i2,dest,j,f,...) + f = _function_arg(f) + for i = i1,i2 do + dest[j] = f(src[i],...) + j = j + 1 + end + return dest +end + +--- map a function over this array. +-- @param f a function, callable or 'string lambda' +-- @param ... any other arguments to the function +-- @treturn array +function array:map (f,...) + return _array(_map(self,1,#self,{},1,f,...)) +end + +--- apply the function to each element of this array. +-- @param f function as in `array.map` +function array:apply (f,...) + _map(self,1,#self,self,1,f,...) +end + +--- map a function over two arrays. +-- @param f as with `array.may` but must have at least two arguments. +-- @tparam array other +-- @treturn array +function array:map2 (f,other) + if #self ~= #other then error("arrays not the same size",2) end + f = _function_arg(f) + local res = {} + for i = 1,#self do + res[i] = f(self[i],other[i]) + end + return _array(res) +end + +--- find the index corresponding to `value`. +-- If it isn't an exact match, will give an index with a +-- _fractional part_. +-- @param value +-- @treturn number +function array:find_linear (value) + for i = 1,#self do + local v = self[i] + if v >= value then + if v > value then + local x1,x2 = self[i-1],self[i] + return i-1 + (value-x1)/(x2-x1) + else + return i -- on the nose! + end + end + end +end + +local floor = math.floor + +local function fsplit (x) + local i = floor(x) + return i, x - i +end + +--- get the numerical value at `idx`. +-- As with `array.find_linear` this index may have +-- a fractional part, allowing for linear interpolation. +-- @return number +function array:at (idx) + local i,delta = fsplit(idx) + local res = self[i] + if delta ~= 0 then + res = delta*(self[i+1]-self[i]) + end + return res +end + +array.append, array.remove = table.insert, table.remove + +--- extend this array with values from `other` +-- @tparam array other +function array:extend (other) + _map(other,1,#other,self,#self+1,'_') +end + +--- get a 'slice' of the array. +-- This works like `string.sub`; `i2` may be a negative integer. +-- Like with `array.at` the indices may have fractional parts. +function array:sub (i1,i2) + i1 = i1 or 1 + i2 = i2 or -1 + if i2 < 0 then i2 = #self + i2 + 1 end -- -1 is #self, and so forth + local res,j = {},1 + local int1,int2 = floor(i1),floor(i2) + if i1 ~= int1 then + res[j] = self:at(i1) + j = j + 1 + end + for i = int1,int2 do + res[j] = self[i] + j = j + 1 + end + if i2 ~= int2 then + res[j] = self:at(i2) + end + return _array(res) +end + +--- concatenation +-- @tparam list other +-- @treturn array +function array:__concat (other) + local res = self:sub(1) + res:extend(other) + return res +end + +local function mapm(a1,op,a2) + local M = type(a2)=='table' and array.map2 or array.map + return M(a1,op,a2) +end + +--- elementwise arithmetric operations +function array.__add(a1,a2) return mapm(a1,'_1 + _2',a2) end +function array.__sub(a1,a2) return mapm(a1,'_1 - _2',a2) end +function array.__div(a1,a2) return mapm(a1,'_1 / _2',a2) end +function array.__mul(a1,a2) return mapm(a2,'_1 * _2',a1) end + +function array:__tostring () + local n,cb = #self,']' + if n > 15 then + n = 15 + cb = '...]' + end + local strs = _map(self,1,n,{},1,tostring) + return '['..table.concat(strs,',')..cb +end + +--- adds a given method to this array for calling that method over all objects. +function array:forall_method (name) + self[name] = function (self,...) + for i = 1,#self do + local obj = self[i] + obj[name](obj,...) + end + end +end + +--- create an iterator over this array's values. +-- @param f optional function for filtering the +-- iterator +-- @return an iterator +function array:iter (f) + local i = 0 + if not f then + return function() + i = i + 1 + return self[i] + end + else + f = _function_arg(f) + return function() + local val + repeat + i = i + 1 + val = self[i] + until val == nil or f(val) + return val + end + end +end + +--- get the minimum and maximum values of this array. +-- The values must be comparable! +-- @return minimum +-- @return maximum +function array:minmax () + local min,max = math.huge,-math.huge + for i = 1,#self do + local val = self[i] + if val > max then max = val end + if val < min then min = val end + end + return min,max +end + +--- 'reduce' an array using a function. +-- @param f a function +function array:reduce (f) + f = _function_arg(f) + local res = self[1] + for i = 2,#self do + res = f(self[i],res) + end + return res +end + +--- sum all values in an array +function array:sum () + return self:reduce '+' +end + +--- scale an array so that the sum of its values is one. +-- @return this array +function array:normalize () + self:apply('/',self:sum()) + return self +end + +--- create a function which scales values between two ranges. +-- @number xmin input min +-- @number xmax input max +-- @number min output min +-- @number max output max +-- @treturn func of one argument +function array.scaler (xmin,xmax,min,max) + local xd = xmax-xmin + local scl = (max-min)/xd + return function(x) + return scl*(x - xmin) + min + end +end + +--- scale this array to the specified range +-- @number min output min +-- @number max output max +-- @return this array +function array:scale_to (min,max) + local xmin,xmax = self:minmax() + self:apply(array.scaler(xmin,xmax,min,max)) + return self +end + +setmetatable(array,{ + __call = function(_,...) return array.new(...) end +}) + +return array diff --git a/app/src/main/assets/android/async.lua b/app/src/main/assets/android/async.lua new file mode 100644 index 0000000..33e89ff --- /dev/null +++ b/app/src/main/assets/android/async.lua @@ -0,0 +1,86 @@ +------------- +-- Asynchronous utilities for running code on main thread +-- and grabbing HTTP requests and sockets in the background. +-- @module android.async + +require 'android.import' +local LS = service -- global for now +local PK = luajava.package +local L = PK 'java.lang' +local U = PK 'java.util' + +local async = {} + +--- create a Runnable from a function. +-- @func callback +-- @treturn L.Runnable +function async.runnable (callback) + return proxy('java.lang.Runnable',{ + run = function() + local ok,err = pcall(callback) + if not ok then LS:log(err) end + end + }) +end + +local handler = bind 'android.os.Handler'() +local runnable_cache = {} + +--- call a function on the main thread. +-- @func callback +-- @param later optional time value in milliseconds +function async.post (callback,later) + local runnable = runnable_cache[callback] + if not runnable then + -- cache runnable so we can delete it if needed + runnable = async.runnable(callback) + runnable_cache[callback] = runnable + elseif later ~= nil then + -- only keep one instance for delayed execution + handler:removeCallbacks(runnable) + end + if not later then + handler:post(runnable) + elseif type(later) == 'number' then + handler:postDelayed(runnable,later) + end +end + +function async.post_later (later,callback) + async.post(callback,later) +end + +function async.cancel_post (callback) + local runnable = runnable_cache[callback] + if runnable then + handler:removeCallbacks(runnable) + runnable_cache[callback] = nil + end +end + +--- read an HTTP request asynchronously. +-- @string request +-- @bool gzip +-- @func callback function to receive string result +function async.read_http(request,gzip,callback) + return LS:createLuaThread('android.http_async',L.Object{request,gzip},nil,callback) +end + +--- read lines from a socket asynchronously. +-- @string address +-- @number port +-- @func on_line called with each line read +-- @func on_error (optional) called with any error message +function async.read_socket_lines(address,port,on_line,on_error) + local args = U.HashMap() + args:put('addr',address) + args:put('port',port) + LS:createLuaThread('android.socket_async',args, + on_line,on_error or function(...) print(...) end + ) + return function() + args:get('socket'):close() + end +end + +return async diff --git a/app/src/main/assets/android/http_async.lua b/app/src/main/assets/android/http_async.lua new file mode 100644 index 0000000..df4c58d --- /dev/null +++ b/app/src/main/assets/android/http_async.lua @@ -0,0 +1,19 @@ +require 'android.import' +local utils = require 'android.utils' + +return function (thread,args) + local res + local url = bind'java.net.URL'(args[1]) + local connect = url:openConnection() + local f = connect:getInputStream() + if args[2] then -- GZIPd response! + local GZIPIS = bind'java.util.zip.GZIPInputStream' + local gf = GZIPIS(f) + print('gf',gf,GZIPIS) + res = utils.readstring(gf) + f:close() + else + res = utils.readstring(f) + end + return res +end diff --git a/app/src/main/assets/android/import.lua b/app/src/main/assets/android/import.lua new file mode 100644 index 0000000..edd6e2b --- /dev/null +++ b/app/src/main/assets/android/import.lua @@ -0,0 +1,192 @@ +--- Basic utilities for making LuaJava more convenient to use. +-- Note that many of these functions are currently global! +-- @module android.import + +local append,pcall,getmetatable = table.insert,pcall,getmetatable +local new, bindClass = luajava.new, luajava.bindClass +local Array + +local function new_len (a) + return Array:getLength(a) +end + +local function new_tostring (o) + return o:toString() +end + +local function primitive_type (t) + local ok,res = pcall(function() return t.TYPE end) + if ok then return res end +end + +local function call (t,...) + local obj,stat + if select('#',...) == 1 and type(select(1,...))=='table' then + local T = select(1,...) + t = primitive_type(t) or t + if #T == 0 and T.n then -- e.g. String{n=10} + T = T.n + end + obj = make_array(t,T) + getmetatable(obj).__len = new_len + else + stat,obj = pcall(new,t,...) + end + getmetatable(obj).__tostring = new_tostring + return obj +end + +local function massage_classname (classname) + if classname:find('_') then + classname = classname:gsub('_','$') + end + return classname +end + +local classes = {} + +--- import a Java class. +-- Like `luajava.bindClass` except it caches classes by class name +-- and makes the result callable, ending the need for explicit `luajava.new` calls. +-- Inner classes are not accessed with a '$' but with '_'. +-- If a single Lua table is passed to such a constructor, this means +-- 'make an array of these values'; if the table is just `{n=number}`, then +-- make an array of this size. If you use an object wrapper like `java.lang.Double` +-- then it will create an array of the _primitive_ type. +-- @see make_array +-- @param klassname - the fully qualified Java class name +function bind (klassname) + local res + klassname = massage_classname(klassname) + if not classes[klassname] then + local res,class = pcall(bindClass,klassname) + if res then + local mt = getmetatable(class) + mt.__call = call + classes[klassname] = class + return class + else + return nil,class + end + else + return classes[klassname] + end +end + +local function import_class (classname,packagename,T) + local class,err = bind(packagename) + if class then + T[classname] = class + end + return class,err +end + +local lookupMT = { + __index = function(T,classname) + classname = massage_classname(classname) + for i,p in ipairs(T._packages) do + local class = import_class(classname,p..classname,T) + if class then return class end + end + error("cannot find "..classname) + end +} + +--- represents a Java package. +-- @param P the full package path +-- @param T optional table to receive the cached results +-- @return package +-- @usage L = luajava.package 'java.lang'; s = L.String() +function luajava.package (P,T) + local pack = T or {} + if P ~= '' then P = P..'.' end + pack._packages={P} + setmetatable(pack,lookupMT) + return pack +end + +--- convenient way to access Java classes globally. +-- However, not a good idea in larger programs. You will have +-- to call `luajava.set_global_package_search` or set the global +-- `GLOBAL_PACKAGE_SEARCH` before requiring `import`. +-- @param P a package path to add to the global lookup paths. +function import (P) + if not rawget(_G,'_packages') then + error('global package lookup not initialized') + end + local i = P:find('%.%*$') + if i then -- a wildcard; put into the package list, including the final '.' + append(_G._packages,P:sub(1,i)) + else + local classname = P:match('([%w_]+)$') + local klass = import_class(classname,P) + if not klass then + error("cannot find "..P) + end + return klass + end +end + +--- enable global lookup of java classes +function luajava.set_global_package_search() + luajava.package('',_G) +end + +--- create a 'class' implementing a Java interface. +-- Thin wrapper over `luajava.createProxy` +function proxy (classname,obj) + classname = massage_classname(classname) + -- if the classname contains dots it's assumed to be fully qualified + if classname:find('.',1,true) then + return luajava.createProxy(classname,obj) + end + -- otherwise, it must lie on the package path! + if rawget(_G,'_packages') then + for i,p in ipairs(_G._packages) do + local ok,res = pcall(luajava.createProxy,p..classname, obj) + if ok then return res end + end + end + error ("cannot find "..classname) +end + +--- return a Lua iterator over an Iterator. +function enum(e) + return function() + if e:hasNext() then + return e:next() + end + end +end + +Array = bind 'java.lang.reflect.Array' + +--- create a Java array. +-- @param Type Java type +-- @param list table of Lua values, or a size +function make_array (Type,list) + local len + local init = type(list)=='table' + if init then + len = #list + else + len = list + end + local arr = Array:newInstance(Type,len) + if arr == nil then return end + if init then + for i,v in ipairs(list) do + arr[i] = v + end + end + return arr +end + + +if GLOBAL_PACKAGE_SEARCH then + luajava.set_global_package_search() + import 'java.lang.*' + import 'java.util.*' +end + + diff --git a/app/src/main/assets/android/init.lua b/app/src/main/assets/android/init.lua new file mode 100644 index 0000000..f37992a --- /dev/null +++ b/app/src/main/assets/android/init.lua @@ -0,0 +1,981 @@ +----------------- +-- AndroLua Activity Framework +-- @module android +require 'android.import' + +-- public for now... +android = {} +local android = android + +local LS = service -- which is global for now +local LPK = luajava.package +local L = LPK 'java.lang' +local C = LPK 'android.content' +local W = LPK 'android.widget' +local app = LPK 'android.app' +local V = LPK 'android.view' +local A = LPK 'android' +local G = LPK 'android.graphics' + +local append = table.insert + +--- Utilities +-- @section utils + +--- split a string using a delimiter. +-- @string s +-- @string delim +-- @treturn {string} +function android.split (s,delim) + local res,pat = {},'[^'..delim..']+' + for p in s:gmatch(pat) do + append(res,p) + end + return res +end +-- mebbe have a module for these useful things (or Microlight?) +local split = android.split + +--- copy items from `src` to `dest`. +-- @tab src +-- @tab dest (may be `nil`, in which case create) +-- @bool dont_overwrite if `true` then don't overwrite fields in `dest` +-- that already exist. +function android.copytable (src,dest,dont_overwrite) + dest = dest or {} + for k,v in pairs(src) do + if not dont_overwrite or dest[k]==nil then + dest[k] = v + end + end + return dest +end + +local app_package + +local function get_app_package (me) + if not app_package then + local package_name = me.a:toString():match '([^@]+)':gsub ('%.%a+$','') + app_package = LPK(package_name) + end + me.app = app_package + return app_package +end + +--- return a Drawable from an name. +-- @param me +-- @string icon_name +-- @treturn G.Drawable +function android.drawable (me,icon_name) + local a = me.a + -- icon is [android.]NAME + local dclass + local name = icon_name:match '^android%.(.+)' + if name then + dclass = A.R_drawable + else + dclass = get_app_package(me).R_drawable + name = icon_name + end + local did = dclass[name] + if not did then error(name..' is not a drawable') end + return a:getResources():getDrawable(did) +end + +--- wrap a callback safely. +-- Error messages will be output using print() or logger, depending. +-- @func callback the function to be called in a protected context. +function android.safe (callback) + return function(...) + local ok,res = pcall(callback,...) + if not ok then + LS:log(res) + elseif res ~= nil then + return res + end + end +end + +--- parse a colour value. +-- @param c either a number (passed through) or a string like #RRGGBB, +-- #AARRGGBB or colour names like 'red','blue','black','white' etc +function android.parse_color(c) + if type(c) == 'string' then + local ok + ok,c = pcall(function() return G.Color:parseColor(c) end) + if not ok then + LS:log("converting colour "..tostring(c).." failed") + return G.Color.WHITE + end + end + return c +end + +local TypedValue = bind 'android.util.TypedValue' + +--- parse a size specification. +-- @param me +-- @param size a number is interpreted as pixels, otherwise a string like '20sp' +-- or '30dp'. (See android.util.TypedValue.COMPLEX_UNIT_*) +-- @return size in pixels +function android.parse_size(me,size) + if type(size) == 'string' then + local sz,unit = size:match '(%d+)(.+)' + sz = tonumber(sz) + if unit == 'dp' then unit = 'dip' end -- common alias + unit = TypedValue['COMPLEX_UNIT_'..unit:upper()] + size = TypedValue:applyDimension(unit,sz,me.metrics) + end + return size +end + +--- suppress initial soft keyboard with edit view. +-- @param me +function android.no_initial_keyboard(me) + local WM_LP = bind 'android.view.WindowManager_LayoutParams' + me.a:getWindow():setSoftInputMode(WM_LP.SOFT_INPUT_STATE_HIDDEN) +end + +--- make the soft keyboard go bye-bye. +-- @param v an edit view +function android.dismiss_keyboard (v) + local ime = v:getContext():getSystemService(C.Context.INPUT_METHOD_SERVICE) + ime:hideSoftInputFromWindow(v:getWindowToken(),0) +end + + +--- return a lazy table for looking up controls in a layout. +-- @param me +function android.wrap_widgets (me) + local rclass = get_app_package(me).R_id + return setmetatable({},{ + __index = function(t,k) + local c = me.a:findViewById(rclass[k]) + rawset(t,k,c) + return c + end + }) +end + +--- set the content view of this activity using the layout name. +-- @param me +-- @param name a layout name in current project +function android.set_content_view (me,name) + me.a:setContentView(get_app_package(me).R_layout[name]) +end + +--- make a `V.View.OnClickListener`. +-- @param me +-- @func callback a Lua function +function android.on_click_handler (me,callback) + return (proxy('android.view.View_OnClickListener',{ + onClick = android.safe(callback) + })) +end + +--- attach a click handler. +-- @param me +-- @param b a widget +-- @func callback a Lua function +function android.on_click (me,b,callback) + b:setOnClickListener(me:on_click_handler(callback)) +end + +--- make a Vew.OnLongClickListener. +-- @param me +-- @func callback a Lua function +function android.on_long_click_handler (me,callback) + return (proxy('android.view.View_OnLongClickListener',{ + onClick = android.safe(callback) + })) +end + +--- attach a long click handler. +-- @param me +-- @tparam widget b +-- @func callback a Lua function +function android.on_long_click (me,b,callback) + b:setOnLongClickListener(me:on_long_click_handler(callback)) +end + +--- make an AdapterView.OnItemClickListener. +-- @param me +-- @tparam ListView lv +-- @func callback a Lua function +function android.on_item_click (me,lv,callback) + lv:setOnItemClickListener(proxy('android.widget.AdapterView_OnItemClickListener',{ + onItemClick = android.safe(callback) + })) +end + + +local option_callbacks,entry_table = {},{} + +local function create_menu (me,is_context,t) + + local mymod = me.mod + + local view,on_create,on_select + if is_context then + view = t.view + if not view then error("must provide view for context menu!") end + me.a:registerForContextMenu(view) + on_create, on_select = 'onCreateContextMenu','onContextItemSelected' + else + on_create, on_select = 'onCreateOptionsMenu','onOptionsItemSelected' + end + + local entries = {} + for i = 1,#t,2 do + local label,icon = t[i] + -- label is TITLE[|ICON] + local title,icon_name = label:match '([^|]+)|(.+)' + if not icon_name then + title = label + else + if is_context then error 'cannot set an icon in a context menu' end + icon = me:drawable(icon_name) + end + local entry = {title=title,id=#option_callbacks+1,icon = icon} + append(entries,entry) + append(option_callbacks,t[i+1]) + end + + entry_table[view and view:getId() or 0] = entries + + -- already patched the activity table! + if is_context and mymod.onCreateContextMenu then return end + + mymod[on_create] = function (menu,v) + local entries = entry_table[v and v:getId() or 0] + local NONE = menu.NONE + for _,entry in ipairs(entries) do + local item = menu:add(NONE,entry.id,NONE,entry.title) + if entry.icon then + item:setIcon(entry.icon) + end + end + return true + end + + mymod[on_select] = function (item) + local id = item:getItemId() + option_callbacks[id](item,id) + return true + end + +end + +--- Properties +-- @section properties +--- view properties +-- @tfield ViewProperties theme provides default properties, does not override +-- @color background colour of view's background +-- @int paddingLeft inner padding +-- @int paddingRight +-- @int paddingBottom +-- @int paddingTop +-- @table android.ViewProperties + +--- Text Properties +-- @color textColor colour of text +-- @int size +-- @int maxLines +-- @int minLines +-- @int lines +-- @string textStyle +-- @string typeface +-- @string gravity +-- @string inputType +-- @bool scrollable +-- @drawable drawableLeft +-- @drawable drawableRight +-- @drawable drawableTop +-- @drawable drawableBottom +-- @table android.TextProperties + +--- Menus and Alerts +-- @section menus + +--- create an options menu. +-- @param me +-- @param t a table containing 2n items; each row is label,callback. +-- The label is either TITLE or TITLE:ICON; if ICON is prefixed by +-- 'android.' then we look up a stock drawable, otherwise in this +-- app package. +function android.options_menu (me,t) + create_menu(me,false,t) +end + +--- create a context menu on a particular view. +-- @param me +-- @param t a table containing 2n items; each row is label,callback. +-- You cannot set icons on these menu items and `t.view` must be +-- defined! +function android.context_menu (me,t) + create_menu(me,true,t) +end + +--- show an alert. +-- @param me +-- @string title caption of dialog 'label[|drawable]' like with menus +-- above, where 'drawable' is '[android.]name' +-- @string kind either 'ok' or 'yesno' +-- @param message text within dialog, or a custom view +-- @func callback optional Lua function to be called +function android.alert(me,title,kind,message,callback) + local Builder = bind 'android.app.AlertDialog_Builder' + local db = Builder(me.a) + local parts = split(title,'|') + db:setTitle(parts[1]) + if parts[2] then + db:setIcon(me:drawable(parts[2])) + end + if type(message) == 'string' then + db:setMessage(message) + else + db:setView(message) + end + callback = callback or function() end -- for now + local listener = proxy('android.content.DialogInterface_OnClickListener', { + onClick = android.safe(callback) + }) + if kind == 'ok' then + db:setNeutralButton("OK",listener) + elseif kind == 'yesno' then + db:setPositiveButton("Yes",listener) + db:setNegativeButton("No",listener) + end + dlg = db:create() + dlg:setOwnerActivity(me.a) + dlg:show() +end + +--- show a toast +-- @param me +-- @string text to show +-- @bool long `true` if you want a long toast! +function android.toast(me,text,long) + W.Toast:makeText(me.a,text,long and W.Toast.LENGTH_LONG or W.Toast.LENGTH_SHORT):show() +end + +--- Creating Views +-- @section views + +function android.give_id (me,w) + if w:getId() == -1 then + if not me.next_id then + me.next_id = 1 + end + w:setId(me.next_id) + me.next_id = me.next_id + 1 + end + return w +end + + +--- set View properties. +-- @see ViewProperties +-- @param me +-- @tparam V.View v +-- @param args table of properties +function android.setViewArgs (me,v,args) + if args.id then + v:setId(args.id) + end + -- @doc me.theme is an optional table of view parameters + -- that provides defaults; does not override existing parameters. + if me.theme then + android.copytable(me.theme,args,true) + end + if args.background then + v:setBackgroundColor(android.parse_color(args.background)) + end + if args.paddingLeft or args.paddingRight or args.paddingBottom or args.paddingTop then + local L,R,B,T = v:getPaddingLeft(), v:getPaddingRight(), v:getPaddingBottom(), v:getPaddingTop() + if args.paddingLeft then + L = me:parse_size(args.paddingLeft) + end + if args.paddingTop then + T = me:parse_size(args.paddingTop) + end + if args.paddingRight then + R = me:parse_size(args.paddingRight) + end + if args.paddingBottom then + B = me:parse_size(args.paddingBottom) + end + v:setPadding(L,T,R,B) + end + return me:give_id(v) +end + +local function parse_input_type (input) + return require 'android.input_type' (input) +end + +local SMM + +--- set properties specific to `TextView` and `EditText`. +-- @see TextProperties +-- @param me +-- @tparam W.TextView txt +-- @param args table of properties +function android.setEditArgs (me,txt,args) + me:setViewArgs(txt,args) + if args.textColor then + txt:setTextColor(android.parse_color(args.textColor)) + end + if args.size then + txt:setTextSize(me:parse_size(args.size)) + end + if args.maxLines then + txt:setMaxLines(args.maxLines) + end + if args.minLines then + txt:setMinLines(args.minLines) + end + if args.lines then + txt:setLines(args.lines) + end + local Typeface,tface = G.Typeface + if args.typeface or args.textStyle then + if args.typeface then + tface = Typeface:create(args.typeface,Typeface.NORMAL) + else + tface = txt:getTypeface() + end + if args.textStyle then + local style = args.textStyle:upper() + tface = Typeface:create(tface,Typeface[style]) + end + txt:setTypeface(tface) + end + if args.inputType then -- see android:inputType + txt:setInputType(parse_input_type(args.inputType)) + if args.inputType == 'textMultiLine' then + if args.gravity == nil then -- sensible default gravity + args.gravity = 'top|left' + end + if args.scrollable then + SMM = SMM or bind 'android.text.method.ScrollingMovementMethod':getInstance() + txt:setMovementMethod(SMM) + end + end + end + if args.focus == false then + txt:setFocusable(false) + end + if args.gravity then + local gg = split(args.gravity,'|') + local g = 0 + for _,p in ipairs(gg) do + g = g + V.Gravity[p:upper()] + end + txt:setGravity(g) + end + if args.scrollable then + local smm = bind 'android.text.method.ScrollingMovementMethod':getInstance() + txt:setMovementMethod(smm) + end + local L,T,R,B = args.drawableLeft,args.drawableTop,args.drawableRight,args.drawableBottom + local compound = L or T or R or B + if compound then + local def = type(compound)=='number' and 0 or nil + txt:setCompoundDrawablesWithIntrinsicBounds(L or def,T or def,R or def,B or def) + end +end + +-- http://stackoverflow.com/questions/3506696/auto-scrolling-textview-in-android-to-bring-text-into-view?rq=1 +function android:scroll_to_end (txt) + local layout = txt:getLayout() + if layout then + local amt = layout:getLineTop(txt:getLineCount()) - txt:getHeight() + if amt > 0 then + txt:scrollTo(0,amt) + end + end +end + + +local function handle_args (args) + if type(args) ~= 'table' then + args = {args} + end + return args[1] or args.text or '',args +end + +--- create a text view style. +-- @param me +-- @param args as in `android.textView` +-- @return a function that creates a text view from a string +function android.textStyle (me,args) + return function(text) + local targs = android.copytable(args) + targs[1] = text + return me:textView(targs) + end +end + +local function tab_content (callback) + return proxy('android.widget.TabHost_TabContentFactory',{ + createTabContent = callback + }) +end + +local function tab_view (v) + return tab_content(function(s) return v end) +end + +--- make a tab view. +-- @param me +-- @param tabs a list of items; +-- +-- - `tag` a string identifying the tab +-- - `label` a string label, or the same args passed to `android.textView`. +-- If `tabs.style` is defined, then the string is processed using it. +-- The default is to use `tag` +-- - `content` a View, or a function returning a View, or a Lua activity module name +-- +-- @treturn W.TabHost +function android.tabView (me,tabs) + local views = {} + me:set_content_view 'tabs' + local tabhost = me.a:findViewById(A.R_id.tabhost) + + local lam = bind 'android.app.LocalActivityManager'(me.a,false) + lam:dispatchCreate(me.state) + tabhost:setup(lam) + + if tabs.changed then + tabhost:setOnTabChangedListener(proxy('android.widget.TabHost_OnTabChangeListener',{ + onTabchanged = android.safe(tabs.changed) + })) + end + + for _,item in ipairs(tabs) do + local tag,content = item.tag,item.content + local label = item.label or tag + local spec = tabhost:newTabSpec(tag) + + -- label is either a string, a table to be passed to textView, + -- or a view or resource id. + if type(label) == 'table' then + label = me:textView(label) + elseif type(label) == 'string' and tabs.style then + label = tabs.style(label) + end + spec:setIndicator(label) + + -- content is either a string (a module name) or a function + -- that generates a View, or a View itself + if type(content) == 'string' then + local mod = require (content) + content = tab_content(function(tag) + if not views[tag] then + views[tag] = mod.onCreate(me.a,item.data,me.state) + end + return views[tag] + end) + elseif type(content) == 'function' then + content = tab_content(content) + elseif type(content) == 'userdata' then + content = tab_view(content) + end + spec:setContent(content) + tabhost:addTab(spec) + end + + return tabhost +end + +--- create a button. +-- @param me +-- @param text of button +-- @param callback a Lua function or an existing click listener. +-- This is passed the button as its argument +-- @treturn W.Button +function android.button (me,text,callback) + local b = W.Button(me.a) + b:setText(text) + if type(callback) == 'function' then + callback = me:on_click_handler(callback) + end + b:setOnClickListener(callback) + ---? set_view_args(b,args,me) + return me:give_id(b) +end + +--- create an edit widget. +-- @param me +-- @param args either a string (which is usually the hint, or the text if it +-- starts with '!') or a table with fields `textColor`, `id`, `background` or `size` +-- @treturn W.EditText +function android.editText (me,args) + local text,args = handle_args(args) + local txt = W.EditText(me.a) + if text:match '^!' then + txt:setText(text:sub(1)) + else + txt:setHint(text) + end + me:setEditArgs(txt,args) + return txt +end + +-- create a text view. +-- @param me +-- @param args as with `android.editText` +-- @treturn W.TextView +function android.textView (me,args) + local text,args = handle_args(args) + local txt = W.TextView(me.a) + txt:setText(text) + me:setEditArgs(txt,args) + return txt +end + +--- create an image view. +-- @param me +-- @param args table of properties to be passed to `android.setViewArgs` +-- @treturn W.ImageView +function android.imageView(me,args) + local text,args = handle_args(args) + local image = W.ImageView(me.a) + return me:setViewArgs(image,args) +end + +--- create a simple list view. +-- @param me +-- @param items a list of strings +-- @treturn W.ListView +function android.listView(me,items) + local lv = W.ListView(me.a) + local adapter = W.ArrayAdapter(me.a, + --A.R_layout.simple_list_item_checked, + A.R_layout.simple_list_item_1, + A.R_id.text1, + L.String(items) + ) + lv:setAdapter(adapter) + return me:give_id(lv) +end + +--- create a spinner. +-- @param me +-- @param args list of strings; if there is a `prompt` field it +-- will be used as the Spinner prompt. Alternatively strings are found +-- in `options` field +-- @treturn W.Spinner +function android.spinner (me,args) + local items = args.options or args + local s = W.Spinner(me.a) + local sa = W.ArrayAdapter(me.a, + A.R_layout.simple_spinner_item, + A.R_id.text1, + L.String(items) + ) + sa:setDropDownViewResource(A.R_layout.simple_spinner_dropdown_item) + s:setAdapter(sa) + if items.prompt then + s:setPrompt(items.prompt) + end + return me:give_id(s) +end + +--- create a check box. +-- @param me +-- @param args as in `android.imageView` +-- @treturn W.CheckBox +function android.checkBox (me,args) + local text,args = handle_args(args) + local check = W.CheckBox(me.a) + check:setText (text) + return me:setViewArgs(check,args) +end + +--- create a toggle button. +-- @param me +-- @param args must have `on` and `off` labels; otherwise as in `android.textView` +-- @treturn W.ToggleButton +function android.toggleButon (me,args) + local tb = W.ToggleButton(me.a) + tb:setTextOn (args.on) + tb:setTextOff (args.off) + return me:setViewArgs(tb,args) +end + +--- create a radio group. +-- @param me +-- @param items a list of strings +-- @treturn W.RadioGroup +-- @treturn {W.RadioButton} +function android.radioGroup (me,items) + local rg = W.RadioGroup(me.a) + for i,item in ipairs(items) do + local b = W.RadioButton(me.a) + b:setText(item) + rg:addView(b) + items[i] = b + end + return rg,unpack(items) +end + +--- create a Lua View. +-- @param me +-- @param t may be a drawing function, or a table that defines `onDraw` +-- and optionally `onSizeChanged`. It will receive the canvas. +-- @treturn .LuaView +function android.luaView(me,t) + if type(t) == 'function' then + t = { onDraw = t } + end + return me:give_id(LS:launchLuaView(me.a,t)) +end + +local function parse_gravity (s) + if type(s) == 'string' then + return V.Gravity[s:upper()] + else + return s + end +end + +local function linear (me,vertical,t) + local LL = not t.radio and W.LinearLayout or W.RadioGroup + local LP = W.LinearLayout_LayoutParams + local wc = LP.WRAP_CONTENT + local fp = LP.FILL_PARENT + local xp, yp, parms + if vertical then + xp = fp; yp = wc; + else + xp = wc; yp = fp + end + local margin + if t.margin then + if type(t.margin) == 'number' then + t.margin = {t.margin,t.margin,t.margin,t.margin} + end + margin = t.margin + end + local ll = LL(me.a) + ll:setOrientation(vertical and LL.VERTICAL or LL.HORIZONTAL) + for i = 1,#t do + local w, gr = t[i] + if type(w) == 'userdata' then + local spacer + if i < #t and type(t[i+1])~='userdata' then + local mods = t[i+1] + local weight,gr,nofill,width + if type(mods) == 'string' then + if mods == '+' then + weight = 1 + elseif mods == '...' then + spacer = true + end + elseif type(mods) == 'table' then + weight = mods.weight + nofill = mods.fill == false + gr = parse_gravity(mods.gravity) + width = mods.width or mods.height + end + local axp,ayp = xp,yp + if nofill then + if vertical then axp = wc else ayp = wc end + end + if width then + width = me:parse_size(width) + if not vertical then axp = width else ayp = width end + end + parms = LP(axp,ayp,weight or 0) + i = i + 1 + else + parms = LP(xp,yp) + end + if margin then + for i=1,4 do margin[i] = me:parse_size(margin[i]) end + parms:setMargins(margin[1],margin[2],margin[3],margin[4]) + end + if gr then + parms.gravity = gr + end + ll:addView(w,parms) + if spacer then + ll:addView(me:textView'',LP(xp,yp,10)) + end + end + end + me:setViewArgs(ll,t) + return ll +end + +--- create a vertical layout. +-- @param me +-- @param t a list of controls, optionally separated by layout strings or tables +-- for example, `{w1,'+',w2} will give `w1` a weight of 1 in the layout; +-- tables of form {width=number,fill=false,gravity=string,weight=number} +-- Any fields are processed as in 'android.setViewArgs` +-- @treturn W.LinearLayout +function android.vbox (me,t) + local vbox = linear(me,true,t) + if t.scrollable then + local hs = W.ScrollView(me.a) + hs:addView(vbox) + return hs + else + return vbox + end +end + +--- create a horizontal layout. +-- @param me +-- @param t a list of controls, as in `android.vbox`. +-- @treturn W.LinearLayout +function android.hbox (me,t) + local hbox = linear(me,false,t) + if t.scrollable then + local hs = W.HorizontalScrollView(me.a) + hs:addView(hbox) + return hs + else + return hbox + end +end + +local function lua_adapter(me,items,impl) + if type(impl) == 'function' then + impl = { getView = impl; items = items } + end + return LS:createLuaListAdapter(items,impl or me) +end + +--- create a Lua list view. +-- @param me +-- @param items a list of Lua values +-- @param impl optional implementation - not needed if `me` +-- has a getView function. May be a function, and then it's +-- assumed to be getView. +-- @treturn W.ListView +-- @treturn .LuaListAdapter +function android.luaListView (me,items,impl) + local adapter = lua_adapter(me,items,impl) + local lv = W.ListView(me.a) + lv:setAdapter(adapter) + lv:setTag(items) + return me:give_id(lv), adapter +end + +-- create a Lua expandable list view +-- @param me +-- @param items a list of lists, where each sublist +-- has a `group` field for the corresponding group data. +-- @param impl a table containing at least `getGroupView` and `getChildView` +-- implementations. (see `example.ela.lua`) +-- @treturn W.ListView +-- @treturn W.ExpandableListAdapter +function android.luaExpandableListView (me,items,impl) + local onChildClick = impl.onChildClick + impl.onChildClick = nil + + local adapter = require 'android.ELVA' (items,impl) + local elv = W.ExpandableListView(me.a) + elv:setAdapter(adapter) + + if onChildClick then + onChildClick = android.safe(onChildClick) + elv:setOnChildClickListener(proxy('android.widget.ExpandableListView_OnChildClickListener',{ + onChildClick = function(parent,view,g,c,id) + local res = onChildClick(adapter:getChild(g,c),view,g,c,id) + return res or false -- ensure we always return a boolean! + end + })) + end + return me:give_id(elv), adapter +end + +--- create a Lua grid view. +-- @param me +-- @param items a table of Lua values +-- @number ncols number of columns (-1 for as many as possible) +-- @param impl optional implementation - not needed if `me` +-- has a getView function. May be a function, and then it's +-- assumed to be getView. +-- @return W.GridView +-- @treturn .LuaListAdapter +function android.luaGridView (me,items,ncols,impl) + local adapter = lua_adapter(me,items,impl) + local lv = W.GridView(me.a) + lv:setNumColumns(ncols or -1) + lv:setAdapter(adapter) + return me:give_id(lv), adapter +end + +--- Acitivies. +-- @section activities + +--- launch a Lua activity. +-- @param me +-- @param mod a Lua module name that defines the activity +-- @param arg optional extra value to pass to activity +function android.luaActivity (me,mod,arg) + return LS:launchLuaActivity(me.a,mod,arg) +end + +local handlers = {} + +--- start an activity with a callback on result. +-- Wraps `startActivityForResult`. +-- @param me +-- @tparam Intent intent +-- @func callback to be called when the result is returned. +function android.intent_for_result (me,intent,callback) + append(handlers,callback) + me.a:startActivityForResult(intent,#handlers) +end + +function android.onActivityResult(request,result,intent,mod_handler) + print('request',request,intent) + local handler = handlers[request] + if handler then + handler(request,result,intent) + table.remove(handlers,request) + elseif mod_handler then + mod_handler(request,result,intent) + else + android.activity_result = {request,result,intent} + end +end + +--- make a new AndroLua module. +function android.new() + local mod = {} + mod.onCreate = function (activity,arg,state) + local me = {a = activity, mod = mod, state = state} + for k,v in pairs(android) do me[k] = v end + -- want any module functions available from the wrapper + setmetatable(me,{ + __index = mod + }) + android.me = me -- useful for debugging and non-activity-specific context + mod.me = me + mod.a = activity + me.metrics = activity:getResources():getDisplayMetrics() + get_app_package(me) -- initializes me.app + local view = mod.create(me,arg) + mod._view = view + return view + end + local oldActivityResult = mod.onActivityResult + local thisActivityResult = android.onActivityResult + if oldActivityResult then + mod.onActivityResult = function(r,R,i) + thisActivityResult(r,R,i,oldActivityResult) + end + else + mod.onActivityResult = thisActivityResult + end + return mod +end + +return android diff --git a/app/src/main/assets/android/input_type.lua b/app/src/main/assets/android/input_type.lua new file mode 100644 index 0000000..86eb3c3 --- /dev/null +++ b/app/src/main/assets/android/input_type.lua @@ -0,0 +1,70 @@ +-- parse inputType specifications + +local T = luajava.package 'android.text' + +local function decamelify (rest) + rest = rest:gsub('(%l)(%u)',function(l,u) + return l..'_'..u + end) + return rest:upper() +end + +-- known input classes. Some of these are synthetic, e.g. +-- 'date' is short for 'datetimeDate' +local kinds = {text=true,number=true,phone=true,datetime=true, + date={'datetime','date'},time={'datetime','time'} +} + +-- these are the known flag patterns for the input classes above; +-- anything else is a variation +local variations = { + TEXT = {'^CAP','^AUTO','^MULTI','^NO'}, + NUMBER = {'^PASSWORD'}, +} + +local function check_flag (kind,rest) + if not variations[kind] then return false end + for _,v in ipairs(variations[kind]) do + if rest:match(v) then return true end + end +end + +return function (input) + local tt = android.split(input,'|') + local IT = 0 + local type_flag = {} + for _,t in ipairs(tt) do + local kind,rest = t:match '(%l+)(.*)' + local what = kinds[kind] + if type(what) == 'table' then + kind = what[1] + rest = what[2] + end + if what then + kind = kind:upper() + if not type_flag[kind] then -- only add this flag once! + --print('kind',kind) + IT = IT + T.InputType['TYPE_CLASS_'..kind] + type_flag[kind] = true + end + if rest ~= '' then + rest = decamelify(rest) + if check_flag(kind,rest) then + rest = '_FLAG_'..rest + else + rest = '_VARIATION_'..rest + end + --print('rest',rest) + local ok,type = pcall(function() + return T.InputType['TYPE_'..kind..rest] + end) + if not ok then error('unknown inputType flag') end + IT = IT + type + end + else + error('unknown inputType '..kind) + end + end + return IT +end + diff --git a/app/src/main/assets/android/intents.lua b/app/src/main/assets/android/intents.lua new file mode 100644 index 0000000..d4d9b46 --- /dev/null +++ b/app/src/main/assets/android/intents.lua @@ -0,0 +1,143 @@ +--- intents.lua +-- Wrappers for common intents like taking pictures and sending messages +-- @submodule android +require 'android' +local PK = luajava.package +local L = PK 'java.lang' +local IO = PK 'java.io' +local app = PK 'android.app' +local C = PK 'android.content' +local P = PK 'android.provider' +local RESULT_CANCELED = app.Activity.RESULT_CANCELED +local Intent = bind 'android.content.Intent' +local Uri = bind 'android.net.Uri' + +--- open the Camera app and let user take a picture. +-- @param me +-- @string file +-- @func callback +function android.take_picture (me,file,callback) + local intent = Intent(P.MediaStore.ACTION_IMAGE_CAPTURE) + callback = android.safe(callback) + if file then + if not file:match '^/' then + file = IO.File(me.a:getFilesDir(),'images'..file) + else + file = IO.File(file) + end + file:getParentFile():mkdir() + intent:putExtra(P.MediaStore.EXTRA_OUTPUT, Uri:fromFile(file)) + me:intent_for_result (intent,function(_,result,intent) + callback(result ~= RESULT_CANCELED,intent) + end) + else + me:intent_for_result (intent,function(_,result,intent) + local data + print(result,intent) + if result ~= RESULT_CANCELED then + data = intent:getExtras():get 'data' + end + callback(data,result,intent) + end) + end +end + +--- open messaging or mail app and prepare message for user. +-- @param me +-- @string subject +-- @string body +-- @string address +function android.send_message (me,subject,body,address) + local intent + if not subject then -- let's let the user decide + local kind = Intent(Intent.ACTION_SEND) + kind:setType 'text/plain' + kind:putExtra(Intent.EXTRA_TEXT,body) + intent = Intent:createChooser(kind,nil) + else + address = address or '' + intent = Intent(Intent.ACTION_VIEW) + intent:setData(Uri:parse ('mailto:'..address..'?subject='..subject..'&body='..body)) + end + me.a:startActivity(intent) +end + +--- choose a picture from Gallery and so forth. +-- @param me +-- @func callback +function android.pick_picture (me,callback) + local intent = Intent() + intent:setType 'image/*' + intent:setAction(Intent.ACTION_GET_CONTENT) + me:intent_for_result(Intent:createChooser(intent,nil),function(_,_,data) + data = data:getData() -- will be a Uri + local c = me.a:getContentResolver():query(data,L.String{'_data'},nil,nil,nil) + c:moveToFirst() + callback(c:getString(0)) + end) +end + +--- record audio. +-- @param me +-- @string file +-- @func callback +function android.record_audio (me,file,callback) + local intent = Intent(P.MediaStore_Audio_Media.RECORD_SOUND_ACTION) + --intent:putExtra(P.MediaStore.EXTRA_OUTPUT, Uri(file)); + me:intent_for_result(intent,function(_,_,data) + if data == null then return callback(nil,'no data') end + data = data:getData() + print('uri',data) + local c = me.a:getContentResolver():query(data,L.String{'_data'},nil,nil,nil) + c:moveToFirst() + callback(c:getString(0)) + end) +end + +local function get_string(c,key,def) + local idx = c:getColumnIndex(key) + if idx == -1 then return def end + return c:getString(idx) +end + +--- let user pick a Contact. +-- @param me +-- @func callback +function android.pick_contact(me,callback) + local CCC = P.ContactsContract_Contacts + local CCP = P.ContactsContract_CommonDataKinds_Phone + local CCE = P.ContactsContract_CommonDataKinds_Email + local intent = Intent(Intent.ACTION_PICK,CCC.CONTENT_URI) + callback = android.safe(callback) + me:intent_for_result(intent,function(_,result,intent) + if result ~= RESULT_CANCELED then + local uri = intent:getData() + local c = me.a:managedQuery(uri,nil,nil,nil,nil) + if c:moveToFirst() then + local res = {uri = uri} + res.id = get_string(c,CCC._ID) + res.name = get_string(c,CCC.DISPLAY_NAME) + res.thumbnail = get_string(c,CCC.PHOTO_THUMBNAIL_URI) + if get_string(c,CCC.HAS_PHONE_NUMBER,'0') ~= '0' then + local phones = me.a:managedQuery(CCP.CONTENT_URI, + nil, CCP.CONTACT_ID..' = '..res.id,nil,nil) + phones:moveToFirst() + res.phone = get_string(phones,"data1") + end + local emails = me.a:managedQuery(CCE.CONTENT_URI, + nil, CCE.CONTACT_ID..' = '..res.id,nil,nil) + if emails:moveToFirst() then + res.email = get_string(emails,"data1") + end + callback(res) + else + callback(nil,'no such contact') + end + else + callback(nil,'cancelled') + end + end) +end + +-- flags whether we're loaded or not +android.intents = true diff --git a/app/src/main/assets/android/plot/init.lua b/app/src/main/assets/android/plot/init.lua new file mode 100644 index 0000000..f67b951 --- /dev/null +++ b/app/src/main/assets/android/plot/init.lua @@ -0,0 +1,1081 @@ +--- Androlua plot library. +-- @module android.plot +local G = luajava.package 'android.graphics' +local L = luajava.package 'java.lang' +local V = luajava.package 'android.view' +local array = require 'android.array' +local append = table.insert + +local Plot = { array = array } + +-- OOP support +local function make_object (obj,T) + T.__index = T + return setmetatable(obj,T) +end + +local function make_callable (type,ctor) + setmetatable(type,{ + __call = function(_,...) return ctor(...) end + }) +end + +local function union (A,B) + if A.left > B.left then A.left = B.left end + if A.bottom > B.bottom then A.bottom = B.bottom end + if A.right < B.right then A.right = B.right end + if A.top < B.top then A.top = B.top end +end + +local Color = G.Color +local FILL,STROKE = G.Paint_Style.FILL,G.Paint_Style.STROKE +local WHITE,BLACK = Color.WHITE, Color.BLACK + +local set_alpha + +local function PC (clr,default) + if type(clr) == 'string' then + local c,alpha = clr:match '([^:]+):(.+)' + if alpha then + print(c,alpha) + c = PC(c) + alpha = tonumber(alpha) + return set_alpha(c,alpha) + end + end + return android.parse_color(clr or default) +end + +function set_alpha (c,alpha) + c = PC(c) + local R,G,B = Color:red(c),Color:green(c),Color:blue(c) + alpha = (alpha/100)*255 + return Color:argb(alpha,R,G,B) +end + +local function newstroke () + local style = G.Paint() + style:setStyle(STROKE) + return style +end + +local function set_color(style,clr) + style:setColor(PC(clr)) +end + +local function fill_paint (clr) + local style = G.Paint() + style:setStyle(FILL) + set_color(style,clr) + return style +end + +local function stroke_paint (clr,width,effect) + local style = newstroke() + set_color(style,clr) + if width then + style:setStrokeWidth(width) + style:setAntiAlias(true) + end + if effect then + style:setPathEffect(effect) + end + return style +end + +local function text_paint (size,clr) + local style = newstroke() + style:setTextSize(size) + if clr then + set_color(style,clr) + end + style:setAntiAlias(true) + return style +end + +local function plot_object_array () + local arr = array() + arr:forall_method 'update' + arr:forall_method 'draw' + return arr +end + +local flot_colours = {PC"#edc240", PC"#afd8f8", PC"#cb4b4b", PC"#4da74d", PC"#9440ed"} + +local Series,Axis,Legend,Anot,TextAnot = {},{},{},{},{} + +function Plot.new (t) + local self = make_object({},Plot) + if not t.theme then + t.theme = {textColor='BLACK',background='WHITE'} + end + t.theme.color = t.theme.color or t.theme.textColor + t.theme.colors = t.theme.colors or flot_colours + self.background = fill_paint(t.background or t.theme.background) + self.area = fill_paint(t.fill or t.theme.background) + self.color = t.color or t.theme.color + self.axis_paint = stroke_paint(self.color) + self.aspect_ratio = t.aspect_ratio or 1 + self.margin = {} + self.series = plot_object_array() + self.annotations = plot_object_array() + self.grid = t.grid + if t.axes == false then + t.xaxis = {invisible=true} + t.yaxis = {invisible=true} + end + self.xaxis = Axis.new(self,t.xaxis or {}) + self.xaxis.horz = true + self.yaxis = Axis.new(self,t.yaxis or {}) + + self.theme = t.theme + self.interactive = t.interactive + + local W = android.me.metrics.widthPixels + local defpad = W/30 + if t.padding then + defpad = t.padding + end + self.padding = {defpad,defpad,defpad,defpad} + self.pad = defpad + self.sample_width = 2*self.pad + + self.colours = self.theme.colors + + if #t == 0 then error("must provide at least one Series!") end -- relax this later?? + for _,s in ipairs(t) do + self:add_series(s) + end + + if t.annotations then + for _,a in ipairs(t.annotations) do + self:add_annotation(a) + end + end + + if t.legend ~= false then + self.legend = Legend.new(self,t.legend) + end + + return self +end + +local function add (arr,obj) + append(arr,obj) + if obj.tag then arr[obj.tag] = obj end + obj.idx = #arr +end + +function Plot:add_series (s) + add(self.series,Series.new (self,s)) +end + +function Plot:add_annotation (a) + local anot = a.text and TextAnot.new(self,a) or Anot.new(self,a) + add(self.annotations,anot) +end + +function Plot:get_series (idx) + return self.series[idx] -- array index _or_ tag +end + +function Plot:get_annotation (idx) + return self.annotations[idx] -- array index _or_ tag +end + +make_callable(Plot,Plot.new) + +function Plot:calculate_bounds_if_needed (force) + local xaxis, yaxis = self.xaxis, self.yaxis + -- have to update Axis bounds if they haven't been set... + -- calculate union of all series bounds + if force or not xaxis:has_bounds() or not yaxis:has_bounds() then + local huge = math.huge + local bounds = {left=huge,right=-huge,bottom=huge,top=-huge} + for s in self.series:iter() do + union(bounds,s:bounds()) + end + if (force and not xaxis.fixed_bounds) or not xaxis:has_bounds() then + xaxis:set_bounds(bounds.left,bounds.right,true) + end + if (force and not yaxis.fixed_bounds) or not yaxis:has_bounds() then + yaxis:set_bounds(bounds.bottom,bounds.top,true) + end + end +end + +function Plot:update_and_paint (noforce) + self.force = not noforce + self:update() + if self.View then self.View:invalidate() end +end + +function Plot:set_xbounds (x1,x2) + self.xaxis:set_bounds(x1,x2) + self:update_and_paint(true) +end + +function Plot:set_ybounds (y1,y2) + self.yaxis:set_bounds(y1,y2) + self:update_and_paint(true) +end + +function Plot:update (width,height,fixed_width,fixed_height) + if width then + if fixed_width and width > 0 then + self.width = width + else + height = 400 + self.width = height + end + elseif not self.init then + -- we aren't ready for business yet + return + end + + local xaxis,yaxis = self.xaxis,self.yaxis + + self:calculate_bounds_if_needed(self.force) + self.force = false + + xaxis:init() + yaxis:init() + + if not self.init then + local P = self.padding + local L,T,R,B = P[1],P[2],P[3],P[4] + local AR,BW,BH = self.aspect_ratio + local X,Y = xaxis.thick,yaxis.thick + + -- margins around boxes + local M = 7 + self.outer_margin = {left=M,top=M,right=M,bottom=M} --outer + -- padding around plot + self.margin = { + left = L + Y, + top = T, + right = R, + bottom = B + X + } + + -- we now know the extents of the axes and can size our plot area + if fixed_width and width > 0 then + BW = width - L - R - Y + BH = AR*BW + self.width = width + self.height = BH + X + T + B + else + BH = height - T - B - X + BW = BH/AR + self.width = BH + Y + L + R + self.height = height + end + self.boxheight = BH + self.boxwidth = BW + + self.init = true + end + + -- we have the exact self area dimensions and can now scale data properly + xaxis:setup_scale() + yaxis:setup_scale() + + self.annotations:update() + +end + +function Plot:next_colour () + return self.colours [#self.series % #self.colours + 1] +end + +function Plot:resized(w,h) + -- do we even use this anymore? + self.width = w + self.height = h +end + +-- get all series with labels, plus the largest label. +function Plot:fetch_labelled_series () + local series = array() + local wlabel = '' + for s in self.series:iter '_.label~=nil' do + series:append(s) + if #s.label > #wlabel then + wlabel = s.label + end + end + return series, wlabel +end + +function Plot.draw(plot,c) + c:drawPaint(plot.background) + + c:save() + c:translate(plot.margin.left,plot.margin.top) + local bounds = G.Rect(0,0,plot.boxwidth,plot.boxheight) + if plot.area then + c:drawRect(bounds,plot.area) + end + c:drawRect(bounds,plot.axis_paint) + c:clipRect(bounds) + plot.series:draw(c) + plot.annotations:draw(c) + c:restore() + plot.xaxis:draw(c) + plot.yaxis:draw(c) + c:translate(plot.margin.left,plot.margin.top) + if plot.legend then + plot.legend:draw(c) + end +end + +function Plot:measure (pwidth,pheight,width_fixed, height_fixed) + if not self.initialized then + --print(pwidth,pheight,width_fixed,height_fixed) + self:update(pwidth,pheight,width_fixed,height_fixed) + self.initialized = true + end + return self.width,self.height +end + +function Plot:view (me) + local MeasureSpec = V.View_MeasureSpec + self.me = me + --me.plot = self + local tbl = { + onDraw = function(c) self:draw(c) end, + onSizeChanged = function(w,h) self:resized(w,h) end, + onMeasure = function(wspec,hspec) + local pwidth = MeasureSpec:getSize(wspec) + local pheight = MeasureSpec:getSize(hspec) + local width_fixed = MeasureSpec:getMode(wspec) --== MeasureSpec.EXACTLY + local height_fixed = MeasureSpec:getMode(hspec) --== MeasureSpec.EXACTLY + local p,w = self:measure(pwidth,pheight,width_fixed,height_fixed) + self.View:measuredDimension(p,w) + return true + end + } + if self.interactive then + tbl.onTouchEvent = require 'android.plot.interactive'(self) + end + self.View = me:luaView(tbl) + return self.View +end + +function Plot:corner (cnr,width,height,M) + local WX,HY = self.boxwidth,self.boxheight + M = M or self.outer_margin + local H,V = cnr:match '(.)(.)' + local x,y + if H == 'L' then + x = M.left + elseif H == 'R' then + x = WX - (width + M.right) + elseif H == 'C' then + x = (WX - width)/2 + end + if V == 'T' then + y = M.top + elseif V == 'B' then + y = HY - (height + M.bottom) + elseif V == 'C' then + y = (HY - height)/2 + end + if not x or not y then + error("bad corner specification",2) + end + return x,y +end + +function Plot:align (cnr,width,height,M,xp,yp) + local H,V = cnr:match '(.)(.)' + local dx,dy + M = M or self.outer_margin + if H == 'L' then + dx = - M.left - width + elseif H == 'R' then + dx = M.right + end + if V == 'T' then + dy = - M.top - height + elseif V == 'B' then + dy = M.bottom + end + if not dx or not dy then + error("bad align specification",2) + end + return xp+dx,yp+dy +end + +-- Axis class ------------ + +function Axis.new (plot,self) + make_object(self,Axis) + self.plot = plot + if self.invisible then + self.thick = 0 + return self + end + self.grid = self.grid or plot.grid + + if self.min and self.max then + self.fixed_bounds = true + end + + self.label_size = android.me:parse_size(self.label_size or '12sp') + self.label_paint = text_paint(self.label_size,plot.color) + + if self.grid then + self.grid = stroke_paint(set_alpha(plot.color,30),1) + end + + self.explicit_ticks = type(self.ticks)=='table' and #self.ticks > 0 + + return self +end + +function Axis:has_bounds () + return self.min and self.max or self.explicit_ticks +end + +function Axis:set_bounds (min,max,init) + if init then + self.old_min, self.old_max = min, max + self.initial_ticks = true + elseif not max then + min, max = self.old_min, self.old_max + end + self.unchanged = false + self.min = min + self.max = max +end + +function Axis:zoomed () + return self.min > self.old_min or self.max < self.old_max +end + +local DAMN_SMALL = 10e-16 + +local function eq (x,y) + return math.abs(x-y) < DAMN_SMALL +end + +function Axis:init() + if self.invisible or self.unchanged then return end + self.unchanged = true + local plot = self.plot + + if not self.explicit_ticks then + local W = plot.width + if not self.horz then W = plot.aspect_ratio*W end + if self.type == 'date' then + self.ticks = require 'android.plot.time_intervals' (self,W) + else + self.ticks = require 'android.plot.intervals' (self,W) + end + end + local ticks = self.ticks + + -- how to convert values to strings for labels; + -- format can be a string (for `string.format`) or a function + local format = ticks.format + if type(format) == 'string' then + local fmt = format + format = function(v) return fmt:format(v) end + elseif not format then + format = tostring + end + + local wlabel = '' + -- We have an array of ticks. Ensure that it is an array of {value,label} pairs + for i = 1,#ticks do + local tick = ticks[i] + local label + if type(tick) == 'number' then + label = format(tick) + ticks[i] = {tick,label} + else + label = tick[2] + end + if #label > #wlabel then + wlabel = label + end + end + + -- adjust our bounds to match ticks, and give some vertical space for series + local start_tick, end_tick = ticks[1][1], ticks[#ticks][1] + + self.min = self.min or start_tick + self.max = self.max or end_tick + + if not self.horz then + local D = (self.max - self.min)/20 + if not eq(self.max,0) and eq(self.max,end_tick) then + self.max = self.max + D + end + if not eq(self.min,0) and eq(self.min,start_tick) then + self.min = self.min - D + end + end + + if self.initial_ticks then + if self.min > start_tick then + self.min = start_tick + end + if self.max < end_tick then + self.max = end_tick + end + self.initial_ticks = false + end + + -- finding our 'thickness', which is the extent in the perp. direction + -- (we'll use this to adjust our plotting area size and position) + self.label_width = self:get_label_extent(wlabel) + if not self.horz then + -- cool, have to find width of y-Axis label on the left... + self.thick = math.floor(1.1*self.label_width) + else + self.thick = self.label_size + end + self.tick_width = self.label_size +end + +function Axis:get_label_extent(wlabel,paint) + local rect = G.Rect() + paint = paint or self.label_paint + -- work with a real Java string to get the actual length of a UTF-8 string! + local str = L.String(wlabel) + paint:getTextBounds(wlabel,0,str:length(),rect) + return rect:width(),rect:height() +end + +function Axis:setup_scale () + local horz,plot = self.horz,self.plot + local W = horz and plot.boxwidth or plot.boxheight + local delta = self.max - self.min + local m,c + if horz then + m = W/delta + c = -self.min*W/delta + else + m = -W/delta + c = self.max*W/delta + end + + self.scale = function(v) + return m*v + c + end + + local minv = 1/m + local cinv = - c/m + local M = self.horz and plot.margin.left or plot.margin.top + + self.unscale = function(p) + return minv*(p-M) + cinv + end + self.pix2plot = self.horz and minv or -minv +end + +function Axis:draw (c) + if self.invisible then return end -- i.e, we don't want to draw ticks or gridlines etc + + local tpaint,apaint,size,scale = self.label_paint,self.plot.axis_paint,self.label_size,self.scale + local boxheight = self.plot.boxheight + local margin = self.plot.margin + local twidth = self.tick_width + local lw = self.label_width + if self.horz then + c:save() + c:translate(margin.left,margin.top + boxheight) + for _,tick in ipairs(self.ticks) do + local x = tick[1] + if x > self.min and x < self.max then + x = scale(x) + --c:drawLine(x,0,x,twidth,apaint) + if tpaint then + lw = self:get_label_extent(tick[2],tpaint) + c:drawText(tick[2],x-lw/2,size,tpaint) + end + if self.grid then + c:drawLine(x,0,x,-boxheight,self.grid) + end + end + end + c:restore() + else + c:save() + local boxwidth = self.plot.boxwidth + c:translate(margin.left,margin.top) + for _,tick in ipairs(self.ticks) do + local y = tick[1] + if y > self.min and y < self.max then + y = scale(y) + --c:drawLine(-twidth,y,0,y,apaint) + if tpaint then + c:drawText(tick[2],-lw,y,tpaint) -- y + sz ! + end + if self.grid then + c:drawLine(0,y,boxwidth,y,self.grid) + end + end + end + c:restore() + end +end + +Plot.Axis = Axis + +------- Series class -------- + +local function unzip (data) + data = array(data) + local xdata = data:map '_[1]' + local ydata = data:map '_[2]' + return xdata,ydata +end + +function Series.new (plot,t) + local self = make_object(t,Series) + self:set_styles(plot,t) + if not self:set_data(t,false) then + error("must provide both xdata and ydata for series",2) + end + self.init = true + return self +end + +function Series:set_styles (plot,t) + self.plot = plot + self.xaxis = plot.xaxis + self.yaxis = plot.yaxis + self.path = G.Path() + local clr = t.color or plot:next_colour() + if not t.points and not t.lines then + t.lines = true + end + if t.lines and t.color ~= 'none' then + self.linestyle = stroke_paint(clr,t.width) + if type(t.lines) == 'string' and t.lines ~= 'steps' then + local w = plot.sample_width + local pat + if t.lines == 'dash' then + pat = {w/4,w/4} + elseif t.lines == 'dot' then + pat = {w/8,w/8} + elseif t.lines == 'dashdot' then + pat = {w/4,w/8,w/8,w/8} + end + pat = G.DashPathEffect(L.Float(pat),#pat/2) + self.linestyle:setPathEffect(pat) + end + if t.shadow then + local c = set_alpha(clr,50) + self.shadowstyle = stroke_paint(c,t.width) + end + end + if t.fill then + local cfill = t.fill + if t.fill == true then + cfill = set_alpha(clr,30) + end + t.fillstyle = fill_paint(cfill) + elseif t.points then + self.pointstyle = stroke_paint(clr,t.pointwidth or 10) -- Magic Number! + local cap = t.points == 'circle' and G.Paint_Cap.ROUND or G.Paint_Cap.SQUARE + self.pointstyle:setStrokeCap(cap) + end + self.color = PC(clr) +end + +function Series:set_data (t,do_update) + do_update = do_update==nil or do_update + local set,xunits = true,self.xunits + local xx, yy + if t.data then -- Flot-style data + xx, yy = unzip(t.data) + elseif not t.xdata and not t.ydata then + set = false + else + xx, yy = array(t.xdata),array(t.ydata) + end + if self.lines == 'steps' then + local xs,ys,k = array(),array(),1 + if #xx == #yy then + local n = #xx + xx[n+1] = xx[n] + (xx[n]-xx[n-1]) + end + for i = 1,#yy do + xs[k] = xx[i]; ys[k] = yy[i] + xs[k+1] = xx[i+1]; ys[k+1] = yy[i] + k = k + 2 + end + xx, yy = xs, ys + end + if self.points then + self.xpoints, self.ypoints = xx, yy + elseif self.fill then + local xf, yf = array(xx),array(yy) + local min = yf:minmax() + xf:append(xf[#xf]) + yf:append(min) + xf:append(xf[1]) + yf:append(min) + self.xfill, self.yfill = xf, yf + end + if xunits then + local fact + if xunits == 'msec' then + fact = 1/1000.0 + end + xx = xx:map('*',fact) + end + local scale_to = self.scale_to_y or self.scale_to_x + if scale_to then + local other = self.plot:get_series(scale_to) + local bounds = other:bounds() + if self.scale_to_y then + yy:scale_to(bounds.bottom,bounds.top) + else + yy:scale_to(bounds.left,bounds.right) + end + end + self.xdata, self.ydata = xx, yy + if do_update then + self.cached_bounds = nil + self.plot:update_and_paint() + end + return set +end + +function Series:update () + +end + +function Series:bounds () + if self.cached_bounds then + return self.cached_bounds + end + if not self.xdata then error('xdata was nil!') end + local xmin,xmax = array.minmax(self.xdata) + if not self.ydata then error('ydata was nil!') end + local ymin,ymax = array.minmax(self.ydata) + self.cached_bounds = {left=xmin,top=ymax,right=xmax,bottom=ymin} + return self.cached_bounds +end + +local function draw_poly (self,c,xdata,ydata,pathstyle) + local scalex,scaley,path = self.xaxis.scale, self.yaxis.scale, self.path + path:reset() + path:moveTo(scalex(xdata[1]),scaley(ydata[1])) + -- cache the lineTo method! + local lineTo = luajava.method(path,'lineTo',0.0,0.0) + for i = 2,#xdata do + lineTo(path,scalex(xdata[i]),scaley(ydata[i])) + end + c:drawPath(path,pathstyle) +end + +function Series:draw(c) + if self.linestyle then + draw_poly (self,c,self.xdata,self.ydata,self.linestyle) + end + if self.fillstyle then + --print('filling',self.tag) + draw_poly (self,c,self.xfill,self.yfill,self.fillstyle) + end + if self.pointstyle then + local scalex,scaley = self.xaxis.scale, self.yaxis.scale + local xdata,ydata = self.xpoints,self.ypoints + for i = 1,#xdata do + c:drawPoint(scalex(xdata[i]),scaley(ydata[i]),self.pointstyle) + end + end +end + +function Series:draw_sample(c,x,y,sw) + if self.linestyle then + c:drawLine(x,y,x+sw,y,self.linestyle) + else + c:drawPoint(x,y,self.pointstyle) + end + return self.label +end + +function Series:get_x_intersection (x) + local idx = self.xdata:find_linear(x) + if not idx then return nil,"no intersection with this series possible" end + local y = self.ydata:at(idx) + return y,idx +end + +function Series:get_data_range (idx1,idx2) + local xx = self.xdata:sub(idx1,idx2) + local yy = self.ydata:sub(idx1,idx2) + return xx:map2('{_1,_2}',yy) +end + +function Anot.new(plot,t) + t.width = 1 + if t.points then t.pointwidth = 7 end --Q: what is optimal default here? + + t.series = t.series and plot:get_series(t.series) + + -- can override default colour, which is 60% opaque series colour + local c = t.series and t.series.color or plot.theme.color + t.color = t.color or set_alpha(c,60) + + if t.bounds then +--~ t.x1,t.y1,t.x2,t.y2 = t[1],t[2],t[3],t[4] + end + + -- simularly default fill colour is 40% series colour + -- we're filling if asked explicitly with a fill colour, or if x1 + -- or y1 is defined + if t.fill or t.x1 ~= nil or t.y1 ~= nil then --) and not (t.x or t.y) then + t.fillstyle = fill_paint(t.fill or set_alpha(c,30)) + else + t.lines = true + end + + -- lean on our 'friend' Series to set up the paints and so forth! + local self = make_object(t,Anot) + Series.set_styles(self,plot,t) + + self.is_anot = true + return self +end + +local function lclamp (x,xmin) return math.max(x or xmin, xmin) end +local function rclamp (x,xmax) return math.min(x or xmax, xmax) end +local function clamp (x1,x2,xmin,xmax) return lclamp(x1,xmin),rclamp(x2,xmax) end + +function Anot:update() + local lines = array() + local A = array + local top + local xmin,xmax,ymin,ymax = self.xaxis.min,self.xaxis.max,self.yaxis.min,self.yaxis.max + local series = self.series + self.points = {} + + local function append (name,xp,yp) + local pt = {xp,yp} + lines:append (pt) + self.points[name] = pt + end + + --print('y',self.y,self.y1,self.fillstyle,self.linestyle) + if self.fillstyle then + self.horz = x1 == nil + if not series then -- a filled box {x1,y1,x2,y2} + local x1,x2 = clamp(self.x1,self.x2,xmin,xmax) + local y1,y2 = clamp(self.y1,self.y2,ymin,ymax) + append('start',x1,y1) + lines:extend {{x2,y1},{x2,y2}} + append('last',x1,y2) + lines:append{x1,y1} -- close the poly + else + -- must clamp x1,x2 to series bounds! + local bounds = series:bounds() + local x1,x2 = clamp(self.x1,self.x2,bounds.left,bounds.right) + local top1,i1 = series:get_x_intersection(x1) + local top2,i2 = series:get_x_intersection(x2) + -- closed polygon including chunk of series that we can fill! + append('start',x1,ymin) + lines:extend (series:get_data_range(i1,i2)) + append('last',x2,ymin) + end + else -- line annotation + local x,y = self.x,self.y + self.horz = x == nil + append('start',x or xmin,y or ymin) + if not series then -- just a vertical or horizontal line + append('last',x or xmax,y or ymax) + else -- try to intersect (only x intersection for now) + top = series:get_x_intersection(x) + if top then + append('intersect',x,top) + append('last',xmin,top) + else + append('last',x,ymax) + end + end + end + + Series.set_data(self,{ data = lines },false) + + -- maybe add a point to the intersection? + if top then + self.xpoints = array{self.x} + self.ypoints = array{top} + end + if self.fillstyle then + self.linestyle = nil + self.xfill, self.yfill = self.xdata, self.ydata + --print(self.xfill) + --print(self.yfill) + end +end + +function Anot:draw(c) + Series.draw(self,c) +end + +function Anot:get_point (which) + local pt = self.points[which] + if not pt then return nil, 'no such point' end + return pt[1],pt[2] +end + +local function set_box (self,plot) + -- inner padding + local P = self.padding or plot.pad/2 + self.padding = {P,P,P,P} --inner + self.plot = plot + + -- text style + local paint + if self.size then + self.color = self.color or plot.color + paint = text_paint(android.me:parse_size(self.size),self.color) + else + paint = plot.xaxis.label_paint + end + self.label_paint = paint + + -- box stuff + self.stroke = plot.axis_paint + self.background = fill_paint(self.fill or plot.theme.background) +end + +local function text_extent (self,text) + return self.plot.xaxis:get_label_extent(text or self.text,self.label_paint) +end + +function TextAnot.new(plot,t) + t.anot = t.anot and plot:get_annotation(t.anot) + set_box(t,plot) + return make_object(t,TextAnot) +end + +function Plot.scale (plot,x,y) + return plot.xaxis.scale(x),plot.yaxis.scale(y) +end + +local function default_align (anot,point) + if anot:get_point 'intersect' then + if point ~= 'intersect' then -- points on axes + local X,Y = 'LT','RT' + -- order of 'first' and 'last' is reversed for horizontal lines + if anot.horz then Y,X = X,Y end + return point=='start' and X or Y + else + return 'LT' + end + else -- straight horizontal or vertical line + --print('horz',anot.horz,point) + if anot.horz then + return point=='start' and 'RT' or 'LT' + else + return point=='start' and 'LT' or 'LB' + end + end + +end + +function TextAnot:update () + local xs,ys + local plot = self.plot + local w,h = text_extent(self) + if not self.anot then -- we align to the edges of the plotbox + self.cnr = self.corner or 'CT' + xs,ys = plot:corner(self.cnr,w,h,empy_margin) + else -- align to the points of the annotation + self.cnr = self.corner or default_align(self.anot,self.point) + px,py = self.anot:get_point(self.point) + px,py = plot:scale(px,py) + xs,ys = plot:align(self.cnr,w,h,empty_margin,px,py) + --print('point',xs,ys) + end + self.xp = xs + self.yp = ys + h +end + +local empty_margin = {left=0,top=0,right=0,bottom=0} + +function TextAnot:draw (c) + --print('draw',self.xp,self.yp) + c:drawText(self.text,self.xp,self.yp,self.label_paint) +end + +-- Legend class ---------- +function Legend.new (plot,t) + if type(t) == 'string' then + t = {corner = t} + elseif t == nil then + t = {} + end + t.cnr = t.corner or 'RT' + t.sample_width = t.sample_width or plot.sample_width + set_box(t,plot) + return make_object(t or {},Legend) +end + +function Legend:draw (c) + local plot = self.plot + local P = self.padding + + local series,wlabel = plot:fetch_labelled_series() + + if #series == 0 then return end -- no series to show! + + -- can now calculate our bounds and ask for our position + local sw = self.sample_width + local w,h = text_extent(self,wlabel) + local W,H + local dx,dy,n = P[1],P[2],#series + if not self.across then + W = P[1] + sw + dx + w + dx + H = P[2] + n*(dy+h) - h/2 + else + W = P[1] + n*(sw+w+2*dx) + H = P[2] + h + dy + end + local margin + local draw_box = self.box == nil or self.box == true + if not draw_box then margin = empty_margin end + local xs,ys = plot:corner(self.cnr,W,H,margin) + + -- draw the box + if draw_box then + local bounds = G.Rect(xs,ys,xs+W,ys+H) + if self.background then + c:drawRect(bounds,self.background) + end + c:drawRect(bounds,self.stroke) + end + self.width = W + self.height = H + + -- draw the entries (ask series to give us a 'sample') + local y = ys + P[2] + h/2 + local offs = h/2 + local x = xs + P[1] + local yspacing = P[2]/2 + if self.across then y = y + h/2 end + for _,s in ipairs(series) do + local label = s:draw_sample(c,x,y-offs,sw) + x = x+sw+P[1] + c:drawText(label,x,y,self.label_paint) + if not self.across then + y = y + h + yspacing + x = xs + P[1] + else + x = x + w/2 + 2*P[1] + end + end + +end + +-- we export this for now +_G.Plot = Plot +return Plot diff --git a/app/src/main/assets/android/plot/interactive.lua b/app/src/main/assets/android/plot/interactive.lua new file mode 100644 index 0000000..54f3a41 --- /dev/null +++ b/app/src/main/assets/android/plot/interactive.lua @@ -0,0 +1,46 @@ +-- interactive logic for AndroLua Plot + +local function xclip (x1,x2,xd,axis) + local xmin, xmax = axis.old_min, axis.old_max + if x1+xd < xmin then + return xmin, xmin + (x2-x1) + elseif x2+xd > xmax then + return xmax - (x2-x1), xmax + end + return x1+xd,x2+xd +end + +return function (plot) + + local unscalex, unscaley + + function plot.touch(kind,idx,x,y,dir,z) + if not unscalex then + unscalex,unscaley = plot.xaxis.unscale,plot.yaxis.unscale + end + x,y = unscalex(x),unscaley(y) + --print('touch',kind,idx,x,y,dir) + if z then -- wipes and pinches give us a distance as well + local is_x = dir=='X' + local axis = is_x and plot.xaxis or plot.yaxis + local zs = z*axis.pix2plot + local v1,v2 = axis.min, axis.max + if kind=='PINCH' then + zs = zs/2 + v1 = v1 + zs + v2 = v2 - zs + elseif kind=='SWIPE' then + v1,v2 = xclip(v1,v2,zs,axis) + end + if is_x then + --print('bounds',v1,v2) + plot:set_xbounds(v1,v2) + else + plot:set_ybounds(v1,v2) + end + end + end + + return require 'android.touch'(plot) +end + diff --git a/app/src/main/assets/android/plot/intervals.lua b/app/src/main/assets/android/plot/intervals.lua new file mode 100644 index 0000000..24cb3b9 --- /dev/null +++ b/app/src/main/assets/android/plot/intervals.lua @@ -0,0 +1,70 @@ +-- round to nearby lower multiple of base +local function floorInBase (n,base) + return base * math.floor(n/base) +end + +return function (axis,width) + local noTicks + if type(axis.ticks) == 'number' then + noTicks = axis.ticks + else + noTicks = 0.3 * math.sqrt(width) + end + + local delta = (axis.max - axis.min)/noTicks + + local maxDec = axis.tickDecimals; + local dec = - math.floor(math.log10(delta)); + if maxDec and dec > maxDec then + dec = maxDec + end + + magn = math.pow(10, -dec) + norm = delta / magn -- norm is between 1.0 and 10.0 + + if norm < 1.5 then + size = 1 + elseif norm < 3 then + size = 2 + -- special case for 2.5, requires an extra decimal + if norm > 2.25 and (maxDec == nil or dec + 1 <= maxDec) then + size = 2.5 + dec = dec + 1 + end + elseif norm < 7.5 then + size = 5 + else + size = 10 + end + + size = size*magn; + + if axis.minTickSize and size < axis.minTickSize then + size = axis.minTickSize + end + + local tickDecimals = math.max(0, maxDec and maxDec or dec) + local tickSize = axis.tickSize or size; + + -- print('ticks',axis.tickDecimals,axis.tickSize) + + if tickDecimals == 0 then + axis.format = '%d' + else + axis.format = '%.'..tickDecimals..'f' + end + + local ticks = {} + local start = floorInBase(axis.min,tickSize) + local i = 0 + local v = -math.huge + while v < axis.max do + v = start + i * tickSize + i = i + 1 + ticks[i] = v + end + ticks.format = axis.format + return ticks + +end + diff --git a/app/src/main/assets/android/plot/time_intervals.lua b/app/src/main/assets/android/plot/time_intervals.lua new file mode 100644 index 0000000..c69cbe6 --- /dev/null +++ b/app/src/main/assets/android/plot/time_intervals.lua @@ -0,0 +1,203 @@ +local DEBUG = arg ~= nil +local MINUTE = 60 +local HOUR = 60*60 +local DAY = 24*HOUR +local YEAR_DAYS = 365 + +local floor = math.floor +local odate = os.date +local function date2table (d) + return odate('*t',d) +end +local table2date = os.time + +local function year (d) + return date2table(d).year +end + +local tmin,tmax = 4,8 + +local seconds = {1,2,5,15,30; s=1} -- 4s/1 to 4min/30 +local minutes = {1,2,5,15,30; s=60} -- 4min/1 to 4h/30 +local hours = {1,2,6,12; s=HOUR} -- 4h/1 to 2d/6 +local days = {1,2,4,7,14; s=DAY} -- 4d/1 to 4m/14 +local months = {1,2,3,6; s=1} -- 4m/1 to 4y/6 +local years = {1,10; s=DAY*YEAR_DAYS} + +local function match (value,intervals) + for _,iter in ipairs(intervals) do + local div = value/iter + div = floor(div) + if div >= tmin and div <= tmax then + if DEBUG then print('divs ',div) end + return iter*intervals.s + end + end +end + +local function hour_format (d) + local tm = odate('%H:%M',d) + if tm == '00:00' then + tm = odate('%d %b',d) + end + return tm +end + +local function check_january (d,last) + if last==nil or odate ('%m',d) == '01' then + return odate(' %Y',d) + else + return '' + end +end + +local function date_format (d, last) + return odate('%d %b',d)..check_january(d,last) +end + +local function month_format (d,last) + local yy = '' + if last == nil or year(d) ~= year(last) then + yy = odate(' %Y',d) + end + return odate('%b',d)..yy +end + +local function nearest_month (d) + local t = date2table(d) + if t.day > 15 then + t.month = t.month + 1 + end + t.day = 1 + return table2date(t) +end + +local function nearest_year (d) + local t = date2table(d) + if t.month > 6 then + t.year = t.year + 1 + end + t.month = 1 + t.day = 1 + return table2date(t) +end + + +local function next_month (d,nmonth) + local t = date2table(d) + for i = 1,nmonth do + t.day = 28 + local m = t.month + while m == t.month do + d = table2date(t) + t = date2table(d) + t.day = t.day + 1 + end + end + return d +end + +function classify (t1,t2) + local span = t2 - t1 -- NB to check this! + local day = floor(span/DAY) + local intvl,fmt,next_tick + if day >= 4 then + if day <= 4*YEAR_DAYS then -- case C + if day < 4*30 then -- four months approx + fmt = date_format -- e.g 4 Mar + if DEBUG then print '(days)' end + intvl = match(day,days) + else + fmt = month_format + if DEBUG then print '(months)' end + t1 = nearest_month(t1) + intvl = match(day/30,months) + next_tick = next_month + end + else -- case D + if DEBUG then print '(years)' end + fmt = '%Y' -- e.g. 2012 + t1 = nearest_year(t1) + intvl = match(day/YEAR_DAYS,years) + end + else + local m = floor(span/MINUTE) + if m < 4 then -- case A + if DEBUG then print '(sec)' end + fmt = '%X' -- e.g. 12:31:40 + intvl = match(span,seconds) + else -- case B + fmt = hour_format + if m < 4*60 then -- less than 4 hours + if DEBUG then print '(min)'end + intvl = match(m,minutes) + else + if DEBUG then print '(hours)' end + intvl = match(m/60,hours) + end + end + end + if not intvl then return print 'no match' end + + if type(fmt) == 'string' then + local dspec = fmt + fmt = function(x) return odate(dspec,x) end + end + if not next_tick then + next_tick = function(t,tick) + return t + tick + end + end + local t,tend, oldt = t1, t2 + local res,append = {},table.insert + while t < tend do + append(res,{t,fmt(t,oldt)}) + oldt = t + t = next_tick(t,intvl) + end + return res,intvl +end + +if arg then + require 'pl' + + local df = Date.Format() + + function test (s1,s2) + local d1,d2 = df:parse(s1),df:parse(s2) + print(d1,d2,d2:diff(d1)) + local res,intvl = classify(d1.time,d2.time) + print('interval',Date(intvl,true)) + for _,item in ipairs(res) do + print(df:tostring(item[1]),item[2]) + end + end + + --~ test('4 Sep','6 Sep') + --~ test('14:20','14:22') + --test('2:05','2:20') + + local tests = { + {'2:15','2.17'}, + {'1 Sep 23:00','2 Sep 00:05'}, + {'14:20','15:22'}, + {'2:10','16:15'}, + {'4 Sep', '6 Sep'}, + {'3 Aug 2010', '2 Feb 2011'}, + {'1 Jan 2010', '3 Mar 2016'}, + } + + if arg[1] then + test(arg[1],arg[2]) + else + for _,pair in ipairs(tests) do + test(pair[1],pair[2]) + print '--------------' + end + end +else + return function(axis) + return classify(axis.min,axis.max) + end +end + diff --git a/app/src/main/assets/android/socket_async.lua b/app/src/main/assets/android/socket_async.lua new file mode 100644 index 0000000..a17652a --- /dev/null +++ b/app/src/main/assets/android/socket_async.lua @@ -0,0 +1,34 @@ +require 'android.import' +local utils = require 'android.utils' +local N = luajava.package 'java.net' + +return function (thrd,arg) + local addr = arg:get 'addr' + local port = arg:get 'port' + local server = addr=='*' + local client, reader, line + if not server then + if type(addr) == 'userdata' then + client = addr + else + client = utils.open_socket(addr,port) + end + else + server = N.ServerSocket(port) + client = server:accept() + end + arg:put('socket',client) + reader = utils.buffered_reader(client:getInputStream()) + client:setKeepAlive(true) + pcall(function() + line = reader:readLine() + while line do + thrd:setProgress(line) + line = reader:readLine() + end + end) + reader:close() + client:close() + if server then server:close() end + return 'ok' +end diff --git a/app/src/main/assets/android/touch.lua b/app/src/main/assets/android/touch.lua new file mode 100644 index 0000000..7ad965c --- /dev/null +++ b/app/src/main/assets/android/touch.lua @@ -0,0 +1,141 @@ +--- AndroLua Touch handling. +-- This module returns a function which generates a suitable `OnTouchEvent` +-- handler. When creating a custom view using @{android.luaView} you can say: +-- +-- T = { +-- draw = ...; +-- onTouchEvent = require 'android.touch'(T) +-- touch = function(kind,idx,x,y,dir,movement) ... end +-- } +-- me:luaView(T) +-- +-- and then `T.touch` will be called appropriately. `kind` can one of 'TAP','DOUBLE-TAP', +-- 'PRESSED' (i.e. long-press) 'SWIPE' and 'PINCH'. For all kinds of events, `idx` is the +-- number of pointers involved (e.g. `idx==2` means that two fingers are involved in a +-- guesture) and `x`,'y` are the view coordinates of the center of the guesture. +-- For 'SWIPE' and 'PINCH', `dir` can be 'X' or 'Y', and then `movement` is non zero. +-- +-- @module android.touch +local append,out = table.insert,{} +local max,abs,sqrt = math.max,math.abs,math.sqrt + +local function len (x,y) + return sqrt(x^2 + y^2) +end + +local function dist (x1,y1,x2,y2) + return len(x1 - x2,y1 - y2) +end + +local function sign (x) + return x >= 0 and 1 or -1 +end + +local MotionEvent = bind 'android.view.MotionEvent' +local actions = { + 'ACTION_DOWN','ACTION_POINTER_DOWN','ACTION_UP','ACTION_POINTER_UP','ACTION_MOVE' +} + +local action_map = {} +for _,aname in ipairs(actions) do + local a = MotionEvent[aname] + action_map[a] = aname +end + +local long_press, double_tap, swipe_min = 600,300,12 +local empty = {UP='ACTION_UP'} +local state = empty + +local function reset_state (when) + state = empty +end + +local function newstate (kind,ev,idx) + idx = idx or (ev:getActionIndex() + 1) -- 1-based pointer indices + local np = ev:getPointerCount() + local x,y = {},{} + for i = 1,np do + x[i] = ev:getX(i-1) + y[i] = ev:getY(i-1) + end + return {UP='ACTION_UP',kind=kind,idx=idx,time=ev:getEventTime(),x=x,y=y} +end + +local function movement (s1,s2,idx) + return dist(s1.x[idx],s1.y[idx],s2.x[idx],s2.y[idx]) +end + +return function(obj) +return function (ev) + local action = ev:getActionMasked() + local aname = action_map[action] + if aname then + local nstate + if aname == 'ACTION_DOWN' or aname == 'ACTION_POINTER_DOWN' then + nstate = newstate(aname,ev) + if state.kind == state.UP and nstate.time-state.time < double_tap then + local idx = state.idx + local x,y = state.x[idx],state.y[idx] + obj.touch('DOUBLE-TAP',idx,x,y,'NONE') + reset_state 'db' + else + state = nstate + state.UP = aname == 'ACTION_POINTER_DOWN' and 'ACTION_POINTER_UP' or 'ACTION_UP' + end + if obj.down then obj.down(nstate.x[1],nstate.y[1]) end + elseif aname == state.UP and state ~= empty then + local idx = state.idx + local x,y = state.x[idx],state.y[idx] + nstate = newstate(aname,ev,idx) + if obj.up then obj.up(x,y) end + if movement(nstate,state,1) > swipe_min then -- finger(s) dragged.. + local sx,sy,nx,ny = state.x,state.y,nstate.x,nstate.y + --local dd = deltas(nstate,state) + -- how much the finger moved, and its direction + local dx1,dy1 = sx[1]-nx[1],sy[1]-ny[1] + local sgn,axis=1 + if abs(dx1) > abs(dy1) then + axis = 'X' + if dx1 < 0 then sgn = -1 end -- -ve means right.. + else + axis = 'Y' + if dy1 < 0 then sgn = -1 end -- -ve means up.. + end + if idx > 1 then -- two fingers! + local dx2,dy2 = sx[2]-nx[2], sy[2]-ny[2] --dd.deltax[2], dd.deltay[2] + local d1,d2 = len(dx1,dy1),len(dx2,dy2) + local z = max(d1,d2) + if sign(dx1)==sign(dx2) and sign(dy1)==sign(dy2) then + obj.touch('SWIPE',idx,x,y,axis,sgn*z) + else + local is_x = abs(dx1) > abs(dy1) + local startd = dist(sx[1],sy[1],sx[2],sy[2]) + local endd = dist(nx[1],ny[1],nx[2],ny[2]) + if startd > endd then + z = -z -- -ve means moving in... + x,y = (nx[1]+nx[2])/2, (ny[1]+ny[2])/2 + else + x,y = (sx[1]+sx[2])/2, (sy[1]+sy[2])/2 + end + obj.touch('PINCH',idx,x,y,(is_x and 'X' or 'Y'),2*z) + end + else + obj.touch('SWIPE',idx,x,y,axis,sgn*len(dx1,dy1)) + end + reset_state 'swipe' + elseif nstate.time - state.time < long_press then + obj.touch('TAP',idx,x,y,'NONE') + -- currently we don't do double-tap with two fingers + state = idx == 1 and nstate or empty + else + obj.touch('PRESSED',idx,x,y,'NONE') + reset_state 'pressed' + end + elseif aname == 'ACTION_MOVE' and obj.move then + obj.move(ev:getX(),ev:getY()) + end + + end + return true +end +end diff --git a/app/src/main/assets/android/utils.lua b/app/src/main/assets/android/utils.lua new file mode 100644 index 0000000..6ef66ec --- /dev/null +++ b/app/src/main/assets/android/utils.lua @@ -0,0 +1,53 @@ +--- I/O Utilities +-- @module android.utils +require 'android.import' +local L = luajava.package 'java.lang' +local IO = luajava.package 'java.io' +local N = luajava.package 'java.net' +local BUFSZ = 4*1024 + +local utils = {} + +--- read all the bytes from a stream as a byte array. +-- @tparam L.InputStream f +-- @treturn [byte] +function utils.readbytes(f) + local buff = L.Byte{n = BUFSZ} + local out = IO.ByteArrayOutputStream(BUFSZ) + local n = f:read(buff) + while n ~= -1 do + out:write(buff,0,n) + n = f:read(buff,0,BUFSZ) + end + f:close() + return out:toByteArray() +end + +--- read all the bytes from a stream as a string. +-- @tparam L.InputStream f +-- @treturn string +function utils.readstring(f) + return tostring(L.String(utils.readbytes(f))) +end + +function utils.open_socket (host,port,timeout) + local client = N.Socket() + local addr = N.InetSocketAddress(host,port) + local ok,err = pcall(function() + client:connect(addr,timeout or 300) + end) + if ok then return client else return nil,err end +end + +function utils.buffered_reader (stream) + return IO.BufferedReader(IO.InputStreamReader(stream)) +end + +function utils.reader_writer (c) + local r = utils.buffered_reader(c:getInputStream()) + local w = IO.PrintWriter(c:getOutputStream()) + return r,w +end + +return utils + diff --git a/app/src/main/assets/example/draw.lua b/app/src/main/assets/example/draw.lua new file mode 100644 index 0000000..37d043d --- /dev/null +++ b/app/src/main/assets/example/draw.lua @@ -0,0 +1,92 @@ +draw = require 'android'.new() +local G = luajava.package 'android.graphics' +local L = luajava.package 'java.lang' + +local paint = G.Paint() +local RED = G.Color.RED +local WHITE = G.Color.WHITE +local BLUE = G.Color.BLUE +-- note how nested classes are accessed... +local FILL = G.Paint_Style.FILL +local STROKE = G.Paint_Style.STROKE + +-- http://bestsiteinthemultiverse.com/2008/11/android-graphics-example/ + +function draw.onDraw(c) + paint:setStyle(FILL) + paint:setColor(WHITE) + c:drawPaint(paint) + paint:setColor(BLUE) + c:drawCircle(20,20,15,paint) + paint:setAntiAlias(true) + c:drawCircle(60,20,15,paint) + + paint:setAntiAlias(false) + paint:setColor(RED) + c:drawRect(100,5,200,30,paint) + + paint:setStyle(STROKE) + paint:setStrokeWidth(2) + paint:setColor(RED) + local path = G.Path() + path:moveTo(0,-10) + path:lineTo(5,0) + path:lineTo(-5,0) + path:close() + + -- can now repeatedly draw triangles + path:offset(10,40) + c:drawPath(path,paint) + path:offset(50,100) + c:drawPath(path,paint) + -- offsets are cumulative + path:offset(50,100) + c:drawPath(path,paint) + + paint:setStyle(STROKE) + paint:setStrokeWidth(1) + paint:setColor(G.Color.MAGENTA) + paint:setTextSize(30) + c:drawText("Style.STROKE",75,75,paint) + + paint:setStyle(FILL) + paint:setAntiAlias(true) + c:drawText("Style.FILL",75,110,paint) + + local x,y = 75,185 + paint:setColor(G.Color.GRAY) + paint:setTextSize(25) + local str = "Rotated" + local rect = G.Rect() + paint:getTextBounds(str,0,#str,rect) + c:translate(x,y) + paint:setStyle(STROKE) + c:drawText(str,0,0,paint) + c:drawRect(rect,paint) + c:translate(-x,-y) + c:save() + c:rotate(90, x + rect:exactCenterX(), + y + rect:exactCenterY()) + paint:setStyle(FILL) + c:drawText(str,x,y,paint) + c:restore() + + local dash = G.DashPathEffect(L.Float{20,5},1) + paint:setPathEffect(dash) + paint:setStrokeWidth(8) + c:drawLine(0,300,320,300,paint) + +end + + +function draw.create (me) + local view = me:luaView(draw) + me:options_menu { + "source",function() + me:luaActivity('example.pretty','example.draw') + end, + } + return view +end + +return draw diff --git a/app/src/main/assets/example/expanded.lua b/app/src/main/assets/example/expanded.lua new file mode 100644 index 0000000..74a2f2a --- /dev/null +++ b/app/src/main/assets/example/expanded.lua @@ -0,0 +1,63 @@ +-- a simple Expandable List View in Lua +-- You have to at least provide `getGroupView` and `getChildView`; +-- the first must provide some padding to avoid the icon. Note that these +-- functions are passed an extra first value which is the group or view object. +expanded = require 'android'.new() + +-- the data is a list of entries, which are lists of children plus a corresponding +-- 'group' field +-- In this case, we fill in the children just before the group is expanded, +-- i.e. it's a lazy list. +groups = { + {group='os'}, + {group='coroutine'}, + {group='io'}, +} + +function expanded.create(me) + local groupStyle = me:textStyle{paddingLeft='35sp',size='30sp'} + local childStyle = me:textStyle{paddingLeft='50sp',size='20sp'} + local elv,adapter + elv,adapter = me:luaExpandableListView (groups,{ + + onGroupExpanded = function (groupPos) + -- this is the group data + local group = groups[groupPos+1].group + -- and the group's children should be here + local children = groups[groupPos+1] + if #children == 0 then + for key in pairs(_G[group]) do + table.insert(children,key) + end + adapter:notifyDataSetChanged() + end + end; + + + getGroupView = function (group,groupPos,expanded,view,parent) + return me:hbox {groupStyle(group)} + end; + + getChildView = function (child,groupPos,childPos,lastChild,view,parent) + return childStyle(child) + end; + + -- may optionally override this as well - remember to return a boolean! + onChildClick = function(child) + me:toast('child: '..child) + return true + end; + + }) + + me:options_menu { + "source",function() + me:luaActivity('example.pretty','example.expanded') + end, + } + + return elv +end + +return expanded + diff --git a/app/src/main/assets/example/financial.lua b/app/src/main/assets/example/financial.lua new file mode 100644 index 0000000..a6f649a --- /dev/null +++ b/app/src/main/assets/example/financial.lua @@ -0,0 +1,62 @@ +-- AndroLua plot version of this Flot example: +-- http://people.iola.dk/olau/flot/examples/multiple-axes.html +-- (Except we don't have multiple axes - yet) + +local financial = require 'android'.new() +local Plot = require 'android.plot' +local plotdata = require 'example.plotdata' + +function financial.create (me) + + local plot = Plot.new { + interactive=true, + grid=true, + legend = { corner='CB'}, + xaxis = { type='date'}, + -- series are in the array part + { + label='oil price US$',tag='oil', + data = plotdata.oilprices, + xunits='msec', + width=2, + }, + { label='USD EUR Exchange Rate', + data = plotdata.exchangerates, + xunits='msec', + width=2, + scale_to_y = 'oil', + }, + annotations = { + { + y = 100,color='red',tag='line' + }, + { + text='Inverse Correlation: Oil Price USD/EUR', + size='20sp' + }, + } + } + -- can access series data and use array methods + local oil = plot:get_series 'oil' + local idx = oil.ydata:find_linear(100) + -- time is already in Unix-style! + local t = oil.xdata:at(idx) + + -- this text annotation is positioned at the start point of + -- the annotation with tag 'line' + plot:add_annotation { + text="$100 limit reached at "..os.date('%Y-%m-%d',t), + anot='line',point='start', + } + + me:options_menu { + "source",function() + me:luaActivity('example.pretty','example.financial') + end, + } + + return plot:view(me) +end + +return financial + diff --git a/app/src/main/assets/example/form.lua b/app/src/main/assets/example/form.lua new file mode 100644 index 0000000..4488cb7 --- /dev/null +++ b/app/src/main/assets/example/form.lua @@ -0,0 +1,73 @@ +form = require 'android'.new() +-- needed for creating an email.. +require 'android.intents' + +-- compare to this more standard implementation: +--http://mobile.tutsplus.com/tutorials/android/android-sdk-creating-forms/ + +function form.create (me) + + me.title = me:textView{'Enter feedback details to developer:',size='25sp'} + me.name = me:editText{'Your Name',inputType='textPersonName'} + me.email = me:editText{'Your Email',inputType='textEmailAddress'} + me.kind = me:spinner { + prompt='Enter feedback type'; + 'Praise','Gripe','Suggestion','Bug' + } + me.details = me:editText{'Feedback Details...',inputType='textMultiLine', + minLines=5,gravity='top|left'} + me.emailResponse = me:checkBox 'Would you like an email response?' + + local function get_text (v) + return v:getText():toString() + end + + local function send_feedback() + local name = get_text(me.name) + local email = get_text(me.email) + local details = get_text(me.details) + local gripe = me.kind:getSelectedItem() + + if name=='' or email=='' then + me:alert('Problem!','ok','Must fill in name and email address!') + return + end + + local body = 'To: The Gripe Department\n\n'.. + details..'\n\n'..name..'('..email..')' + + if me.emailResponse:isChecked() then + body = body .. '\nRequires a response' + end + + me:send_message('Application Feedback ('..gripe..')', + body,'appfeedback@yourappsite.com') + + end + + me:options_menu { + "source",function() + me:luaActivity('example.pretty','example.form') + end, + } + + return me:vbox{ + scrollable = true; + me.title, + me.name, + me.email, + me.kind, + me.details, + me.emailResponse, + me:button('Send Feedback',send_feedback), + me:button('Test Values',function() + me.name:setText 'Patsy Stone' + me.email:setText 'patsy@fabulous.org' + me.details:setText 'too slow darling!' + end) + } +end + +return form + + diff --git a/app/src/main/assets/example/launch.lua b/app/src/main/assets/example/launch.lua new file mode 100644 index 0000000..5b94cfc --- /dev/null +++ b/app/src/main/assets/example/launch.lua @@ -0,0 +1,54 @@ +-- Androlua examples +local launch = require 'android'.new() + +local groups = { + {group='GUI','list','expanded','pretty'}, + {group='Graphics','draw','plot','financial'}, + {group='Forms','form','password','.questions'}, +} + +local text = [[ +Androlua examples, by category. +All of these activities have an option menu for viewing their source. +]] + +function launch.create(me) + local groupStyle = me:textStyle{paddingLeft='35sp',size='30sp'} + local childStyle = me:textStyle{paddingLeft='50sp',size='20sp'} + local elv,adapter + elv,adapter = me:luaExpandableListView (groups,{ + + getGroupView = function (group,groupPos,expanded,view,parent) + return me:hbox {groupStyle(group)} + end; + + getChildView = function (child,groupPos,childPos,lastChild,view,parent) + return childStyle(child) + end; + + onChildClick = function(child) + if child:match '^%.' then + child = child:sub(2) + else + child = 'example.'..child + end + me:luaActivity(child) + return true + end; + + }) + + me:options_menu { + "source",function() + me:luaActivity('example.pretty','example.launch') + end, + } + + return me:vbox{ + me:textView{text,size='20sp',textColor='#EEEEFF'}, + elv + } +end + +return launch + diff --git a/app/src/main/assets/example/list.lua b/app/src/main/assets/example/list.lua new file mode 100644 index 0000000..53b99fa --- /dev/null +++ b/app/src/main/assets/example/list.lua @@ -0,0 +1,75 @@ +-- list.lua +-- This is a AndroLua activity where the view is an explicitly generated +-- custom Lua ListView, where we define the layout of each item +-- programmatically. +-- +-- Note that these activities can be passed any Lua data, which allows us +-- to _recursively_ create child activities. + +list = require 'android'.new() + +A = luajava.package 'android' +W = luajava.package 'android.widget' +V = luajava.package 'android.view' + +function contents_of (T) + local items,append = {},table.insert + for k,v in pairs(T) do + append(items,{name=k,type=type(v),value=v}) + end + return items +end + +local PC = android.parse_color +local tableclr, otherclr = PC'#FFEEAA', PC'#EEFFEE' + +function list.create (me,arg) + local name, T = 'Inspecting _G',_G -- defaults + if arg then + name = arg.name + T = arg.T + end + local items = contents_of(T) + me.a:setTitle(name) + + local lv = me:luaListView(items,function (impl,position,view,parent) + local item = items[position+1] + local txt1,txt2 + if not view then + txt1 = me:textView{id = 10, size = '20sp'} + txt2 = me:textView{id = 20, background = '#222222'} + view = me:hbox{ + id = 1, + txt1,'+',txt2 + } + else + txt1 = view:findViewById(10) + txt2 = view:findViewById(20) + end + if not pcall(function() + txt1:setText(item.name) + txt2:setText(item.type) + txt1:setTextColor(item.type=='table' and tableclr or otherclr) + end) then + print(txt1,txt2,'que?') + end + return view + end) + + me:on_item_click(lv,function(av,view,idx) + local item = items[idx+1] + if item.type == 'table' then + me:luaActivity('example.list',{T=item.value,name=name..'.'..item.name}) + end + end) + + me:options_menu { + "source",function() + me:luaActivity('example.pretty','example.list') + end, + } + + return lv +end + +return list diff --git a/app/src/main/assets/example/password.lua b/app/src/main/assets/example/password.lua new file mode 100644 index 0000000..0e89fc8 --- /dev/null +++ b/app/src/main/assets/example/password.lua @@ -0,0 +1,60 @@ +password = require 'android'.new() + + +function password.create (me) + + me.title = me:textView{ + 'Enter user name and password:', + size='20sp',textColor='white' + } + me.name = me:editText{'Your UserName',inputType='textPersonName'} + me.password = me:editText{'Your Password',inputType='textPassword'} + me.viewPassword = me:checkBox 'view password' + + me:on_click(me.viewPassword,function(v) + local type = v:isChecked() and 'text' or 'textPassword' + me:setEditArgs(me.password,{inputType=type}) + end) + + local function get_text (v) + return v:getText():toString() + end + + local function only_connect() + local name = get_text(me.name) + local password = get_text(me.password) + + if name=='' or password=='' then + me:alert('Problem!','ok','Must fill in name and password!') + return + end + + end + + me:options_menu { + "source",function() + me:luaActivity('example.pretty','example.password') + end, + } + + local v = me:vbox{ + scrollable = true; + margin = {20,10,20,10}; + background = '#447744'; + me.title, + me.name,{width='200sp'}, + me.password,{width='200sp'}, + me.viewPassword, + me:button('Login',only_connect),{fill=false,gravity='center_horizontal'}, + me:button('Test Values',function() + me.name:setText 'Patsy Stone' + me.password:setText 'fabulous' + end),{fill=false,gravity='center'}, + } + return v + +end + +return password + + diff --git a/app/src/main/assets/example/plot.lua b/app/src/main/assets/example/plot.lua new file mode 100644 index 0000000..ada8842 --- /dev/null +++ b/app/src/main/assets/example/plot.lua @@ -0,0 +1,93 @@ +-- demonstrates Androlua plotting library +local draw = require 'android'.new() +local Plot = require 'android.plot' + +function draw.create (me) + me.a:setTitle 'Androlua Plot Example' + ME = me + local pi = math.pi + local xx,sin,cos = {},{},{} + local i = 1 + for x = 0, 2*pi, 0.1 do + xx[i] = x + sin[i] = math.sin(x) + cos[i] = math.cos(x) + i = i + 1 + end + + local samples = Plot.array{0.1, 0.8*pi,1.2*pi,2*pi-0.3} + + local plot = Plot.new { + grid = true, + fill = '#EFEFFF', -- fill the plot area with light blue + aspect_ratio = 0.5, -- plot area is twice as wide as its height + -- legend is in left-bottom corner, arranged horizontally, filled with yellow + legend = { + corner='LB',across=true,fill='#FFFFEF', + }, + xaxis = { -- we have our own ticks with labels - UTF-8 is fine... + ticks = {{0,'0'},{pi/2,'π/2'},{pi,'π'},{3*pi/2,'3π/2'},{2*pi,'2π'}}, + }, + -- our series follow... + { label = 'sin', width=2, + xdata = xx, ydata = sin, + }, + { label = 'cos', width=2, + xdata = xx, ydata = cos, + }, + { + -- doesn't have label, won't be in legend + width=8,points='circle', + xdata = samples, + ydata = samples:map(math.cos) + } + } + + local xvalues = Plot.array(0,10,0.1) + + local spi = math.sqrt(2*math.pi) + + local function norm_distrib (x,mu,sigma) + local sfact = 2*sigma^2 + return math.exp(-(x-mu)^2/sfact)/(spi*sigma) + end + + local plot2 = Plot.new { + aspect_ratio = 0.5, + padding = pad, + legend = {box=false,corner='LT'}, + { + label = 'μ = 5, σ = 1',width=2,color='#000000', + xdata = xvalues, + ydata = xvalues:map(norm_distrib,5,1), + tag = '5' + }, + { + label = 'μ = 6, σ = 0.7',width=2,color='#AAAAAA', + xdata = xvalues, + ydata = xvalues:map(norm_distrib,6,0.7), + tag = '6' + }, + annotations = { + {x = 4, series='5', points=true}, + {x1 = 5.5, x2 = 6.5, color='#10000000',series=2}, + } + } + + me:options_menu { + "source",function() + me:luaActivity('example.pretty','example.plot!') + end, + } + + me.theme = {textColor='BLACK',background='WHITE'} + local caption = me:textStyle{size='15sp',gravity='center'} + + return me:vbox{ + caption 'Plot Examples', + plot:view(me), + plot2:view(me) + } +end + +return draw diff --git a/app/src/main/assets/example/plotdata.lua b/app/src/main/assets/example/plotdata.lua new file mode 100644 index 0000000..c00d847 --- /dev/null +++ b/app/src/main/assets/example/plotdata.lua @@ -0,0 +1,6 @@ +return { + oilprices = {{1167692400000,61.05}, {1167778800000,58.32}, {1167865200000,57.35}, {1167951600000,56.31}, {1168210800000,55.55}, {1168297200000,55.64}, {1168383600000,54.02}, {1168470000000,51.88}, {1168556400000,52.99}, {1168815600000,52.99}, {1168902000000,51.21}, {1168988400000,52.24}, {1169074800000,50.48}, {1169161200000,51.99}, {1169420400000,51.13}, {1169506800000,55.04}, {1169593200000,55.37}, {1169679600000,54.23}, {1169766000000,55.42}, {1170025200000,54.01}, {1170111600000,56.97}, {1170198000000,58.14}, {1170284400000,58.14}, {1170370800000,59.02}, {1170630000000,58.74}, {1170716400000,58.88}, {1170802800000,57.71}, {1170889200000,59.71}, {1170975600000,59.89}, {1171234800000,57.81}, {1171321200000,59.06}, {1171407600000,58.00}, {1171494000000,57.99}, {1171580400000,59.39}, {1171839600000,59.39}, {1171926000000,58.07}, {1172012400000,60.07}, {1172098800000,61.14}, {1172444400000,61.39}, {1172530800000,61.46}, {1172617200000,61.79}, {1172703600000,62.00}, {1172790000000,60.07}, {1173135600000,60.69}, {1173222000000,61.82}, {1173308400000,60.05}, {1173654000000,58.91}, {1173740400000,57.93}, {1173826800000,58.16}, {1173913200000,57.55}, {1173999600000,57.11}, {1174258800000,56.59}, {1174345200000,59.61}, {1174518000000,61.69}, {1174604400000,62.28}, {1174860000000,62.91}, {1174946400000,62.93}, {1175032800000,64.03}, {1175119200000,66.03}, {1175205600000,65.87}, {1175464800000,64.64}, {1175637600000,64.38}, {1175724000000,64.28}, {1175810400000,64.28}, {1176069600000,61.51}, {1176156000000,61.89}, {1176242400000,62.01}, {1176328800000,63.85}, {1176415200000,63.63}, {1176674400000,63.61}, {1176760800000,63.10}, {1176847200000,63.13}, {1176933600000,61.83}, {1177020000000,63.38}, {1177279200000,64.58}, {1177452000000,65.84}, {1177538400000,65.06}, {1177624800000,66.46}, {1177884000000,64.40}, {1178056800000,63.68}, {1178143200000,63.19}, {1178229600000,61.93}, {1178488800000,61.47}, {1178575200000,61.55}, {1178748000000,61.81}, {1178834400000,62.37}, {1179093600000,62.46}, {1179180000000,63.17}, {1179266400000,62.55}, {1179352800000,64.94}, {1179698400000,66.27}, {1179784800000,65.50}, {1179871200000,65.77}, {1179957600000,64.18}, {1180044000000,65.20}, {1180389600000,63.15}, {1180476000000,63.49}, {1180562400000,65.08}, {1180908000000,66.30}, {1180994400000,65.96}, {1181167200000,66.93}, {1181253600000,65.98}, {1181599200000,65.35}, {1181685600000,66.26}, {1181858400000,68.00}, {1182117600000,69.09}, {1182204000000,69.10}, {1182290400000,68.19}, {1182376800000,68.19}, {1182463200000,69.14}, {1182722400000,68.19}, {1182808800000,67.77}, {1182895200000,68.97}, {1182981600000,69.57}, {1183068000000,70.68}, {1183327200000,71.09}, {1183413600000,70.92}, {1183586400000,71.81}, {1183672800000,72.81}, {1183932000000,72.19}, {1184018400000,72.56}, {1184191200000,72.50}, {1184277600000,74.15}, {1184623200000,75.05}, {1184796000000,75.92}, {1184882400000,75.57}, {1185141600000,74.89}, {1185228000000,73.56}, {1185314400000,75.57}, {1185400800000,74.95}, {1185487200000,76.83}, {1185832800000,78.21}, {1185919200000,76.53}, {1186005600000,76.86}, {1186092000000,76.00}, {1186437600000,71.59}, {1186696800000,71.47}, {1186956000000,71.62}, {1187042400000,71.00}, {1187301600000,71.98}, {1187560800000,71.12}, {1187647200000,69.47}, {1187733600000,69.26}, {1187820000000,69.83}, {1187906400000,71.09}, {1188165600000,71.73}, {1188338400000,73.36}, {1188511200000,74.04}, {1188856800000,76.30}, {1189116000000,77.49}, {1189461600000,78.23}, {1189548000000,79.91}, {1189634400000,80.09}, {1189720800000,79.10}, {1189980000000,80.57}, {1190066400000,81.93}, {1190239200000,83.32}, {1190325600000,81.62}, {1190584800000,80.95}, {1190671200000,79.53}, {1190757600000,80.30}, {1190844000000,82.88}, {1190930400000,81.66}, {1191189600000,80.24}, {1191276000000,80.05}, {1191362400000,79.94}, {1191448800000,81.44}, {1191535200000,81.22}, {1191794400000,79.02}, {1191880800000,80.26}, {1191967200000,80.30}, {1192053600000,83.08}, {1192140000000,83.69}, {1192399200000,86.13}, {1192485600000,87.61}, {1192572000000,87.40}, {1192658400000,89.47}, {1192744800000,88.60}, {1193004000000,87.56}, {1193090400000,87.56}, {1193176800000,87.10}, {1193263200000,91.86}, {1193612400000,93.53}, {1193698800000,94.53}, {1193871600000,95.93}, {1194217200000,93.98}, {1194303600000,96.37}, {1194476400000,95.46}, {1194562800000,96.32}, {1195081200000,93.43}, {1195167600000,95.10}, {1195426800000,94.64}, {1195513200000,95.10}, {1196031600000,97.70}, {1196118000000,94.42}, {1196204400000,90.62}, {1196290800000,91.01}, {1196377200000,88.71}, {1196636400000,88.32}, {1196809200000,90.23}, {1196982000000,88.28}, {1197241200000,87.86}, {1197327600000,90.02}, {1197414000000,92.25}, {1197586800000,90.63}, {1197846000000,90.63}, {1197932400000,90.49}, {1198018800000,91.24}, {1198105200000,91.06}, {1198191600000,90.49}, {1198710000000,96.62}, {1198796400000,96.00}, {1199142000000,99.62}, {1199314800000,99.18}, {1199401200000,95.09}, {1199660400000,96.33}, {1199833200000,95.67}, {1200351600000,91.90}, {1200438000000,90.84}, {1200524400000,90.13}, {1200610800000,90.57}, {1200956400000,89.21}, {1201042800000,86.99}, {1201129200000,89.85}, {1201474800000,90.99}, {1201561200000,91.64}, {1201647600000,92.33}, {1201734000000,91.75}, {1202079600000,90.02}, {1202166000000,88.41}, {1202252400000,87.14}, {1202338800000,88.11}, {1202425200000,91.77}, {1202770800000,92.78}, {1202857200000,93.27}, {1202943600000,95.46}, {1203030000000,95.46}, {1203289200000,101.74}, {1203462000000,98.81}, {1203894000000,100.88}, {1204066800000,99.64}, {1204153200000,102.59}, {1204239600000,101.84}, {1204498800000,99.52}, {1204585200000,99.52}, {1204671600000,104.52}, {1204758000000,105.47}, {1204844400000,105.15}, {1205103600000,108.75}, {1205276400000,109.92}, {1205362800000,110.33}, {1205449200000,110.21}, {1205708400000,105.68}, {1205967600000,101.84}, {1206313200000,100.86}, {1206399600000,101.22}, {1206486000000,105.90}, {1206572400000,107.58}, {1206658800000,105.62}, {1206914400000,101.58}, {1207000800000,100.98}, {1207173600000,103.83}, {1207260000000,106.23}, {1207605600000,108.50}, {1207778400000,110.11}, {1207864800000,110.14}, {1208210400000,113.79}, {1208296800000,114.93}, {1208383200000,114.86}, {1208728800000,117.48}, {1208815200000,118.30}, {1208988000000,116.06}, {1209074400000,118.52}, {1209333600000,118.75}, {1209420000000,113.46}, {1209592800000,112.52}, {1210024800000,121.84}, {1210111200000,123.53}, {1210197600000,123.69}, {1210543200000,124.23}, {1210629600000,125.80}, {1210716000000,126.29}, {1211148000000,127.05}, {1211320800000,129.07}, {1211493600000,132.19}, {1211839200000,128.85}, {1212357600000,127.76}, {1212703200000,138.54}, {1212962400000,136.80}, {1213135200000,136.38}, {1213308000000,134.86}, {1213653600000,134.01}, {1213740000000,136.68}, {1213912800000,135.65}, {1214172000000,134.62}, {1214258400000,134.62}, {1214344800000,134.62}, {1214431200000,139.64}, {1214517600000,140.21}, {1214776800000,140.00}, {1214863200000,140.97}, {1214949600000,143.57}, {1215036000000,145.29}, {1215381600000,141.37}, {1215468000000,136.04}, {1215727200000,146.40}, {1215986400000,145.18}, {1216072800000,138.74}, {1216159200000,134.60}, {1216245600000,129.29}, {1216332000000,130.65}, {1216677600000,127.95}, {1216850400000,127.95}, {1217282400000,122.19}, {1217455200000,124.08}, {1217541600000,125.10}, {1217800800000,121.41}, {1217887200000,119.17}, {1217973600000,118.58}, {1218060000000,120.02}, {1218405600000,114.45}, {1218492000000,113.01}, {1218578400000,116.00}, {1218751200000,113.77}, {1219010400000,112.87}, {1219096800000,114.53}, {1219269600000,114.98}, {1219356000000,114.98}, {1219701600000,116.27}, {1219788000000,118.15}, {1219874400000,115.59}, {1219960800000,115.46}, {1220306400000,109.71}, {1220392800000,109.35}, {1220565600000,106.23}, {1220824800000,106.34}}, + exchangerates = {{1167606000000,0.7580}, {1167692400000,0.7580}, {1167778800000,0.75470}, + {1167865200000,0.75490}, {1167951600000,0.76130}, {1168038000000,0.76550}, {1168124400000,0.76930}, {1168210800000,0.76940}, {1168297200000,0.76880}, {1168383600000,0.76780}, {1168470000000,0.77080}, {1168556400000,0.77270}, {1168642800000,0.77490}, {1168729200000,0.77410}, {1168815600000,0.77410}, {1168902000000,0.77320}, {1168988400000,0.77270}, {1169074800000,0.77370}, {1169161200000,0.77240}, {1169247600000,0.77120}, {1169334000000,0.7720}, {1169420400000,0.77210}, {1169506800000,0.77170}, {1169593200000,0.77040}, {1169679600000,0.7690}, {1169766000000,0.77110}, {1169852400000,0.7740}, {1169938800000,0.77450}, {1170025200000,0.77450}, {1170111600000,0.7740}, {1170198000000,0.77160}, {1170284400000,0.77130}, {1170370800000,0.76780}, {1170457200000,0.76880}, {1170543600000,0.77180}, {1170630000000,0.77180}, {1170716400000,0.77280}, {1170802800000,0.77290}, {1170889200000,0.76980}, {1170975600000,0.76850}, {1171062000000,0.76810}, {1171148400000,0.7690}, {1171234800000,0.7690}, {1171321200000,0.76980}, {1171407600000,0.76990}, {1171494000000,0.76510}, {1171580400000,0.76130}, {1171666800000,0.76160}, {1171753200000,0.76140}, {1171839600000,0.76140}, {1171926000000,0.76070}, {1172012400000,0.76020}, {1172098800000,0.76110}, {1172185200000,0.76220}, {1172271600000,0.76150}, {1172358000000,0.75980}, {1172444400000,0.75980}, {1172530800000,0.75920}, {1172617200000,0.75730}, {1172703600000,0.75660}, {1172790000000,0.75670}, {1172876400000,0.75910}, {1172962800000,0.75820}, {1173049200000,0.75850}, {1173135600000,0.76130}, {1173222000000,0.76310}, {1173308400000,0.76150}, {1173394800000,0.760}, {1173481200000,0.76130}, {1173567600000,0.76270}, {1173654000000,0.76270}, {1173740400000,0.76080}, {1173826800000,0.75830}, {1173913200000,0.75750}, {1173999600000,0.75620}, {1174086000000,0.7520}, {1174172400000,0.75120}, {1174258800000,0.75120}, {1174345200000,0.75170}, {1174431600000,0.7520}, {1174518000000,0.75110}, {1174604400000,0.7480}, {1174690800000,0.75090}, {1174777200000,0.75310}, {1174860000000,0.75310}, {1174946400000,0.75270}, {1175032800000,0.74980}, {1175119200000,0.74930}, {1175205600000,0.75040}, {1175292000000,0.750}, {1175378400000,0.74910}, {1175464800000,0.74910}, {1175551200000,0.74850}, {1175637600000,0.74840}, {1175724000000,0.74920}, {1175810400000,0.74710}, {1175896800000,0.74590}, {1175983200000,0.74770}, {1176069600000,0.74770}, {1176156000000,0.74830}, {1176242400000,0.74580}, {1176328800000,0.74480}, {1176415200000,0.7430}, {1176501600000,0.73990}, {1176588000000,0.73950}, {1176674400000,0.73950}, {1176760800000,0.73780}, {1176847200000,0.73820}, {1176933600000,0.73620}, {1177020000000,0.73550}, {1177106400000,0.73480}, {1177192800000,0.73610}, {1177279200000,0.73610}, {1177365600000,0.73650}, {1177452000000,0.73620}, {1177538400000,0.73310}, {1177624800000,0.73390}, {1177711200000,0.73440}, {1177797600000,0.73270}, {1177884000000,0.73270}, {1177970400000,0.73360}, {1178056800000,0.73330}, {1178143200000,0.73590}, {1178229600000,0.73590}, {1178316000000,0.73720}, {1178402400000,0.7360}, {1178488800000,0.7360}, {1178575200000,0.7350}, {1178661600000,0.73650}, {1178748000000,0.73840}, {1178834400000,0.73950}, {1178920800000,0.74130}, {1179007200000,0.73970}, {1179093600000,0.73960}, {1179180000000,0.73850}, {1179266400000,0.73780}, {1179352800000,0.73660}, {1179439200000,0.740}, {1179525600000,0.74110}, {1179612000000,0.74060}, {1179698400000,0.74050}, {1179784800000,0.74140}, {1179871200000,0.74310}, {1179957600000,0.74310}, {1180044000000,0.74380}, {1180130400000,0.74430}, {1180216800000,0.74430}, {1180303200000,0.74430}, {1180389600000,0.74340}, {1180476000000,0.74290}, {1180562400000,0.74420}, {1180648800000,0.7440}, {1180735200000,0.74390}, {1180821600000,0.74370}, {1180908000000,0.74370}, {1180994400000,0.74290}, {1181080800000,0.74030}, {1181167200000,0.73990}, {1181253600000,0.74180}, {1181340000000,0.74680}, {1181426400000,0.7480}, {1181512800000,0.7480}, {1181599200000,0.7490}, {1181685600000,0.74940}, {1181772000000,0.75220}, {1181858400000,0.75150}, {1181944800000,0.75020}, {1182031200000,0.74720}, {1182117600000,0.74720}, {1182204000000,0.74620}, {1182290400000,0.74550}, {1182376800000,0.74490}, {1182463200000,0.74670}, {1182549600000,0.74580}, {1182636000000,0.74270}, {1182722400000,0.74270}, {1182808800000,0.7430}, {1182895200000,0.74290}, {1182981600000,0.7440}, {1183068000000,0.7430}, {1183154400000,0.74220}, {1183240800000,0.73880}, {1183327200000,0.73880}, {1183413600000,0.73690}, {1183500000000,0.73450}, {1183586400000,0.73450}, {1183672800000,0.73450}, {1183759200000,0.73520}, {1183845600000,0.73410}, {1183932000000,0.73410}, {1184018400000,0.7340}, {1184104800000,0.73240}, {1184191200000,0.72720}, {1184277600000,0.72640}, {1184364000000,0.72550}, {1184450400000,0.72580}, {1184536800000,0.72580}, {1184623200000,0.72560}, {1184709600000,0.72570}, {1184796000000,0.72470}, {1184882400000,0.72430}, {1184968800000,0.72440}, {1185055200000,0.72350}, {1185141600000,0.72350}, {1185228000000,0.72350}, {1185314400000,0.72350}, {1185400800000,0.72620}, {1185487200000,0.72880}, {1185573600000,0.73010}, {1185660000000,0.73370}, {1185746400000,0.73370}, {1185832800000,0.73240}, {1185919200000,0.72970}, {1186005600000,0.73170}, {1186092000000,0.73150}, {1186178400000,0.72880}, {1186264800000,0.72630}, {1186351200000,0.72630}, {1186437600000,0.72420}, {1186524000000,0.72530}, {1186610400000,0.72640}, {1186696800000,0.7270}, {1186783200000,0.73120}, {1186869600000,0.73050}, {1186956000000,0.73050}, {1187042400000,0.73180}, {1187128800000,0.73580}, {1187215200000,0.74090}, {1187301600000,0.74540}, {1187388000000,0.74370}, {1187474400000,0.74240}, {1187560800000,0.74240}, {1187647200000,0.74150}, {1187733600000,0.74190}, {1187820000000,0.74140}, {1187906400000,0.73770}, {1187992800000,0.73550}, {1188079200000,0.73150}, {1188165600000,0.73150}, {1188252000000,0.7320}, {1188338400000,0.73320}, {1188424800000,0.73460}, {1188511200000,0.73280}, {1188597600000,0.73230}, {1188684000000,0.7340}, {1188770400000,0.7340}, {1188856800000,0.73360}, {1188943200000,0.73510}, {1189029600000,0.73460}, {1189116000000,0.73210}, {1189202400000,0.72940}, {1189288800000,0.72660}, {1189375200000,0.72660}, {1189461600000,0.72540}, {1189548000000,0.72420}, {1189634400000,0.72130}, {1189720800000,0.71970}, {1189807200000,0.72090}, {1189893600000,0.7210}, {1189980000000,0.7210}, {1190066400000,0.7210}, {1190152800000,0.72090}, {1190239200000,0.71590}, {1190325600000,0.71330}, {1190412000000,0.71050}, {1190498400000,0.70990}, {1190584800000,0.70990}, {1190671200000,0.70930}, {1190757600000,0.70930}, {1190844000000,0.70760}, {1190930400000,0.7070}, {1191016800000,0.70490}, {1191103200000,0.70120}, {1191189600000,0.70110}, {1191276000000,0.70190}, {1191362400000,0.70460}, {1191448800000,0.70630}, {1191535200000,0.70890}, {1191621600000,0.70770}, {1191708000000,0.70770}, {1191794400000,0.70770}, {1191880800000,0.70910}, {1191967200000,0.71180}, {1192053600000,0.70790}, {1192140000000,0.70530}, {1192226400000,0.7050}, {1192312800000,0.70550}, {1192399200000,0.70550}, {1192485600000,0.70450}, {1192572000000,0.70510}, {1192658400000,0.70510}, {1192744800000,0.70170}, {1192831200000,0.70}, {1192917600000,0.69950}, {1193004000000,0.69940}, {1193090400000,0.70140}, {1193176800000,0.70360}, {1193263200000,0.70210}, {1193349600000,0.70020}, {1193436000000,0.69670}, {1193522400000,0.6950}, {1193612400000,0.6950}, {1193698800000,0.69390}, {1193785200000,0.6940}, {1193871600000,0.69220}, {1193958000000,0.69190}, {1194044400000,0.69140}, {1194130800000,0.68940}, {1194217200000,0.68910}, {1194303600000,0.69040}, {1194390000000,0.6890}, {1194476400000,0.68340}, {1194562800000,0.68230}, {1194649200000,0.68070}, {1194735600000,0.68150}, {1194822000000,0.68150}, {1194908400000,0.68470}, {1194994800000,0.68590}, {1195081200000,0.68220}, {1195167600000,0.68270}, {1195254000000,0.68370}, {1195340400000,0.68230}, {1195426800000,0.68220}, {1195513200000,0.68220}, {1195599600000,0.67920}, {1195686000000,0.67460}, {1195772400000,0.67350}, {1195858800000,0.67310}, {1195945200000,0.67420}, {1196031600000,0.67440}, {1196118000000,0.67390}, {1196204400000,0.67310}, {1196290800000,0.67610}, {1196377200000,0.67610}, {1196463600000,0.67850}, {1196550000000,0.68180}, {1196636400000,0.68360}, {1196722800000,0.68230}, {1196809200000,0.68050}, {1196895600000,0.67930}, {1196982000000,0.68490}, {1197068400000,0.68330}, {1197154800000,0.68250}, {1197241200000,0.68250}, {1197327600000,0.68160}, {1197414000000,0.67990}, {1197500400000,0.68130}, {1197586800000,0.68090}, {1197673200000,0.68680}, {1197759600000,0.69330}, {1197846000000,0.69330}, {1197932400000,0.69450}, {1198018800000,0.69440}, {1198105200000,0.69460}, {1198191600000,0.69640}, {1198278000000,0.69650}, {1198364400000,0.69560}, {1198450800000,0.69560}, {1198537200000,0.6950}, {1198623600000,0.69480}, {1198710000000,0.69280}, {1198796400000,0.68870}, {1198882800000,0.68240}, {1198969200000,0.67940}, {1199055600000,0.67940}, {1199142000000,0.68030}, {1199228400000,0.68550}, {1199314800000,0.68240}, {1199401200000,0.67910}, {1199487600000,0.67830}, {1199574000000,0.67850}, {1199660400000,0.67850}, {1199746800000,0.67970}, {1199833200000,0.680}, {1199919600000,0.68030}, {1200006000000,0.68050}, {1200092400000,0.6760}, {1200178800000,0.6770}, {1200265200000,0.6770}, {1200351600000,0.67360}, {1200438000000,0.67260}, {1200524400000,0.67640}, {1200610800000,0.68210}, {1200697200000,0.68310}, {1200783600000,0.68420}, {1200870000000,0.68420}, {1200956400000,0.68870}, {1201042800000,0.69030}, {1201129200000,0.68480}, {1201215600000,0.68240}, {1201302000000,0.67880}, {1201388400000,0.68140}, {1201474800000,0.68140}, {1201561200000,0.67970}, {1201647600000,0.67690}, {1201734000000,0.67650}, {1201820400000,0.67330}, {1201906800000,0.67290}, {1201993200000,0.67580}, {1202079600000,0.67580}, {1202166000000,0.6750}, {1202252400000,0.6780}, {1202338800000,0.68330}, {1202425200000,0.68560}, + {1202511600000,0.69030}, {1202598000000,0.68960}, {1202684400000,0.68960}, {1202770800000,0.68820}, {1202857200000,0.68790}, {1202943600000,0.68620}, {1203030000000,0.68520}, {1203116400000,0.68230}, {1203202800000,0.68130}, {1203289200000,0.68130}, {1203375600000,0.68220}, {1203462000000,0.68020}, {1203548400000,0.68020}, {1203634800000,0.67840}, {1203721200000,0.67480}, {1203807600000,0.67470}, {1203894000000,0.67470}, {1203980400000,0.67480}, {1204066800000,0.67330}, {1204153200000,0.6650}, {1204239600000,0.66110}, {1204326000000,0.65830}, {1204412400000,0.6590}, {1204498800000,0.6590}, {1204585200000,0.65810}, {1204671600000,0.65780}, {1204758000000,0.65740}, {1204844400000,0.65320}, {1204930800000,0.65020}, {1205017200000,0.65140}, {1205103600000,0.65140}, {1205190000000,0.65070}, {1205276400000,0.6510}, {1205362800000,0.64890}, {1205449200000,0.64240}, {1205535600000,0.64060}, {1205622000000,0.63820}, {1205708400000,0.63820}, {1205794800000,0.63410}, {1205881200000,0.63440}, {1205967600000,0.63780}, {1206054000000,0.64390}, {1206140400000,0.64780}, {1206226800000,0.64810}, {1206313200000,0.64810}, {1206399600000,0.64940}, {1206486000000,0.64380}, {1206572400000,0.63770}, {1206658800000,0.63290}, {1206745200000,0.63360}, {1206831600000,0.63330}, {1206914400000,0.63330}, {1207000800000,0.6330}, {1207087200000,0.63710}, {1207173600000,0.64030}, {1207260000000,0.63960}, {1207346400000,0.63640}, {1207432800000,0.63560}, {1207519200000,0.63560}, {1207605600000,0.63680}, {1207692000000,0.63570}, {1207778400000,0.63540}, {1207864800000,0.6320}, {1207951200000,0.63320}, {1208037600000,0.63280}, {1208124000000,0.63310}, {1208210400000,0.63420}, {1208296800000,0.63210}, {1208383200000,0.63020}, {1208469600000,0.62780}, {1208556000000,0.63080}, {1208642400000,0.63240}, {1208728800000,0.63240}, {1208815200000,0.63070}, {1208901600000,0.62770}, {1208988000000,0.62690}, {1209074400000,0.63350}, {1209160800000,0.63920}, {1209247200000,0.640}, {1209333600000,0.64010}, {1209420000000,0.63960}, {1209506400000,0.64070}, {1209592800000,0.64230}, {1209679200000,0.64290}, {1209765600000,0.64720}, {1209852000000,0.64850}, {1209938400000,0.64860}, {1210024800000,0.64670}, {1210111200000,0.64440}, {1210197600000,0.64670}, {1210284000000,0.65090}, {1210370400000,0.64780}, {1210456800000,0.64610}, {1210543200000,0.64610}, {1210629600000,0.64680}, {1210716000000,0.64490}, {1210802400000,0.6470}, {1210888800000,0.64610}, {1210975200000,0.64520}, {1211061600000,0.64220}, {1211148000000,0.64220}, {1211234400000,0.64250}, {1211320800000,0.64140}, {1211407200000,0.63660}, {1211493600000,0.63460}, {1211580000000,0.6350}, {1211666400000,0.63460}, {1211752800000,0.63460}, {1211839200000,0.63430}, {1211925600000,0.63460}, {1212012000000,0.63790}, {1212098400000,0.64160}, {1212184800000,0.64420}, {1212271200000,0.64310}, {1212357600000,0.64310}, {1212444000000,0.64350}, {1212530400000,0.6440}, {1212616800000,0.64730}, {1212703200000,0.64690}, {1212789600000,0.63860}, {1212876000000,0.63560}, {1212962400000,0.6340}, {1213048800000,0.63460}, {1213135200000,0.6430}, {1213221600000,0.64520}, {1213308000000,0.64670}, {1213394400000,0.65060}, {1213480800000,0.65040}, {1213567200000,0.65030}, {1213653600000,0.64810}, {1213740000000,0.64510}, {1213826400000,0.6450}, {1213912800000,0.64410}, {1213999200000,0.64140}, {1214085600000,0.64090}, {1214172000000,0.64090}, {1214258400000,0.64280}, {1214344800000,0.64310}, {1214431200000,0.64180}, {1214517600000,0.63710}, {1214604000000,0.63490}, {1214690400000,0.63330}, {1214776800000,0.63340}, {1214863200000,0.63380}, {1214949600000,0.63420}, {1215036000000,0.6320}, {1215122400000,0.63180}, {1215208800000,0.6370}, {1215295200000,0.63680}, {1215381600000,0.63680}, {1215468000000,0.63830}, {1215554400000,0.63710}, {1215640800000,0.63710}, {1215727200000,0.63550}, {1215813600000,0.6320}, {1215900000000,0.62770}, {1215986400000,0.62760}, {1216072800000,0.62910}, {1216159200000,0.62740}, {1216245600000,0.62930}, {1216332000000,0.63110}, {1216418400000,0.6310}, {1216504800000,0.63120}, {1216591200000,0.63120}, {1216677600000,0.63040}, {1216764000000,0.62940}, {1216850400000,0.63480}, {1216936800000,0.63780}, {1217023200000,0.63680}, {1217109600000,0.63680}, {1217196000000,0.63680}, {1217282400000,0.6360}, {1217368800000,0.6370}, {1217455200000,0.64180}, {1217541600000,0.64110}, {1217628000000,0.64350}, {1217714400000,0.64270}, {1217800800000,0.64270}, {1217887200000,0.64190}, {1217973600000,0.64460}, {1218060000000,0.64680}, {1218146400000,0.64870}, {1218232800000,0.65940}, {1218319200000,0.66660}, {1218405600000,0.66660}, {1218492000000,0.66780}, {1218578400000,0.67120}, {1218664800000,0.67050}, {1218751200000,0.67180}, {1218837600000,0.67840}, {1218924000000,0.68110}, {1219010400000,0.68110}, {1219096800000,0.67940}, {1219183200000,0.68040}, {1219269600000,0.67810}, {1219356000000,0.67560}, {1219442400000,0.67350}, {1219528800000,0.67630}, {1219615200000,0.67620}, {1219701600000,0.67770}, {1219788000000,0.68150}, {1219874400000,0.68020}, {1219960800000,0.6780}, {1220047200000,0.67960}, {1220133600000,0.68170}, {1220220000000,0.68170}, {1220306400000,0.68320}, {1220392800000,0.68770}, {1220479200000,0.69120}, {1220565600000,0.69140}, {1220652000000,0.70090}, {1220738400000,0.70120}, {1220824800000,0.7010}, {1220911200000,0.70050}}; +} diff --git a/app/src/main/assets/example/pretty.lua b/app/src/main/assets/example/pretty.lua new file mode 100644 index 0000000..b592aad --- /dev/null +++ b/app/src/main/assets/example/pretty.lua @@ -0,0 +1,105 @@ +pretty = require 'android'.new() +local utils = require 'android.utils' + +local LP = luajava.package +local L = LP 'java.lang' +local G = LP 'android.graphics' +local W = LP 'android.widget' +local T = LP 'android.text' +local S = LP 'android.text.style' +local IO = LP 'java.io' + + +local lua_keyword = { + ["and"] = true, ["break"] = true, ["do"] = true, + ["else"] = true, ["elseif"] = true, ["end"] = true, + ["false"] = true, ["for"] = true, ["function"] = true, + ["if"] = true, ["in"] = true, ["local"] = true, ["nil"] = true, + ["not"] = true, ["or"] = true, ["repeat"] = true, + ["return"] = true, ["then"] = true, ["true"] = true, + ["until"] = true, ["while"] = true +} + +local sep = package.config:sub(1,1) + +function readmodule (me,mod) + mod = mod:gsub('%.',sep) + for m in package.path:gmatch('[^;]+') do + local nm = m:gsub('?',mod) + local f = io.open(nm,'r') + if f then + local contents = f:read '*a' + f:close() + return contents + end + end + -- try assets? + local am = me.a:getAssets() + local f = am:open(mod..'.lua') + return utils.readstring(f) +end + + +function pretty.create (me,mod) + mod = mod or 'example.pretty' + + local show_pretty = true + if mod:match '!$' then + show_pretty = false + mod = mod:sub(1,-2) + end + me.a:setTitle("Showing "..mod) + local source = readmodule(me,mod,package.path) + + local text + if show_pretty then + text = T.SpannableString(source) + + local s_comment = G.Color.RED + local s_string = G.Color.GREEN + local s_keyword = G.Color:parseColor '#AAAAFF' + + local slen = #source + + local function span (style,i1,i2) + text:setSpan(S.ForegroundColorSpan(style),i1-1,i2,0) + end + + local i1,i2,is,ie = 1,1 + while true do + i2 = source:find('%S',i2) -- next non-space + if not i2 then break end + is,ie = source:find ('^%-%-.-\n',i2) + if is then + span(s_comment,is,ie-1) + else + quote = source:match ('^(%[%[)',i2) or source:match([[^(["'])]],i2) + if quote then + if quote == '[[' then quote = '%]%]' end + _,ie = source:find(quote,i2+1) + span(s_string,i2,ie) + else + is,ie,word = source:find ('^(%a+)[^%d_]',i2) + if is and lua_keyword[word] then + span(s_keyword,is,ie-1) + else + _,ie = source:find('^%S*',i2) + ie = i2 + end + end + end + i2 = ie+1 + end + end + + local view = me:textView{size='12sp',scrollable=true} + if show_pretty then + view:setText(text,W.TextView_BufferType.SPANNABLE) + else + view:setText(source) + end + return view + +end + +return pretty diff --git a/app/src/main/assets/main.lua b/app/src/main/assets/main.lua new file mode 100644 index 0000000..77c0252 --- /dev/null +++ b/app/src/main/assets/main.lua @@ -0,0 +1,48 @@ +-- main.lua +-- This is an AndroLua activity which uses a traditional layout defined +-- in XML. In this case, the `create` function does not return a view, +-- but must set the content view explicitly. +-- Note that `ctrls` is a cunning lazy table for accessing named +-- controls in the layout! + +main = require 'android'.new() + +local SMM = bind 'android.text.method.ScrollingMovementMethod' +local InputType = bind 'android.text.InputType' + +function main.create(me) + local a = me.a + + -- yes, it's a global! + activity = a + + me:set_content_view 'main' + local ctrls = me:wrap_widgets() + local status = ctrls.statusText + + status:setText "listening on port 3333\n" + local smm = SMM:getInstance() + status:setMovementMethod(smm) + + ctrls.source:setText "require 'import'\nlocal L = luajava.package 'java.lang'\nprint(L.Math:sin(2.3))\n" + + me:on_click(ctrls.executeBtn,function() + local src = ctrls.source:getText():toString() + local ok,err = pcall(function() + local res = service:evalLua(src,"tmp") + status:append(res..'\n') + status:append("Finished Successfully\n") + end) + if not ok then -- make a loonnng toast.. + me:toast(err,true) + end + end) + + me:on_click(ctrls.exampleBtn,function() + me:luaActivity("example.launch") + end) + + return true +end + +return main diff --git a/app/src/main/java/com/github/ildar/AndroidLuaSDK/Lua.java b/app/src/main/java/com/github/ildar/AndroidLuaSDK/Lua.java index b6a1e7a..8005b61 100644 --- a/app/src/main/java/com/github/ildar/AndroidLuaSDK/Lua.java +++ b/app/src/main/java/com/github/ildar/AndroidLuaSDK/Lua.java @@ -159,7 +159,7 @@ public static void log(String msg) { public void setGlobal(String name, Object value) { L.pushJavaObject(value); - L.setGlobal(name); + L.setGlobal(name); } public LuaObject require(String mod) {