Skip to content

Instantly share code, notes, and snippets.

@ka2n
Created July 21, 2011 11:47
Show Gist options
  • Save ka2n/1097029 to your computer and use it in GitHub Desktop.
Save ka2n/1097029 to your computer and use it in GitHub Desktop.
builder.py/1.7.1 quick fix for Android SDK R12
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Android Simulator for building a project and launching
# the Android Emulator or on the device
#
import os, sys, subprocess, shutil, time, signal, string, platform, re, glob, hashlib, imp
import run, avd, prereq, zipfile, tempfile, fnmatch, codecs, traceback, simplejson
from os.path import splitext
from compiler import Compiler
from os.path import join, splitext, split, exists
from shutil import copyfile
from xml.dom.minidom import parseString
from tilogger import *
from datetime import datetime, timedelta
template_dir = os.path.abspath(os.path.dirname(sys._getframe(0).f_code.co_filename))
top_support_dir = os.path.dirname(template_dir)
sys.path.append(top_support_dir)
sys.path.append(os.path.join(top_support_dir, 'common'))
sys.path.append(os.path.join(top_support_dir, 'module'))
from tiapp import *
from android import Android
from androidsdk import AndroidSDK
from deltafy import Deltafy, Delta
from css import csscompiler
from module import ModuleDetector
import localecompiler
import fastdev
ignoreFiles = ['.gitignore', '.cvsignore', '.DS_Store'];
ignoreDirs = ['.git','.svn','_svn', 'CVS'];
android_avd_hw = {'hw.camera': 'yes', 'hw.gps':'yes'}
res_skips = ['style']
log = None
# Copied from frameworks/base/tools/aapt/Package.cpp
uncompressed_types = [
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv"
]
MIN_API_LEVEL = 7
# ZipFile.extractall introduced in Python 2.6, so this is workaround for earlier
# versions
def zip_extractall(zfile, target_dir):
file_infos = zfile.infolist()
for info in file_infos:
if info.file_size > 0:
file_path = os.path.join(target_dir, os.path.normpath(info.filename))
parent_path = os.path.dirname(file_path)
if not os.path.exists(parent_path):
os.makedirs(parent_path)
out_file = open(file_path, "wb")
out_file.write(zfile.read(info.filename))
out_file.close()
def dequote(s):
if s[0:1] == '"':
return s[1:-1]
return s
def pipe(args1,args2):
p1 = subprocess.Popen(args1, stdout=subprocess.PIPE)
p2 = subprocess.Popen(args2, stdin=p1.stdout, stdout=subprocess.PIPE)
return p2.communicate()[0]
def read_properties(propFile, separator=":= "):
propDict = dict()
for propLine in propFile:
propDef = propLine.strip()
if len(propDef) == 0:
continue
if propDef[0] in ( '!', '#' ):
continue
punctuation= [ propDef.find(c) for c in separator ] + [ len(propDef) ]
found= min( [ pos for pos in punctuation if pos != -1 ] )
name= propDef[:found].rstrip()
value= propDef[found:].lstrip(separator).rstrip()
propDict[name]= value
propFile.close()
return propDict
def info(msg):
log.info(msg)
def debug(msg):
log.debug(msg)
def warn(msg):
log.warn(msg)
def trace(msg):
log.trace(msg)
def error(msg):
log.error(msg)
def copy_all(source_folder, dest_folder, ignore_dirs=[], ignore_files=[], ignore_exts=[], one_time_msg=""):
msg_shown = False
for root, dirs, files in os.walk(source_folder):
for d in dirs:
if d in ignore_dirs:
dirs.remove(d)
for f in files:
if f in ignore_files:
continue
ext = os.path.splitext(f)[1]
if ext in ignore_exts:
continue
if one_time_msg and not msg_shown:
info(one_time_msg)
msg_shown = True
from_ = os.path.join(root, f)
to_ = from_.replace(source_folder, dest_folder, 1)
to_directory = os.path.split(to_)[0]
if not os.path.exists(to_directory):
os.makedirs(to_directory)
shutil.copyfile(from_, to_)
def remove_orphaned_files(source_folder, target_folder):
is_res = source_folder.endswith('Resources') or source_folder.endswith('Resources' + os.sep)
for root, dirs, files in os.walk(target_folder):
for f in files:
full = os.path.join(root, f)
rel = full.replace(target_folder, '')
if rel[0] == os.sep:
rel = rel[1:]
is_orphan = False
if not os.path.exists(os.path.join(source_folder, rel)):
is_orphan = True
# But it could be under android/... too (platform-specific)
if is_orphan and is_res:
if os.path.exists(os.path.join(source_folder, 'android', rel)):
is_orphan = False
if is_orphan:
os.remove(full)
def is_resource_drawable(path):
if re.search("android/images/(high|medium|low|res-[^/]+)/", path.replace("\\", "/")):
return True
else:
return False
def resource_drawable_folder(path):
if not is_resource_drawable(path):
return None
else:
pattern = r'/android/images/(high|medium|low|res-[^/]+)/'
match = re.search(pattern, path.replace("\\", "/"))
if not match.groups():
return None
folder = match.groups()[0]
if re.match('high|medium|low', folder):
return 'drawable-%sdpi' % folder[0]
else:
return 'drawable-%s' % folder.replace('res-', '')
class Builder(object):
def __init__(self, name, sdk, project_dir, support_dir, app_id):
self.top_dir = project_dir
self.project_tiappxml = os.path.join(self.top_dir,'tiapp.xml')
self.project_dir = os.path.join(project_dir,'build','android')
self.res_dir = os.path.join(self.project_dir,'res')
self.platform_dir = os.path.join(project_dir, 'platform', 'android')
self.project_src_dir = os.path.join(self.project_dir, 'src')
self.project_gen_dir = os.path.join(self.project_dir, 'gen')
self.name = name
self.app_id = app_id
self.support_dir = support_dir
self.compiled_files = []
self.force_rebuild = False
self.debugger_host = None
self.debugger_port = -1
self.fastdev_port = -1
self.fastdev = False
temp_tiapp = TiAppXML(self.project_tiappxml)
if temp_tiapp and temp_tiapp.android and 'tool-api-level' in temp_tiapp.android:
self.tool_api_level = int(temp_tiapp.android['tool-api-level'])
else:
self.tool_api_level = MIN_API_LEVEL
self.sdk = AndroidSDK(sdk, self.tool_api_level)
self.tiappxml = temp_tiapp
self.set_java_commands()
# start in 1.4, you no longer need the build/android directory
# if missing, we'll create it on the fly
if not os.path.exists(self.project_dir) or not os.path.exists(os.path.join(self.project_dir,'AndroidManifest.xml')):
android_creator = Android(name, app_id, self.sdk, None, self.java)
parent_dir = os.path.dirname(self.top_dir)
if os.path.exists(self.top_dir):
android_creator.create(parent_dir, project_dir=self.top_dir, build_time=True)
else:
android_creator.create(parent_dir)
self.force_rebuild = True
sys.stdout.flush()
# we place some files in the users home
if platform.system() == "Windows":
self.home_dir = os.path.join(os.environ['USERPROFILE'], '.titanium')
self.android_home_dir = os.path.join(os.environ['USERPROFILE'], '.android')
else:
self.home_dir = os.path.join(os.path.expanduser('~'), '.titanium')
self.android_home_dir = os.path.join(os.path.expanduser('~'), '.android')
if not os.path.exists(self.home_dir):
os.makedirs(self.home_dir)
self.sdcard = os.path.join(self.home_dir,'android2.sdcard')
self.classname = Android.strip_classname(self.name)
def set_java_commands(self):
self.jarsigner = "jarsigner"
self.javac = "javac"
self.java = "java"
if platform.system() == "Windows":
if os.environ.has_key("JAVA_HOME"):
home_jarsigner = os.path.join(os.environ["JAVA_HOME"], "bin", "jarsigner.exe")
home_javac = os.path.join(os.environ["JAVA_HOME"], "bin", "javac.exe")
home_java = os.path.join(os.environ["JAVA_HOME"], "bin", "java.exe")
found = True
# TODO Document this path and test properly under windows
if os.path.exists(home_jarsigner):
self.jarsigner = home_jarsigner
else:
# Expected but not found
found = False
error("Required jarsigner not found")
if os.path.exists(home_javac):
self.javac = home_javac
else:
error("Required javac not found")
found = False
if os.path.exists(home_java):
self.java = home_java
else:
error("Required java not found")
found = False
if found == False:
error("One or more required files not found - please check your JAVA_HOME environment variable")
sys.exit(1)
else:
found = False
for path in os.environ['PATH'].split(os.pathsep):
if os.path.exists(os.path.join(path, 'jarsigner.exe')) and os.path.exists(os.path.join(path, 'javac.exe')):
self.jarsigner = os.path.join(path, 'jarsigner.exe')
self.javac = os.path.join(path, 'javac.exe')
self.java = os.path.join(path, 'java.exe')
found = True
break
if not found:
error("Error locating JDK: set $JAVA_HOME or put javac and jarsigner on your $PATH")
sys.exit(1)
def wait_for_home(self, type):
max_wait = 20
attempts = 0
while True:
processes = self.sdk.list_processes(['-%s' % type])
found_home = False
for process in processes:
if process["name"] == "android.process.acore":
found_home = True
break
if found_home:
break
attempts += 1
if attempts == max_wait:
error("Timed out waiting for android.process.acore")
return False
time.sleep(1)
return True
def wait_for_device(self, type):
debug("Waiting for device to be ready ...")
t = time.time()
max_wait = 30
max_zero = 6
attempts = 0
zero_attempts = 0
timed_out = True
no_devices = False
while True:
devices = self.sdk.list_devices()
trace("adb devices returned %s devices/emulators" % len(devices))
if len(devices) > 0:
found = False
for device in devices:
if type == "e" and device.is_emulator() and not device.is_offline(): found = True
elif type == "d" and device.is_device(): found = True
if found:
timed_out = False
break
else: zero_attempts += 1
try: time.sleep(5) # for some reason KeyboardInterrupts get caught here from time to time
except KeyboardInterrupt: pass
attempts += 1
if attempts == max_wait:
break
elif zero_attempts == max_zero:
no_devices = True
break
if timed_out:
if type == "e":
device = "emulator"
extra_message = "you may need to close the emulator and try again"
else:
device = "device"
extra_message = "you may try reconnecting the USB cable"
error("Timed out waiting for %s to be ready, %s" % (device, extra_message))
if no_devices:
sys.exit(1)
return False
debug("Device connected... (waited %d seconds)" % (attempts*5))
duration = time.time() - t
debug("waited %f seconds on emulator to get ready" % duration)
if duration > 1.0:
info("Waiting for the Android Emulator to become available")
return self.wait_for_home(type)
#time.sleep(20) # give it a little more time to get installed
return True
def create_avd(self,avd_id,avd_skin):
name = "titanium_%s_%s" % (avd_id,avd_skin)
name = name.replace(' ', '_')
if not os.path.exists(self.home_dir):
os.makedirs(self.home_dir)
if not os.path.exists(self.sdcard):
info("Creating shared 64M SD card for use in Android emulator(s)")
run.run([self.sdk.get_mksdcard(), '64M', self.sdcard])
avd_path = os.path.join(self.android_home_dir, 'avd')
my_avd = os.path.join(avd_path,"%s.avd" % name)
if not os.path.exists(my_avd):
info("Creating new Android Virtual Device (%s %s)" % (avd_id,avd_skin))
inputgen = os.path.join(template_dir,'input.py')
pipe([sys.executable, inputgen], [self.sdk.get_android(), '--verbose', 'create', 'avd', '--name', name, '--target', avd_id, '-s', avd_skin, '--force', '--sdcard', self.sdcard])
inifile = os.path.join(my_avd,'config.ini')
inifilec = open(inifile,'r').read()
inifiledata = open(inifile,'w')
inifiledata.write(inifilec)
# TODO - Document options
for hw_option in android_avd_hw.keys():
inifiledata.write("%s=%s\n" % (hw_option, android_avd_hw[hw_option]))
inifiledata.close()
return name
def run_emulator(self,avd_id,avd_skin):
info("Launching Android emulator...one moment")
debug("From: " + self.sdk.get_emulator())
debug("SDCard: " + self.sdcard)
debug("AVD ID: " + avd_id)
debug("AVD Skin: " + avd_skin)
debug("SDK: " + sdk_dir)
# make sure adb is running on windows, else XP can lockup the python
# process when adb runs first time
if platform.system() == "Windows":
run.run([self.sdk.get_adb(), "start-server"], True, ignore_output=True)
devices = self.sdk.list_devices()
for device in devices:
if device.is_emulator() and device.get_port() == 5560:
info("Emulator is running.")
sys.exit(0)
# this will create an AVD on demand or re-use existing one if already created
avd_name = self.create_avd(avd_id,avd_skin)
# start the emulator
emulator_cmd = [
self.sdk.get_emulator(),
'-avd',
avd_name,
'-port',
'5560',
'-sdcard',
self.sdcard,
'-logcat',
"\"*:d *\"",
'-no-boot-anim',
'-partition-size',
'128' # in between nexusone and droid
]
debug(' '.join(emulator_cmd))
p = subprocess.Popen(emulator_cmd)
def handler(signum, frame):
debug("signal caught: %d" % signum)
if not p == None:
debug("calling emulator kill on %d" % p.pid)
if platform.system() == "Windows":
os.system("taskkill /F /T /PID %i" % p.pid)
else:
os.kill(p.pid, signal.SIGTERM)
if platform.system() != "Windows":
signal.signal(signal.SIGHUP, handler)
signal.signal(signal.SIGQUIT, handler)
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGABRT, handler)
signal.signal(signal.SIGTERM, handler)
# give it some time to exit prematurely
time.sleep(1)
rc = p.poll()
if rc != None:
handler(3,None)
sys.exit(rc)
# wait for the emulator to finish
try:
rc = p.wait()
except OSError:
handler(3,None)
info("Android Emulator has exited")
sys.exit(rc)
def check_file_exists(self, path):
output = self.run_adb('shell', 'ls', path)
if output != None:
if output.find("No such file or directory") == -1 \
and output.find("error: device offline") == -1:
return True
return False
def is_app_installed(self):
return self.check_file_exists('/data/app/%s*.apk' % self.app_id)
def are_resources_installed(self):
return self.check_file_exists(self.sdcard_resources+'/app.js')
def include_path(self, path, isfile):
if not isfile and os.path.basename(path) in ignoreDirs: return False
elif isfile and os.path.basename(path) in ignoreFiles: return False
return True
def warn_dupe_drawable_folders(self):
tocheck = ('high', 'medium', 'low')
image_parent = os.path.join(self.top_dir, 'Resources', 'android', 'images')
for check in tocheck:
if os.path.exists(os.path.join(image_parent, check)) and os.path.exists(os.path.join(image_parent, 'res-%sdpi' % check[0])):
warn('You have both an android/images/%s folder and an android/images/res-%sdpi folder. Files from both of these folders will end up in res/drawable-%sdpi. If two files are named the same, there is no guarantee which one will be copied last and therefore be the one the application uses. You should use just one of these folders to avoid conflicts.' % (check, check[0], check[0]))
def copy_module_platform_folders(self):
module_dir = os.path.join(self.top_dir, 'modules', 'android')
if not os.path.exists(module_dir):
return
for module in self.modules:
platform_folder = os.path.join(module.path, 'platform', 'android')
if os.path.exists(platform_folder):
copy_all(platform_folder, self.project_dir, one_time_msg="Copying platform-specific files for '%s' module" % module.manifest.name)
def copy_project_platform_folder(self, ignore_dirs=[], ignore_files=[]):
if not os.path.exists(self.platform_dir):
return
copy_all(self.platform_dir, self.project_dir, ignore_dirs, ignore_files, one_time_msg="Copying platform-specific files ...")
def copy_resource_drawables(self):
debug('Processing Android resource drawables')
def make_resource_drawable_filename(orig):
normalized = orig.replace("\\", "/")
matches = re.search("/android/images/(high|medium|low|res-[^/]+)/(?P<chopped>.*$)", normalized)
if matches and matches.groupdict() and 'chopped' in matches.groupdict():
chopped = matches.groupdict()['chopped'].lower()
for_hash = chopped
if for_hash.endswith('.9.png'):
for_hash = for_hash[:-6] + '.png'
extension = ""
without_extension = chopped
if re.search("\\..*$", chopped):
if chopped.endswith('.9.png'):
extension = '9.png'
without_extension = chopped[:-6]
else:
extension = chopped.split(".")[-1]
without_extension = chopped[:-(len(extension)+1)]
cleaned_without_extension = re.sub(r'[^a-z0-9_]', '_', without_extension)
cleaned_extension = re.sub(r'[^a-z0-9\._]', '_', extension)
result = cleaned_without_extension[:80] + "_" + hashlib.md5(for_hash).hexdigest()[:10]
if extension:
result += "." + extension
return result
else:
trace("Regexp for resource drawable file %s failed" % orig)
return None
def delete_resource_drawable(orig):
folder = resource_drawable_folder(orig)
res_file = os.path.join(self.res_dir, folder, make_resource_drawable_filename(orig))
if os.path.exists(res_file):
try:
trace("DELETING FILE: %s" % res_file)
os.remove(res_file)
except:
warn('Unable to delete %s: %s. Execution will continue.' % (res_file, sys.exc_info()[0]))
def copy_resource_drawable(orig):
partial_folder = resource_drawable_folder(orig)
if not partial_folder:
trace("Could not copy %s; resource folder not determined" % orig)
return
dest_folder = os.path.join(self.res_dir, partial_folder)
dest_filename = make_resource_drawable_filename(orig)
if dest_filename is None:
return
dest = os.path.join(dest_folder, dest_filename)
if not os.path.exists(dest_folder):
os.makedirs(dest_folder)
trace("COPYING FILE: %s => %s" % (orig, dest))
shutil.copy(orig, dest)
fileset = []
if self.force_rebuild or self.deploy_type == 'production' or \
(self.js_changed and not self.fastdev):
for root, dirs, files in os.walk(os.path.join(self.top_dir, "Resources")):
for f in files:
path = os.path.join(root, f)
if is_resource_drawable(path) and f != 'default.png':
fileset.append(path)
else:
if self.project_deltas:
for delta in self.project_deltas:
path = delta.get_path()
if is_resource_drawable(path):
if delta.get_status() == Delta.DELETED:
delete_resource_drawable(path)
else:
fileset.append(path)
if len(fileset) == 0:
return False
for f in fileset:
copy_resource_drawable(f)
return True
def copy_project_resources(self):
info("Copying project resources..")
resources_dir = os.path.join(self.top_dir, 'Resources')
android_resources_dir = os.path.join(resources_dir, 'android')
self.project_deltafy = Deltafy(resources_dir, include_callback=self.include_path)
self.project_deltas = self.project_deltafy.scan()
self.js_changed = False
tiapp_delta = self.project_deltafy.scan_single_file(self.project_tiappxml)
self.tiapp_changed = tiapp_delta is not None
if self.tiapp_changed or self.force_rebuild:
info("Detected tiapp.xml change, forcing full re-build...")
# force a clean scan/copy when the tiapp.xml has changed
self.project_deltafy.clear_state()
self.project_deltas = self.project_deltafy.scan()
# rescan tiapp.xml so it doesn't show up as created next time around
self.project_deltafy.scan_single_file(self.project_tiappxml)
def strip_slash(s):
if s[0:1]=='/' or s[0:1]=='\\': return s[1:]
return s
def make_relative(path, relative_to, prefix=None):
relative_path = strip_slash(path[len(relative_to):])
if prefix is not None:
return os.path.join(prefix, relative_path)
return relative_path
for delta in self.project_deltas:
path = delta.get_path()
if re.search("android/images/(high|medium|low|res-[^/]+)/", path.replace("\\", "/")):
continue # density images are handled later
if delta.get_status() == Delta.DELETED and path.startswith(android_resources_dir):
shared_path = path.replace(android_resources_dir, resources_dir, 1)
if os.path.exists(shared_path):
dest = make_relative(shared_path, resources_dir, self.assets_resources_dir)
trace("COPYING FILE: %s => %s (platform-specific file was removed)" % (shared_path, dest))
shutil.copy(shared_path, dest)
if delta.get_status() != Delta.DELETED:
if path.startswith(android_resources_dir):
dest = make_relative(path, android_resources_dir, self.assets_resources_dir)
else:
# don't copy it if there is an android-specific file
if os.path.exists(path.replace(resources_dir, android_resources_dir, 1)):
continue
dest = make_relative(path, resources_dir, self.assets_resources_dir)
# check to see if this is a compiled file and if so, don't copy
if dest in self.compiled_files: continue
if path.startswith(os.path.join(resources_dir, "iphone")) or path.startswith(os.path.join(resources_dir, "blackberry")):
continue
parent = os.path.dirname(dest)
if not os.path.exists(parent):
os.makedirs(parent)
trace("COPYING %s FILE: %s => %s" % (delta.get_status_str(), path, dest))
shutil.copy(path, dest)
if (path.startswith(resources_dir) or path.startswith(android_resources_dir)) and path.endswith(".js"):
self.js_changed = True
# copy to the sdcard in development mode
if self.sdcard_copy and self.app_installed and (self.deploy_type == 'development' or self.deploy_type == 'test'):
if path.startswith(android_resources_dir):
relative_path = make_relative(delta.get_path(), android_resources_dir)
else:
relative_path = make_relative(delta.get_path(), resources_dir)
relative_path = relative_path.replace("\\", "/")
self.run_adb('push', delta.get_path(), "%s/%s" % (self.sdcard_resources, relative_path))
def generate_android_manifest(self,compiler):
self.generate_localizations()
# NOTE: these are built-in permissions we need -- we probably need to refine when these are needed too
permissions_required = ['INTERNET','ACCESS_WIFI_STATE','ACCESS_NETWORK_STATE', 'WRITE_EXTERNAL_STORAGE']
GEO_PERMISSION = [ 'ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION', 'ACCESS_MOCK_LOCATION']
CONTACTS_PERMISSION = ['READ_CONTACTS']
VIBRATE_PERMISSION = ['VIBRATE']
CAMERA_PERMISSION = ['CAMERA']
WALLPAPER_PERMISSION = ['SET_WALLPAPER']
# this is our module method to permission(s) trigger - for each method on the left, require the permission(s) on the right
permission_mapping = {
# GEO
'Geolocation.watchPosition' : GEO_PERMISSION,
'Geolocation.getCurrentPosition' : GEO_PERMISSION,
'Geolocation.watchHeading' : GEO_PERMISSION,
'Geolocation.getCurrentHeading' : GEO_PERMISSION,
# MEDIA
'Media.vibrate' : VIBRATE_PERMISSION,
'Media.createVideoPlayer' : CAMERA_PERMISSION,
'Media.showCamera' : CAMERA_PERMISSION,
# CONTACTS
'Contacts.createContact' : CONTACTS_PERMISSION,
'Contacts.saveContact' : CONTACTS_PERMISSION,
'Contacts.removeContact' : CONTACTS_PERMISSION,
'Contacts.addContact' : CONTACTS_PERMISSION,
'Contacts.getAllContacts' : CONTACTS_PERMISSION,
'Contacts.showContactPicker' : CONTACTS_PERMISSION,
'Contacts.showContacts' : CONTACTS_PERMISSION,
'Contacts.getPersonByID' : CONTACTS_PERMISSION,
'Contacts.getPeopleWithName' : CONTACTS_PERMISSION,
'Contacts.getAllPeople' : CONTACTS_PERMISSION,
'Contacts.getAllGroups' : CONTACTS_PERMISSION,
'Contacts.getGroupByID' : CONTACTS_PERMISSION,
# WALLPAPER
'Media.Android.setSystemWallpaper' : WALLPAPER_PERMISSION,
}
VIDEO_ACTIVITY = """<activity
android:name="ti.modules.titanium.media.TiVideoActivity"
android:configChanges="keyboardHidden|orientation"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:launchMode="singleTask"
/>"""
MAP_ACTIVITY = """<activity
android:name="ti.modules.titanium.map.TiMapActivity"
android:configChanges="keyboardHidden|orientation"
android:launchMode="singleTask"
/>
<uses-library android:name="com.google.android.maps" />"""
FACEBOOK_ACTIVITY = """<activity
android:name="ti.modules.titanium.facebook.FBActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
/>"""
CAMERA_ACTIVITY = """<activity
android:name="ti.modules.titanium.media.TiCameraActivity"
android:configChanges="keyboardHidden|orientation"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
/>"""
activity_mapping = {
# MEDIA
'Media.createVideoPlayer' : VIDEO_ACTIVITY,
'Media.showCamera' : CAMERA_ACTIVITY,
# MAPS
'Map.createView' : MAP_ACTIVITY,
# FACEBOOK
'Facebook.setup' : FACEBOOK_ACTIVITY,
'Facebook.login' : FACEBOOK_ACTIVITY,
'Facebook.createLoginButton' : FACEBOOK_ACTIVITY,
}
# this is a map of our APIs to ones that require Google APIs to be available on the device
google_apis = {
"Map.createView" : True
}
activities = []
# figure out which permissions we need based on the used module methods
for mn in compiler.module_methods:
try:
perms = permission_mapping[mn]
if perms:
for perm in perms:
try:
permissions_required.index(perm)
except:
permissions_required.append(perm)
except:
pass
try:
mappings = activity_mapping[mn]
try:
if google_apis[mn] and not self.google_apis_supported:
warn("Google APIs detected but a device has been selected that doesn't support them. The API call to Titanium.%s will fail using '%s'" % (mn,my_avd['name']))
continue
except:
pass
try:
activities.index(mappings)
except:
activities.append(mappings)
except:
pass
# Javascript-based activities defined in tiapp.xml
if self.tiapp and self.tiapp.android and 'activities' in self.tiapp.android:
tiapp_activities = self.tiapp.android['activities']
for key in tiapp_activities:
activity = tiapp_activities[key]
if not 'url' in activity:
continue
activity_name = self.app_id + '.' + activity['classname']
activity_str = '<activity \n\t\t\tandroid:name="%s"' % activity_name
for subkey in activity:
if subkey not in ('nodes', 'name', 'url', 'options', 'classname', 'android:name'):
activity_str += '\n\t\t\t%s="%s"' % (subkey, activity[subkey])
if 'android:config' not in activity:
activity_str += '\n\t\t\tandroid:configChanges="keyboardHidden|orientation"'
if 'nodes' in activity:
activity_str += '>'
for node in activity['nodes']:
activity_str += '\n\t\t\t\t' + node.toxml()
activities.append(activity_str + '\n\t\t</activity>\n')
else:
activities.append(activity_str + '\n\t\t/>\n')
activities = set(activities)
services = []
# Javascript-based services defined in tiapp.xml
if self.tiapp and self.tiapp.android and 'services' in self.tiapp.android:
tiapp_services = self.tiapp.android['services']
for key in tiapp_services:
service = tiapp_services[key]
if not 'url' in service:
continue
service_name = self.app_id + '.' + service['classname']
service_str = '<service \n\t\t\tandroid:name="%s"' % service_name
for subkey in service:
if subkey not in ('nodes', 'service_type', 'type', 'name', 'url', 'options', 'classname', 'android:name'):
service_str += '\n\t\t\t%s="%s"' % (subkey, service[subkey])
if 'nodes' in service:
service_str += '>'
for node in service['nodes']:
service_str += '\n\t\t\t\t' + node.toxml()
services.append(service_str + '\n\t\t</service>\n')
else:
services.append(service_str + '\n\t\t/>\n')
self.use_maps = False
self.res_changed = False
iconname = self.tiapp.properties['icon']
iconpath = os.path.join(self.assets_resources_dir, iconname)
iconext = os.path.splitext(iconpath)[1]
res_drawable_dest = os.path.join(self.project_dir, 'res','drawable')
if not os.path.exists(res_drawable_dest):
os.makedirs(res_drawable_dest)
default_icon = os.path.join(self.support_resources_dir, 'default.png')
dest_icon = os.path.join(res_drawable_dest, 'appicon%s' % iconext)
if Deltafy.needs_update(iconpath, dest_icon):
self.res_changed = True
debug("copying app icon: %s" % iconpath)
shutil.copy(iconpath, dest_icon)
elif Deltafy.needs_update(default_icon, dest_icon):
self.res_changed = True
debug("copying default app icon")
shutil.copy(default_icon, dest_icon)
# make our Titanium theme for our icon
res_values_dir = os.path.join(self.project_dir, 'res','values')
if not os.path.exists(res_values_dir):
os.makedirs(res_values_dir)
theme_xml = os.path.join(res_values_dir,'theme.xml')
if not os.path.exists(theme_xml):
self.res_changed = True
debug('generating theme.xml')
theme_file = open(theme_xml, 'w')
theme_flags = "Theme"
# We need to treat the default values for fulscreen and
# navbar-hidden the same as android.py does -- false for both.
theme_fullscreen = False
theme_navbarhidden = False
if (self.tiapp.properties.get("fullscreen") == "true" or
self.tiapp.properties.get("statusbar-hidden") == "true"):
theme_fullscreen = True
elif self.tiapp.properties.get("navbar-hidden") == "true":
theme_navbarhidden = True
if theme_fullscreen:
theme_flags += ".NoTitleBar.Fullscreen"
elif theme_navbarhidden:
theme_flags += ".NoTitleBar"
# Wait, one exception. If you want the notification area (very
# top of screen) hidden, but want the title bar in the app,
# there's no theme for that. So we have to use the default theme (no flags)
# and when the application code starts running, the adjustments are then made.
# Only do this when the properties are explicitly set, so as to avoid changing
# old default behavior.
if theme_flags.endswith('.Fullscreen') and \
self.tiapp.properties.get("navbar-hidden") == 'false' and \
('fullscreen' in self.tiapp.explicit_properties or \
'statusbar-hidden' in self.tiapp.explicit_properties) and \
'navbar-hidden' in self.tiapp.explicit_properties:
theme_flags = 'Theme'
TITANIUM_THEME="""<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Titanium" parent="android:%s">
<item name="android:windowBackground">@drawable/background</item>
</style>
</resources>
""" % theme_flags
theme_file.write(TITANIUM_THEME)
theme_file.close()
# create our background image which acts as splash screen during load
resources_dir = os.path.join(self.top_dir, 'Resources')
android_images_dir = os.path.join(resources_dir, 'android', 'images')
# look for density-specific default.png's first
if os.path.exists(android_images_dir):
pattern = r'/android/images/(high|medium|low|res-[^/]+)/default.png'
for root, dirs, files in os.walk(android_images_dir):
for f in files:
path = os.path.join(root, f)
if re.search(pattern, path):
res_folder = resource_drawable_folder(path)
debug('found %s splash screen at %s' % (res_folder, path))
dest_path = os.path.join(self.res_dir, res_folder)
dest_file = os.path.join(dest_path, 'background.png')
if not os.path.exists(dest_path):
os.makedirs(dest_path)
if Deltafy.needs_update(path, dest_file):
self.res_changed = True
debug('copying %s splash screen to %s' % (path, dest_file))
shutil.copy(path, dest_file)
default_png = os.path.join(self.assets_resources_dir, 'default.png')
support_default_png = os.path.join(self.support_resources_dir, 'default.png')
background_png = os.path.join(self.project_dir, 'res','drawable','background.png')
if os.path.exists(default_png) and Deltafy.needs_update(default_png, background_png):
self.res_changed = True
debug("found splash screen at %s" % os.path.abspath(default_png))
shutil.copy(default_png, background_png)
elif Deltafy.needs_update(support_default_png, background_png):
self.res_changed = True
debug("copying default splash screen")
shutil.copy(support_default_png, background_png)
android_manifest = os.path.join(self.project_dir, 'AndroidManifest.xml')
android_manifest_to_read = android_manifest
# NOTE: allow the user to use their own custom AndroidManifest if they put a file named
# AndroidManifest.xml in platform/android, in which case all bets are off
is_custom = False
# Catch people who may have it in project root (un-released 1.4.x android_native_refactor branch users)
if os.path.exists(os.path.join(self.top_dir, 'AndroidManifest.xml')):
warn('AndroidManifest.xml file in the project root is ignored. Move it to platform/android if you want it to be your custom manifest.')
android_custom_manifest = os.path.join(self.project_dir, 'AndroidManifest.custom.xml')
if not os.path.exists(android_custom_manifest):
android_custom_manifest = os.path.join(self.platform_dir, 'AndroidManifest.xml')
else:
warn('Use of AndroidManifest.custom.xml is deprecated. Please put your custom manifest as "AndroidManifest.xml" in the "platform/android" directory if you do not need to compile for versions < 1.5')
if os.path.exists(android_custom_manifest):
android_manifest_to_read = android_custom_manifest
is_custom = True
info("Detected custom ApplicationManifest.xml -- no Titanium version migration supported")
default_manifest_contents = self.android.render_android_manifest()
custom_manifest_contents = None
if is_custom:
custom_manifest_contents = open(android_manifest_to_read,'r').read()
manifest_xml = ''
def get_manifest_xml(tiapp):
xml = ''
if 'manifest' in tiapp.android_manifest:
for manifest_el in tiapp.android_manifest['manifest']:
# since we already track permissions in another way, go ahead and us e that
if manifest_el.nodeName == 'uses-permission' and manifest_el.hasAttribute('android:name'):
if manifest_el.getAttribute('android:name').split('.')[-1] not in permissions_required:
permissions_required.append(manifest_el.getAttribute('android:name'))
elif manifest_el.nodeName not in ('supports-screens', 'uses-sdk'):
xml += manifest_el.toprettyxml()
return xml
application_xml = ''
def get_application_xml(tiapp):
xml = ''
if 'application' in tiapp.android_manifest:
for app_el in tiapp.android_manifest['application']:
xml += app_el.toxml()
return xml
# add manifest / application entries from tiapp.xml
manifest_xml += get_manifest_xml(self.tiapp)
application_xml += get_application_xml(self.tiapp)
# add manifest / application entries from modules
for module in self.modules:
if module.xml == None: continue
manifest_xml += get_manifest_xml(module.xml)
application_xml += get_application_xml(module.xml)
# build the permissions XML based on the permissions detected
permissions_required = set(permissions_required)
permissions_required_xml = ""
for p in permissions_required:
if '.' not in p:
permissions_required_xml+="<uses-permission android:name=\"android.permission.%s\"/>\n\t" % p
else:
permissions_required_xml+="<uses-permission android:name=\"%s\"/>\n\t" % p
def fill_manifest(manifest_source):
ti_activities = '<!-- TI_ACTIVITIES -->'
ti_permissions = '<!-- TI_PERMISSIONS -->'
ti_manifest = '<!-- TI_MANIFEST -->'
ti_application = '<!-- TI_APPLICATION -->'
ti_services = '<!-- TI_SERVICES -->'
manifest_source = manifest_source.replace(ti_activities,"\n\n\t\t".join(activities))
manifest_source = manifest_source.replace(ti_services,"\n\n\t\t".join(services))
manifest_source = manifest_source.replace(ti_permissions,permissions_required_xml)
if len(manifest_xml) > 0:
manifest_source = manifest_source.replace(ti_manifest, manifest_xml)
if len(application_xml) > 0:
manifest_source = manifest_source.replace(ti_application, application_xml)
return manifest_source
default_manifest_contents = fill_manifest(default_manifest_contents)
# if a custom uses-sdk or supports-screens has been specified via tiapp.xml
# <android><manifest>..., we need to replace the ones in the generated
# default manifest
supports_screens_node = None
uses_sdk_node = None
if 'manifest' in self.tiapp.android_manifest:
for node in self.tiapp.android_manifest['manifest']:
if node.nodeName == 'uses-sdk':
uses_sdk_node = node
elif node.nodeName == 'supports-screens':
supports_screens_node = node
if supports_screens_node or uses_sdk_node or ('manifest-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['manifest-attributes'].length) or ('application-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['application-attributes'].length):
dom = parseString(default_manifest_contents)
def replace_node(olddom, newnode):
nodes = olddom.getElementsByTagName(newnode.nodeName)
retval = False
if nodes:
olddom.documentElement.replaceChild(newnode, nodes[0])
retval = True
return retval
if supports_screens_node:
if not replace_node(dom, supports_screens_node):
dom.documentElement.insertBefore(supports_screens_node, dom.documentElement.firstChild.nextSibling)
if uses_sdk_node:
replace_node(dom, uses_sdk_node)
def set_attrs(element, new_attr_set):
for k in new_attr_set.keys():
if element.hasAttribute(k):
element.removeAttribute(k)
element.setAttribute(k, new_attr_set.get(k).value)
if 'manifest-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['manifest-attributes'].length:
set_attrs(dom.documentElement, self.tiapp.android_manifest['manifest-attributes'])
if 'application-attributes' in self.tiapp.android_manifest and self.tiapp.android_manifest['application-attributes'].length:
set_attrs(dom.getElementsByTagName('application')[0], self.tiapp.android_manifest['application-attributes'])
default_manifest_contents = dom.toxml()
if application_xml:
# If the tiapp.xml <manifest><application> section was not empty, it could be
# that user put in <activity> entries that duplicate our own,
# such as if they want a custom theme on TiActivity. So we should delete any dupes.
dom = parseString(default_manifest_contents)
manifest_activities = dom.getElementsByTagName('activity')
activity_names = []
nodes_to_delete = []
for manifest_activity in manifest_activities:
if manifest_activity.hasAttribute('android:name'):
activity_name = manifest_activity.getAttribute('android:name')
if activity_name in activity_names:
nodes_to_delete.append(manifest_activity)
else:
activity_names.append(activity_name)
if nodes_to_delete:
for node_to_delete in nodes_to_delete:
node_to_delete.parentNode.removeChild(node_to_delete)
default_manifest_contents = dom.toxml()
if custom_manifest_contents:
custom_manifest_contents = fill_manifest(custom_manifest_contents)
new_manifest_contents = None
android_manifest_gen = android_manifest + '.gen'
if custom_manifest_contents:
new_manifest_contents = custom_manifest_contents
# Write the would-be default as well so user can see
# some of the auto-gen'd insides of it if they need/want.
amf = open(android_manifest + '.gen', 'w')
amf.write(default_manifest_contents)
amf.close()
else:
new_manifest_contents = default_manifest_contents
if os.path.exists(android_manifest_gen):
os.remove(android_manifest_gen)
manifest_changed = False
old_contents = None
if os.path.exists(android_manifest):
old_contents = open(android_manifest, 'r').read()
if new_manifest_contents != old_contents:
trace("Writing out AndroidManifest.xml")
amf = open(android_manifest,'w')
amf.write(new_manifest_contents)
amf.close()
manifest_changed = True
if self.res_changed or manifest_changed:
res_dir = os.path.join(self.project_dir, 'res')
output = run.run([self.aapt, 'package', '-m', '-J', self.project_gen_dir, '-M', android_manifest, '-S', res_dir, '-I', self.android_jar],
warning_regex=r'skipping')
r_file = os.path.join(self.project_gen_dir, self.app_id.replace('.', os.sep), 'R.java')
if not os.path.exists(r_file) or (self.res_changed and output == None):
error("Error generating R.java from manifest")
sys.exit(1)
return manifest_changed
def generate_stylesheet(self):
update_stylesheet = False
resources_dir = os.path.join(self.top_dir, 'Resources')
project_gen_pkg_dir = os.path.join(self.project_gen_dir, self.app_id.replace('.', os.sep))
app_stylesheet = os.path.join(project_gen_pkg_dir, 'ApplicationStylesheet.java')
if not os.path.exists(app_stylesheet):
update_stylesheet = True
else:
for root, dirs, files in os.walk(resources_dir):
for file in files:
if file.endswith(".jss"):
absolute_path = os.path.join(root, file)
if Deltafy.needs_update(absolute_path, app_stylesheet):
update_stylesheet = True
break
if not update_stylesheet:
return
cssc = csscompiler.CSSCompiler(resources_dir, 'android', self.app_id)
if not os.path.exists(project_gen_pkg_dir):
os.makedirs(project_gen_pkg_dir)
debug("app stylesheet => %s" % app_stylesheet)
asf = codecs.open(app_stylesheet, 'w', 'utf-8')
asf.write(cssc.code)
asf.close()
def generate_localizations(self):
# compile localization files
localecompiler.LocaleCompiler(self.name,self.top_dir,'android',sys.argv[1]).compile()
# fix un-escaped single-quotes and full-quotes
offending_pattern = '[^\\\\][\'"]'
for root, dirs, files in os.walk(self.res_dir):
for f in files:
if not f.endswith('.xml'):
continue
full_path = os.path.join(root, f)
f = codecs.open(full_path, 'r', 'utf-8')
contents = f.read()
f.close()
if not re.search(r"<string ", contents):
continue
doc = parseString(contents.encode("utf-8"))
string_nodes = doc.getElementsByTagName('string')
if len(string_nodes) == 0:
continue
made_change = False
for string_node in string_nodes:
if not string_node.hasChildNodes():
continue
string_child = string_node.firstChild
if string_child.nodeType == string_child.CDATA_SECTION_NODE or string_child.nodeType == string_child.TEXT_NODE:
string_value = string_child.nodeValue
if not re.search(offending_pattern, string_value):
continue
offenders = re.findall(offending_pattern, string_value)
if offenders:
for offender in offenders:
string_value = string_value.replace(offender, offender[0] + "\\" + offender[-1:])
made_change = True
string_child.nodeValue = string_value
if made_change:
new_contents = doc.toxml()
f = codecs.open(full_path, 'w', 'utf-8')
f.write(new_contents)
f.close()
def recurse(self, paths, file_glob=None):
if paths == None: yield None
if not isinstance(paths, list): paths = [paths]
for path in paths:
for root, dirs, files in os.walk(path):
for filename in files:
if file_glob != None:
if not fnmatch.fnmatch(filename, file_glob): continue
yield os.path.join(root, filename)
def generate_aidl(self):
# support for android remote interfaces in platform/android/src
framework_aidl = self.sdk.platform_path('framework.aidl')
aidl_args = [self.sdk.get_aidl(), '-p' + framework_aidl, '-I' + self.project_src_dir, '-o' + self.project_gen_dir]
for aidl_file in self.recurse(self.project_src_dir, '*.aidl'):
run.run(aidl_args + [aidl_file])
def build_generated_classes(self):
src_list = []
self.module_jars = []
class_delta = timedelta(seconds=1)
for java_file in self.recurse([self.project_src_dir, self.project_gen_dir], '*.java'):
if self.project_src_dir in java_file:
relative_path = java_file[len(self.project_src_dir)+1:]
else:
relative_path = java_file[len(self.project_gen_dir)+1:]
class_file = os.path.join(self.classes_dir, relative_path.replace('.java', '.class'))
if Deltafy.needs_update(java_file, class_file) > 0:
# the file list file still needs each file escaped apparently
debug("adding %s to javac build list" % java_file)
src_list.append('"%s"' % java_file.replace("\\", "\\\\"))
if len(src_list) == 0:
# No sources are older than their classfile counterparts, we can skip javac / dex
return False
classpath = os.pathsep.join([self.android_jar, os.pathsep.join(self.android_jars)])
project_module_dir = os.path.join(self.top_dir,'modules','android')
for module in self.modules:
if module.jar == None: continue
self.module_jars.append(module.jar)
classpath = os.pathsep.join([classpath, module.jar])
module_lib = module.get_resource('lib')
for jar in glob.glob(os.path.join(module_lib, '*.jar')):
self.module_jars.append(jar)
classpath = os.pathsep.join([classpath, jar])
if self.deploy_type != 'production':
classpath = os.pathsep.join([classpath,
os.path.join(self.support_dir, 'lib', 'titanium-verify.jar'),
os.path.join(self.support_dir, 'lib', 'titanium-debug.jar')])
debug("Building Java Sources: " + " ".join(src_list))
javac_command = [self.javac, '-encoding', 'utf8', '-classpath', classpath, '-d', self.classes_dir, '-sourcepath', self.project_src_dir, '-sourcepath', self.project_gen_dir]
(src_list_osfile, src_list_filename) = tempfile.mkstemp()
src_list_file = os.fdopen(src_list_osfile, 'w')
src_list_file.write("\n".join(src_list))
src_list_file.close()
javac_command.append('@' + src_list_filename)
out = run.run(javac_command)
os.remove(src_list_filename)
return True
def create_unsigned_apk(self, resources_zip_file):
unsigned_apk = os.path.join(self.project_dir, 'bin', 'app-unsigned.apk')
self.apk_updated = False
apk_modified = None
if os.path.exists(unsigned_apk):
apk_modified = Deltafy.get_modified_datetime(unsigned_apk)
debug("creating unsigned apk: " + unsigned_apk)
# copy existing resources into the APK
apk_zip = zipfile.ZipFile(unsigned_apk, 'w', zipfile.ZIP_DEFLATED)
def skip_jar_path(path):
ext = os.path.splitext(path)[1]
if path.endswith('/'): return True
if path.startswith('META-INF/'): return True
if path.split('/')[-1].startswith('.'): return True
if ext == '.class': return True
if 'org/appcelerator/titanium/bindings' in path and ext == '.json': return True
def compression_type(path):
ext = os.path.splitext(path)[1]
if ext in uncompressed_types:
return zipfile.ZIP_STORED
return zipfile.ZIP_DEFLATED
def zipinfo(path):
info = zipfile.ZipInfo(path)
info.compress_type = compression_type(path)
return info
def is_modified(path):
return apk_modified is None or Deltafy.needs_update_timestamp(path, apk_modified)
def zip_contains(zip, entry):
try:
zip.getinfo(entry)
except:
return False
return True
if is_modified(resources_zip_file):
self.apk_updated = True
resources_zip = zipfile.ZipFile(resources_zip_file)
for path in resources_zip.namelist():
if skip_jar_path(path): continue
debug("from resource zip => " + path)
apk_zip.writestr(zipinfo(path), resources_zip.read(path))
resources_zip.close()
# add classes.dex
if is_modified(self.classes_dex) or not zip_contains(apk_zip, 'classes.dex'):
apk_zip.write(self.classes_dex, 'classes.dex')
# add all resource files from the project
for root, dirs, files in os.walk(self.project_src_dir):
for file in files:
if os.path.splitext(file)[1] != '.java':
absolute_path = os.path.join(root, file)
relative_path = os.path.join(root[len(self.project_src_dir)+1:], file)
if is_modified(absolute_path):
self.apk_updated = True
debug("resource file => " + relative_path)
apk_zip.write(os.path.join(root, file), relative_path, compression_type(file))
def add_resource_jar(jar_file):
jar = zipfile.ZipFile(jar_file)
for path in jar.namelist():
if skip_jar_path(path): continue
debug("from JAR %s => %s" % (jar_file, path))
apk_zip.writestr(zipinfo(path), jar.read(path))
jar.close()
for jar_file in self.module_jars:
add_resource_jar(jar_file)
for jar_file in self.android_jars:
add_resource_jar(jar_file)
def add_native_libs(libs_dir):
if os.path.exists(libs_dir):
for abi_dir in os.listdir(libs_dir):
libs_abi_dir = os.path.join(libs_dir, abi_dir)
if not os.path.isdir(libs_abi_dir): continue
for file in os.listdir(libs_abi_dir):
if file.endswith('.so'):
native_lib = os.path.join(libs_abi_dir, file)
if is_modified(native_lib):
self.apk_updated = True
debug("installing native lib: %s" % native_lib)
apk_zip.write(native_lib, '/'.join(['lib', abi_dir, file]))
# add any native libraries : libs/**/*.so -> lib/**/*.so
add_native_libs(os.path.join(self.project_dir, 'libs'))
# add module native libraries
for module in self.modules:
add_native_libs(module.get_resource('libs'))
apk_zip.close()
return unsigned_apk
def run_adb(self, *args):
command = [self.sdk.get_adb()]
command.extend(self.device_args)
command.extend(args)
return run.run(command)
def package_and_deploy(self):
ap_ = os.path.join(self.project_dir, 'bin', 'app.ap_')
rhino_jar = os.path.join(self.support_dir, 'js.jar')
# This is only to check if this has been overridden in production
has_compile_js = self.tiappxml.has_app_property("ti.android.compilejs")
compile_js = not has_compile_js or (has_compile_js and \
self.tiappxml.to_bool(self.tiappxml.get_app_property('ti.android.compilejs')))
pkg_assets_dir = self.assets_dir
if self.deploy_type == "production" and compile_js:
non_js_assets = os.path.join(self.project_dir, 'bin', 'non-js-assets')
if not os.path.exists(non_js_assets):
os.mkdir(non_js_assets)
copy_all(self.assets_dir, non_js_assets, ignore_exts=[".js"])
pkg_assets_dir = non_js_assets
run.run([self.aapt, 'package', '-f', '-M', 'AndroidManifest.xml', '-A', pkg_assets_dir,
'-S', 'res', '-I', self.android_jar, '-I', self.titanium_jar, '-F', ap_], warning_regex=r'skipping')
unsigned_apk = self.create_unsigned_apk(ap_)
#unsigned_apk = os.path.join(self.project_dir, 'bin', 'app-unsigned.apk')
#apk_build_cmd = [self.apkbuilder, unsigned_apk, '-u', '-z', ap_, '-f', self.classes_dex, '-rf', self.project_src_dir]
#for jar in self.android_jars:
# apk_build_cmd += ['-rj', jar]
#for jar in self.module_jars:
# apk_build_cmd += ['-rj', jar]
#output, err_output = run.run(apk_build_cmd, ignore_error=True, return_error=True)
#if err_output:
# if 'THIS TOOL IS DEPRECATED' in err_output:
# debug('apkbuilder deprecation warning received')
# else:
# run.check_and_print_err(err_output, None)
if self.dist_dir:
app_apk = os.path.join(self.dist_dir, self.name + '.apk')
else:
app_apk = os.path.join(self.project_dir, 'bin', 'app.apk')
output = run.run([self.jarsigner, '-storepass', self.keystore_pass, '-keystore', self.keystore, '-signedjar', app_apk, unsigned_apk, self.keystore_alias])
run.check_output_for_error(output, r'RuntimeException: (.*)', True)
run.check_output_for_error(output, r'^jarsigner: (.*)', True)
# TODO Document Exit message
#success = re.findall(r'RuntimeException: (.*)', output)
#if len(success) > 0:
# error(success[0])
# sys.exit(1)
# zipalign to align byte boundaries
zipalign = self.sdk.get_zipalign()
if os.path.exists(app_apk+'z'):
os.remove(app_apk+'z')
ALIGN_32_BIT = 4
output = run.run([zipalign, '-v', str(ALIGN_32_BIT), app_apk, app_apk+'z'])
# TODO - Document Exit message
if output == None:
error("System Error while compiling Android classes.dex")
sys.exit(1)
else:
os.unlink(app_apk)
os.rename(app_apk+'z',app_apk)
if self.dist_dir:
sys.exit(0)
if self.build_only:
return (False, False)
out = self.run_adb('get-state')
#out = subprocess.Popen([self.sdk.get_adb(), self.device_type_arg, 'get-state'], stderr=subprocess.PIPE, stdout=subprocess.PIPE).communicate()[0]
out = str(out).strip()
# try a few times as sometimes it fails waiting on boot
attempts = 0
launched = False
launch_failed = False
while attempts < 5:
try:
if self.install:
self.wait_for_device('d')
info("Installing application on emulator")
else:
self.wait_for_device('e')
info("Installing application on device")
output = self.run_adb('install', '-r', app_apk)
#output = run.run(cmd)
if output == None:
launch_failed = True
elif "Failure" in output:
error("Failed installing %s: %s" % (self.app_id, output))
launch_failed = True
elif not self.install:
launched = True
break
except Exception, e:
error(e)
time.sleep(3)
attempts+=1
return (launched, launch_failed)
def run_app(self):
info("Launching application ... %s" % self.name)
output = self.run_adb('shell', 'am', 'start',
'-a', 'android.intent.action.MAIN',
'-c','android.intent.category.LAUNCHER',
'-n', '%s/.%sActivity' % (self.app_id , self.classname))
trace("Launch output: %s" % output)
def wait_for_sdcard(self):
info("Waiting for SDCard to become available..")
waited = 0
max_wait = 60
while waited < max_wait:
output = self.run_adb('shell', 'mount')
if output != None:
mount_points = output.splitlines()
for mount_point in mount_points:
tokens = mount_point.split()
if len(tokens) < 2: continue
mount_path = tokens[1]
if mount_path in ['/sdcard', '/mnt/sdcard']:
return True
else:
error("Error checking for SDCard using 'mount'")
return False
time.sleep(1)
waited += 1
error("Timed out waiting for SDCard to become available (%ds)" % max_wait)
return False
def push_deploy_json(self):
deploy_data = {
"debuggerEnabled": self.debugger_host != None,
"debuggerPort": self.debugger_port,
"fastdevPort": self.fastdev_port
}
deploy_json = os.path.join(self.project_dir, 'bin', 'deploy.json')
open(deploy_json, 'w+').write(simplejson.dumps(deploy_data))
sdcard_available = self.wait_for_sdcard()
if sdcard_available:
self.run_adb('shell', 'mkdir /sdcard/%s || echo' % self.app_id)
self.run_adb('push', deploy_json, '/sdcard/%s/deploy.json' % self.app_id)
os.unlink(deploy_json)
def verify_fastdev(self):
lock_file = os.path.join(self.top_dir, '.fastdev.lock')
if not fastdev.is_running(self.top_dir):
if os.path.exists(lock_file):
os.unlink(lock_file)
return False
else:
data = simplejson.loads(open(lock_file, 'r').read())
self.fastdev_port = data["port"]
return True
def fastdev_kill_app(self):
lock_file = os.path.join(self.top_dir, ".fastdev.lock")
if os.path.exists(lock_file):
class Options(object): pass
options = Options()
options.lock_file = lock_file
try:
return fastdev.kill_app(self.top_dir, options)
except Exception, e:
return False
def merge_internal_module_resources(self):
if not self.android_jars:
return
for jar in self.android_jars:
if not os.path.exists(jar):
continue
res_zip = jar[:-4] + '.res.zip'
if not os.path.exists(res_zip):
continue
res_zip_file = zipfile.ZipFile(res_zip, "r")
try:
zip_extractall(res_zip_file, self.project_dir)
except:
raise
finally:
res_zip_file.close()
def build_and_run(self, install, avd_id, keystore=None, keystore_pass='tirocks', keystore_alias='tidev', dist_dir=None, build_only=False, device_args=None, debugger_host=None):
deploy_type = 'development'
self.build_only = build_only
self.device_args = device_args
if install:
if self.device_args == None:
self.device_args = ['-d']
if keystore == None:
deploy_type = 'test'
else:
deploy_type = 'production'
if self.device_args == None:
self.device_args = ['-e']
self.deploy_type = deploy_type
(java_failed, java_status) = prereq.check_java()
if java_failed:
error(java_status)
sys.exit(1)
# attempt to load any compiler plugins
if len(self.tiappxml.properties['plugins']) > 0:
titanium_dir = os.path.abspath(os.path.join(template_dir,'..','..','..','..'))
local_compiler_dir = os.path.abspath(os.path.join(self.top_dir,'plugins'))
tp_compiler_dir = os.path.abspath(os.path.join(titanium_dir,'plugins'))
if not os.path.exists(tp_compiler_dir) and not os.path.exists(local_compiler_dir):
error("Build Failed (Missing plugins directory)")
sys.exit(1)
compiler_config = {
'platform':'android',
'tiapp':self.tiappxml,
'project_dir':self.top_dir,
'titanium_dir':titanium_dir,
'appid':self.app_id,
'template_dir':template_dir,
'project_name':self.name,
'command':self.command,
'build_dir':s.project_dir,
'app_name':self.name,
'android_builder':self,
'deploy_type':deploy_type,
'dist_dir':dist_dir,
'logger':log
}
for plugin in self.tiappxml.properties['plugins']:
local_plugin_file = os.path.join(local_compiler_dir,plugin['name'],'plugin.py')
plugin_file = os.path.join(tp_compiler_dir,plugin['name'],plugin['version'],'plugin.py')
info("plugin=%s" % plugin_file)
if not os.path.exists(local_plugin_file) and not os.path.exists(plugin_file):
error("Build Failed (Missing plugin for %s)" % plugin['name'])
sys.exit(1)
info("Detected compiler plugin: %s/%s" % (plugin['name'],plugin['version']))
code_path = plugin_file
if os.path.exists(local_plugin_file):
code_path = local_plugin_file
compiler_config['plugin']=plugin
fin = open(code_path, 'rb')
m = hashlib.md5()
m.update(open(code_path,'rb').read())
code_hash = m.hexdigest()
p = imp.load_source(code_hash, code_path, fin)
p.compile(compiler_config)
fin.close()
# in Windows, if the adb server isn't running, calling "adb devices"
# will fork off a new adb server, and cause a lock-up when we
# try to pipe the process' stdout/stderr. the workaround is
# to simply call adb start-server here, and not care about
# the return code / pipes. (this is harmless if adb is already running)
# -- thanks to Bill Dawson for the workaround
if platform.system() == "Windows" and not build_only:
run.run([self.sdk.get_adb(), "start-server"], True, ignore_output=True)
ti_version_file = os.path.join(self.support_dir, '..', 'version.txt')
if os.path.exists(ti_version_file):
ti_version_info = read_properties(open(ti_version_file, 'r'), '=')
if not ti_version_info is None and 'version' in ti_version_info:
ti_version_string = 'Titanium SDK version: %s' % ti_version_info['version']
if 'timestamp' in ti_version_info or 'githash' in ti_version_info:
ti_version_string += ' ('
if 'timestamp' in ti_version_info:
ti_version_string += '%s' % ti_version_info['timestamp']
if 'githash' in ti_version_info:
ti_version_string += ' %s' % ti_version_info['githash']
ti_version_string += ')'
info(ti_version_string)
if not build_only:
if deploy_type == 'development':
self.wait_for_device('e')
elif deploy_type == 'test':
self.wait_for_device('d')
self.install = install
self.dist_dir = dist_dir
self.aapt = self.sdk.get_aapt()
self.android_jar = self.sdk.get_android_jar()
self.titanium_jar = os.path.join(self.support_dir,'titanium.jar')
dx = self.sdk.get_dx()
self.apkbuilder = self.sdk.get_apkbuilder()
self.sdcard_resources = '/sdcard/Ti.debug/%s/Resources' % self.app_id
self.resources_installed = False
if deploy_type == "production":
self.app_installed = False
else:
self.app_installed = not build_only and self.is_app_installed()
debug("%s installed? %s" % (self.app_id, self.app_installed))
#self.resources_installed = not build_only and self.are_resources_installed()
#debug("%s resources installed? %s" % (self.app_id, self.resources_installed))
if keystore == None:
keystore = os.path.join(self.support_dir,'dev_keystore')
self.keystore = keystore
self.keystore_pass = keystore_pass
self.keystore_alias = keystore_alias
curdir = os.getcwd()
self.support_resources_dir = os.path.join(self.support_dir, 'resources')
try:
os.chdir(self.project_dir)
self.android = Android(self.name, self.app_id, self.sdk, deploy_type, self.java)
if not os.path.exists('bin'):
os.makedirs('bin')
resources_dir = os.path.join(self.top_dir,'Resources')
self.assets_dir = os.path.join(self.project_dir,'bin','assets')
self.assets_resources_dir = os.path.join(self.assets_dir,'Resources')
if not os.path.exists(self.assets_dir):
os.makedirs(self.assets_dir)
shutil.copy(self.project_tiappxml, self.assets_dir)
finalxml = os.path.join(self.assets_dir,'tiapp.xml')
self.tiapp = TiAppXML(finalxml)
self.tiapp.setDeployType(deploy_type)
self.sdcard_copy = False
sdcard_property = "ti.android.loadfromsdcard"
if self.tiapp.has_app_property(sdcard_property):
self.sdcard_copy = self.tiapp.to_bool(self.tiapp.get_app_property(sdcard_property))
fastdev_property = "ti.android.fastdev"
fastdev_enabled = (self.deploy_type == 'development' and not self.build_only)
if self.tiapp.has_app_property(fastdev_property):
fastdev_enabled = self.tiapp.to_bool(self.tiapp.get_app_property(fastdev_property))
if fastdev_enabled:
if self.verify_fastdev():
info("Fastdev server running, deploying in Fastdev mode")
self.fastdev = True
else:
warn("Fastdev enabled, but server isn't running, deploying normally")
self.classes_dir = os.path.join(self.project_dir, 'bin', 'classes')
if not os.path.exists(self.classes_dir):
os.makedirs(self.classes_dir)
if (not debugger_host is None) and len(debugger_host) > 0:
hostport = debugger_host.split(":")
self.debugger_host = hostport[0]
self.debugger_port = int(hostport[1])
debugger_enabled = self.debugger_host != None and len(self.debugger_host) > 0
# self.enable_debugger(debugger_host)
self.copy_project_resources()
last_build_info = None
built_all_modules = False
build_info_path = os.path.join(self.project_dir, 'bin', 'build_info.json')
if os.path.exists(build_info_path):
last_build_info = simplejson.loads(open(build_info_path, 'r').read())
built_all_modules = last_build_info["include_all_modules"]
include_all_ti_modules = self.fastdev
if (self.tiapp.has_app_property('ti.android.include_all_modules')):
if self.tiapp.to_bool(self.tiapp.get_app_property('ti.android.include_all_modules')):
include_all_ti_modules = True
if self.tiapp_changed or (self.js_changed and not self.fastdev) or \
self.force_rebuild or self.deploy_type == "production" or \
(self.fastdev and (not self.app_installed or not built_all_modules)):
trace("Generating Java Classes")
self.android.create(os.path.abspath(os.path.join(self.top_dir,'..')),
True, project_dir = self.top_dir, include_all_ti_modules=include_all_ti_modules)
open(build_info_path, 'w').write(simplejson.dumps({
"include_all_modules": include_all_ti_modules
}))
else:
info("Tiapp.xml unchanged, skipping class generation")
# compile resources
full_resource_dir = os.path.join(self.project_dir, self.assets_resources_dir)
compiler = Compiler(self.tiapp, full_resource_dir, self.java, self.classes_dir, self.project_dir,
include_all_modules=include_all_ti_modules)
compiler.compile()
self.compiled_files = compiler.compiled_files
self.android_jars = compiler.jar_libraries
self.merge_internal_module_resources()
if not os.path.exists(self.assets_dir):
os.makedirs(self.assets_dir)
self.resource_drawables_changed = self.copy_resource_drawables()
self.warn_dupe_drawable_folders()
# Detect which modules are being used.
# We need to know this info in a few places, so the info is saved
# in self.missing_modules and self.modules
detector = ModuleDetector(self.top_dir)
self.missing_modules, self.modules = detector.find_app_modules(self.tiapp, 'android')
self.copy_module_platform_folders()
special_resources_dir = os.path.join(self.top_dir,'platform','android')
if os.path.exists(special_resources_dir):
debug("found special platform files dir = %s" % special_resources_dir)
ignore_files = ignoreFiles
ignore_files.extend(['AndroidManifest.xml']) # don't want to overwrite build/android/AndroidManifest.xml yet
self.copy_project_platform_folder(ignoreDirs, ignore_files)
self.generate_stylesheet()
self.generate_aidl()
self.manifest_changed = self.generate_android_manifest(compiler)
my_avd = None
self.google_apis_supported = False
# find the AVD we've selected and determine if we support Google APIs
for avd_props in avd.get_avds(self.sdk):
if avd_props['id'] == avd_id:
my_avd = avd_props
self.google_apis_supported = (my_avd['name'].find('Google')!=-1 or my_avd['name'].find('APIs')!=-1)
break
if build_only:
self.google_apis_supported = True
remove_orphaned_files(resources_dir, os.path.join(self.project_dir, 'bin', 'assets', 'Resources'))
generated_classes_built = self.build_generated_classes()
# TODO: enable for "test" / device mode for debugger / fastdev
if not self.build_only and self.deploy_type == "development":
self.push_deploy_json()
self.classes_dex = os.path.join(self.project_dir, 'bin', 'classes.dex')
def jar_includer(path, isfile):
if isfile and path.endswith(".jar"): return True
return False
support_deltafy = Deltafy(self.support_dir, jar_includer)
self.support_deltas = support_deltafy.scan()
dex_built = False
if len(self.support_deltas) > 0 or generated_classes_built or self.deploy_type == "production":
# the dx.bat that ships with android in windows doesn't allow command line
# overriding of the java heap space, so we call the jar directly
if platform.system() == 'Windows':
dex_args = [self.java, '-Xmx1024M', '-Djava.ext.dirs=%s' % self.sdk.get_platform_tools_dir(), '-jar', self.sdk.get_dx_jar()]
else:
dex_args = [dx, '-JXmx1536M', '-JXX:-UseGCOverheadLimit']
dex_args += ['--dex', '--output='+self.classes_dex, self.classes_dir]
dex_args += self.android_jars
dex_args += self.module_jars
if self.deploy_type != 'production':
dex_args.append(os.path.join(self.support_dir, 'lib', 'titanium-verify.jar'))
dex_args.append(os.path.join(self.support_dir, 'lib', 'titanium-debug.jar'))
# the verifier depends on Ti.Network classes, so we may need to inject it
has_network_jar = False
for jar in self.android_jars:
if jar.endswith('titanium-network.jar'):
has_network_jar = True
break
if not has_network_jar:
dex_args.append(os.path.join(self.support_dir, 'modules', 'titanium-network.jar'))
# substitute for the debugging js jar in non production mode
for jar in self.android_jars:
if jar.endswith('js.jar'):
dex_args.remove(jar)
dex_args.append(os.path.join(self.support_dir, 'js-debug.jar'))
info("Compiling Android Resources... This could take some time")
# TODO - Document Exit message
run_result = run.run(dex_args, warning_regex=r'warning: ')
if (run_result == None):
dex_built = False
error("System Error while compiling Android classes.dex")
sys.exit(1)
else:
dex_built = True
debug("Android classes.dex built")
"""if self.sdcard_copy and not build_only and \
(not self.resources_installed or not self.app_installed) and \
(self.deploy_type == 'development' or self.deploy_type == 'test'):
if self.install: self.wait_for_device('e')
else: self.wait_for_device('d')
trace("Performing full copy to SDCARD -> %s" % self.sdcard_resources)
output = self.run_adb('push', os.path.join(self.top_dir, 'Resources'), self.sdcard_resources)
trace("result: %s" % output)
android_resources_dir = os.path.join(self.top_dir, 'Resources', 'android')
if os.path.exists(android_resources_dir):
output = self.run_adb('push', android_resources_dir, self.sdcard_resources)
trace("result: %s" % output)"""
if dex_built or generated_classes_built or self.tiapp_changed or self.manifest_changed or not self.app_installed or not self.fastdev:
# metadata has changed, we need to do a full re-deploy
launched, launch_failed = self.package_and_deploy()
if launched:
self.run_app()
info("Deployed %s ... Application should be running." % self.name)
elif launch_failed==False and not build_only:
info("Application installed. Launch from drawer on Home Screen")
elif not build_only:
# Relaunch app if nothing was built
info("Re-launching application ... %s" % self.name)
relaunched = False
killed = False
if self.fastdev:
killed = self.fastdev_kill_app()
if not killed:
processes = self.run_adb('shell', 'ps')
for line in processes.splitlines():
columns = line.split()
if len(columns) > 1:
pid = columns[1]
id = columns[len(columns)-1]
if id == self.app_id:
self.run_adb('shell', 'kill', pid)
relaunched = True
self.run_app()
if relaunched:
info("Relaunched %s ... Application should be running." % self.name)
#intermediary code for on-device debugging (later)
#if debugger_host != None:
#import debugger
#debug("connecting to debugger: %s, debugger=%s" % (debugger_host, str(debugger)))
#debugger.run(debugger_host, '127.0.0.1:5999')
finally:
os.chdir(curdir)
sys.stdout.flush()
if __name__ == "__main__":
def usage():
print "%s <command> <project_name> <sdk_dir> <project_dir> <app_id> [key] [password] [alias] [dir] [avdid] [avdsdk]" % os.path.basename(sys.argv[0])
print
print "available commands: "
print
print " emulator build and run the emulator"
print " simulator build and run the app on the simulator"
print " install build and install the app on the device"
print " distribute build final distribution package for upload to marketplace"
print " run build and run the project using values from tiapp.xml"
print " run-emulator run the emulator with a default AVD ID and skin"
sys.exit(1)
argc = len(sys.argv)
if argc < 2:
usage()
command = sys.argv[1]
template_dir = os.path.abspath(os.path.dirname(sys._getframe(0).f_code.co_filename))
get_values_from_tiapp = False
if command == 'run':
if argc < 4:
print 'Usage: %s run <project_dir> <android_sdk>' % sys.argv[0]
sys.exit(1)
get_values_from_tiapp = True
project_dir = sys.argv[2]
sdk_dir = sys.argv[3]
avd_id = "7"
elif command == 'run-emulator':
if argc < 4:
print 'Usage: %s run-emulator <project_dir> <android_sdk>' % sys.argv[0]
sys.exit(1)
get_values_from_tiapp = True
project_dir = sys.argv[2]
sdk_dir = sys.argv[3]
# sensible defaults?
avd_id = "7"
avd_skin = "HVGA"
else:
if argc < 6 or command == '--help' or (command=='distribute' and argc < 10):
usage()
if get_values_from_tiapp:
tiappxml = TiAppXML(os.path.join(project_dir, 'tiapp.xml'))
app_id = tiappxml.properties['id']
project_name = tiappxml.properties['name']
else:
project_name = dequote(sys.argv[2])
sdk_dir = os.path.abspath(os.path.expanduser(dequote(sys.argv[3])))
project_dir = os.path.abspath(os.path.expanduser(dequote(sys.argv[4])))
app_id = dequote(sys.argv[5])
log = TiLogger(os.path.join(os.path.abspath(os.path.expanduser(dequote(project_dir))), 'build.log'))
log.debug(" ".join(sys.argv))
s = Builder(project_name,sdk_dir,project_dir,template_dir,app_id)
s.command = command
try:
if command == 'run-emulator':
s.run_emulator(avd_id, avd_skin)
elif command == 'run':
s.build_and_run(False, avd_id)
elif command == 'emulator':
avd_id = dequote(sys.argv[6])
avd_skin = dequote(sys.argv[7])
s.run_emulator(avd_id, avd_skin)
elif command == 'simulator':
info("Building %s for Android ... one moment" % project_name)
avd_id = dequote(sys.argv[6])
debugger_host = None
if len(sys.argv) > 8:
debugger_host = dequote(sys.argv[8])
s.build_and_run(False, avd_id, debugger_host=debugger_host)
elif command == 'install':
avd_id = dequote(sys.argv[6])
device_args = ['-d']
if len(sys.argv) >= 8:
device_args = ['-s', sys.argv[7]]
s.build_and_run(True, avd_id, device_args=device_args)
elif command == 'distribute':
key = os.path.abspath(os.path.expanduser(dequote(sys.argv[6])))
password = dequote(sys.argv[7])
alias = dequote(sys.argv[8])
output_dir = dequote(sys.argv[9])
avd_id = dequote(sys.argv[10])
s.build_and_run(True, avd_id, key, password, alias, output_dir)
elif command == 'build':
s.build_and_run(False, 1, build_only=True)
else:
error("Unknown command: %s" % command)
usage()
except SystemExit, n:
sys.exit(n)
except:
e = traceback.format_exc()
error("Exception occured while building Android project:")
for line in e.splitlines():
error(line)
sys.exit(1)
sys.exit(0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment