From 2feea4028e44d70608a6d3307a7e771b1d805610 Mon Sep 17 00:00:00 2001 From: feiwang Date: Mon, 11 Apr 2016 01:05:19 +0800 Subject: [PATCH 01/11] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86build.gradle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++++ build.gradle | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/.gitignore b/.gitignore index 03355af..3ef6924 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ *.apk *.ap_ +gradle/ +gradlew +gradlew.bat + # Files for the Dalvik VM *.dex diff --git a/build.gradle b/build.gradle index ee7766c..4f15a2c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,18 @@ buildscript { repositories { + mavenCentral() + maven { + url "http://maven.chunyu.mobi/content/groups/public/" + credentials { + username maven_user + password maven_password + } + } + jcenter() } + + dependencies { classpath 'com.android.tools.build:gradle:1.2.3' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' @@ -11,6 +22,14 @@ buildscript { allprojects { repositories { + mavenCentral() + maven { + url "http://maven.chunyu.mobi/content/groups/public/" + credentials { + username maven_user + password maven_password + } + } jcenter() } } From c20628348489cb462583651a2f2d7be3cd3e72cf Mon Sep 17 00:00:00 2001 From: feiwang Date: Mon, 11 Apr 2016 10:38:22 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cast.py | 419 +++++++--- .../github/mmin18/layoutcast/LayoutCast.java | 1 + .../mmin18/layoutcast/ResetActivity.java | 3 + .../mmin18/layoutcast/server/LcastServer.java | 534 +++++++------ .../mmin18/layoutcast/util/ArtUtils.java | 75 +- .../layoutcast/util/EmbedHttpServer.java | 745 +++++++++--------- .../src/main/AndroidManifest.xml | 8 +- .../src/main/res/layout/fragment_main.xml | 11 +- 8 files changed, 1041 insertions(+), 755 deletions(-) diff --git a/cast.py b/cast.py index 3025272..0a8b601 100644 --- a/cast.py +++ b/cast.py @@ -16,11 +16,15 @@ import json import zipfile + # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + def which(program): import os + fpath, fname = os.path.split(program) if fpath: if is_exe(program): @@ -34,17 +38,20 @@ def which(program): return None + def cexec_fail_exit(args, code, stdout, stderr): if code != 0: - print('Fail to exec %s'%args) + print('Fail to exec %s' % args) print(stdout) print(stderr) exit(code) -def cexec(args, callback = cexec_fail_exit, addPath = None, exitcode=1): + +def cexec(args, callback=cexec_fail_exit, addPath=None, exitcode=1): env = None if addPath: import copy + env = copy.copy(os.environ) env['PATH'] = addPath + os.path.pathsep + env['PATH'] p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) @@ -53,17 +60,22 @@ def cexec(args, callback = cexec_fail_exit, addPath = None, exitcode=1): if code and exitcode: code = exitcode if callback: - callback(args, code, output, err) + callback(args, code, output, err) return output + def curl(url, body=None, ignoreError=False, exitcode=1): + print ("URL: ", url) import sys + try: if sys.version_info >= (3, 0): import urllib.request + return urllib.request.urlopen(url, data=body).read().decode('utf-8').strip() else: import urllib2 + return urllib2.urlopen(url, data=body).read().decode('utf-8').strip() except Exception as e: if ignoreError: @@ -72,20 +84,25 @@ def curl(url, body=None, ignoreError=False, exitcode=1): print(e) exit(exitcode) + def open_as_text(path): if not path or not os.path.isfile(path): return '' with io.open(path, 'r', errors='replace') as f: data = f.read() return data - print('fail to open %s'%path) + print('fail to open %s' % path) return '' + def is_gradle_project(dir): return os.path.isfile(os.path.join(dir, 'build.gradle')) + def parse_properties(path): - return os.path.isfile(path) and dict(line.strip().split('=') for line in open(path) if ('=' in line and not line.startswith('#'))) or {} + return os.path.isfile(path) and dict(line.strip().split('=') for line in open(path) if + ('=' in line and not line.startswith('#'))) or {} + def balanced_braces(arg): if '{' not in arg: @@ -107,13 +124,15 @@ def balanced_braces(arg): chars.append(c) return '' + def remove_comments(str): # remove comments in groovy return re.sub(r'''(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)''', '', str) + def __deps_list_eclipse(list, project): prop = parse_properties(os.path.join(project, 'project.properties')) - for i in range(1,100): + for i in range(1, 100): dep = prop.get('android.library.reference.%d' % i) if dep: absdep = os.path.abspath(os.path.join(project, dep)) @@ -121,11 +140,15 @@ def __deps_list_eclipse(list, project): if not absdep in list: list.append(absdep) + +# def __deps_list_gradle(list, project): str = open_as_text(os.path.join(project, 'build.gradle')) str = remove_comments(str) ideps = [] + # for depends in re.findall(r'dependencies\s*\{.*?\}', str, re.DOTALL | re.MULTILINE): + # 1. 获取项目内的依赖 for m in re.finditer(r'dependencies\s*\{', str): depends = balanced_braces(str[m.start():]) for proj in re.findall(r'''compile\s+project\s*\(.*['"]:(.+)['"].*\)''', depends): @@ -135,15 +158,22 @@ def __deps_list_gradle(list, project): path = project for i in range(1, 3): + # 这是什么意思? + # /xxx/path0/path1/path2/ + # /xxx/path0/path1/path2/.. + # /xxx/path0/path1/path2/../.. path = os.path.abspath(os.path.join(path, os.path.pardir)) b = True deps = [] for idep in ideps: + # 定位: deps dep = os.path.join(path, idep) if not os.path.isdir(dep): b = False break deps.append(dep) + + # 如果有: deps, 如何处理呢? if b: for dep in deps: __deps_list_gradle(list, dep) @@ -151,6 +181,7 @@ def __deps_list_gradle(list, project): list.append(dep) break + def deps_list(dir): if is_gradle_project(dir): list = [] @@ -161,24 +192,40 @@ def deps_list(dir): __deps_list_eclipse(list, dir) return list + def manifestpath(dir): + # Android项目的布局 + # 要么在跟目录下有啥,要么在: src/main目录下有啥 if os.path.isfile(os.path.join(dir, 'AndroidManifest.xml')): return os.path.join(dir, 'AndroidManifest.xml') if os.path.isfile(os.path.join(dir, 'src', 'main', 'AndroidManifest.xml')): return os.path.join(dir, 'src', 'main', 'AndroidManifest.xml') + def package_name(dir): + # 1. 获取AndroidManifest.xml文件 path = manifestpath(dir) data = open_as_text(path) + + # 2. 判断是否存在: package= + # 可能AndroidManifest为空? for pn in re.findall('package=\"([\w\d_\.]+)\"', data): return pn + def get_apk_path(dir): + # 获取最新的apk + # apk的路径: + # bin/*.apk + # build/outputs/apk/*.apk + # if not is_gradle_project(dir): - apkpath = os.path.join(dir,'bin') + apkpath = os.path.join(dir, 'bin') else: - apkpath = os.path.join(dir,'build','outputs','apk') - #Get the lastmodified *.apk file + apkpath = os.path.join(dir, 'build', 'outputs', 'apk') + + + # Get the lastmodified *.apk file maxt = 0 maxd = None for dirpath, dirnames, files in os.walk(apkpath): @@ -190,107 +237,123 @@ def get_apk_path(dir): maxd = os.path.join(dirpath, fn) return maxd + def package_name_fromapk(dir, sdkdir): - #Get the package name from maxd + # Get the package name from maxd aaptpath = get_aapt(sdkdir) if aaptpath: apkpath = get_apk_path(dir) if apkpath: - aaptargs = [aaptpath, 'dump','badging', apkpath] + # 通过: aapt来操作 + aaptargs = [aaptpath, 'dump', 'badging', apkpath] output = cexec(aaptargs, callback=None) + for pn in re.findall('package: name=\'([^\']+)\'', output): return pn return package_name(dir) -def get_latest_packagename(dirlist,sdkdir): + +def get_latest_packagename(dirlist, sdkdir): maxt = 0 maxd = None for dir in dirlist: if dir: - apkfile= get_apk_path(dir) + # 这个是什么意思呢? + # 是不是最终只有一个apk是签名的呢? + apkfile = get_apk_path(dir) if apkfile: lastModified = os.path.getmtime(apkfile) if lastModified > maxt: maxt = lastModified maxd = dir if maxd: - return package_name_fromapk(maxd,sdkdir) + return package_name_fromapk(maxd, sdkdir) + def isResName(name): - if name=='drawable' or name.startswith('drawable-'): + if name == 'drawable' or name.startswith('drawable-'): return 2 - if name=='layout' or name.startswith('layout-'): + if name == 'layout' or name.startswith('layout-'): return 2 - if name=='values' or name.startswith('values-'): + if name == 'values' or name.startswith('values-'): return 2 - if name=='anim' or name.startswith('anim-'): + if name == 'anim' or name.startswith('anim-'): return 1 - if name=='color' or name.startswith('color-'): + if name == 'color' or name.startswith('color-'): return 1 - if name=='menu' or name.startswith('menu-'): + if name == 'menu' or name.startswith('menu-'): return 1 - if name=='raw' or name.startswith('raw-'): + if name == 'raw' or name.startswith('raw-'): return 1 - if name=='xml' or name.startswith('xml-'): + if name == 'xml' or name.startswith('xml-'): return 1 - if name=='mipmap' or name.startswith('mipmap-'): + if name == 'mipmap' or name.startswith('mipmap-'): return 1 - if name=='animator' or name.startswith('animator-'): + if name == 'animator' or name.startswith('animator-'): return 1 return 0 + def countResDir(dir): c = 0 d = 0 if os.path.isdir(dir): for subd in os.listdir(dir): v = isResName(subd) - if v>1: - d+=1 - if v>0: - c+=1 - if d==0: + if v > 1: + d += 1 + if v > 0: + c += 1 + if d == 0: return 0 return c + def countAssetDir(dir): a = 0 if os.path.isdir(dir): for subd in os.listdir(dir): if not subd.startswith('.'): - a+=1 + a += 1 return a + def resdir(dir): dir1 = os.path.join(dir, 'res') dir2 = os.path.join(dir, 'src', 'main', 'res') a = countResDir(dir1) b = countResDir(dir2) - if b==0 and a==0: + if b == 0 and a == 0: return None - elif b>a: + elif b > a: return dir2 else: return dir1 + def assetdir(dir): + # 注意项目的组织形式: + # asserts + # src/main/asserts dir1 = os.path.join(dir, 'assets') dir2 = os.path.join(dir, 'src', 'main', 'assets') a = countAssetDir(dir1) b = countAssetDir(dir2) - if b==0 and a==0: + if b == 0 and a == 0: return None - elif b>a: + elif b > a: return dir2 else: return dir1 + def get_asset_from_apk(apk_filename, dest_dir): with zipfile.ZipFile(apk_filename) as zf: for member in zf.infolist(): path = dest_dir if member.filename.startswith('assets/'): - zf.extract(member,path) + zf.extract(member, path) + def countSrcDir2(dir, lastBuild=0, list=None): count = 0 @@ -303,22 +366,26 @@ def countSrcDir2(dir, lastBuild=0, list=None): count += 1 mt = os.path.getmtime(os.path.join(dirpath, fn)) lastModified = max(lastModified, mt) - if list!=None and mt>lastBuild: + if list != None and mt > lastBuild: list.append(os.path.join(dirpath, fn)) return (count, lastModified) + def srcdir2(dir, lastBuild=0, list=None): for srcdir in [os.path.join(dir, 'src', 'main', 'java'), os.path.join(dir, 'src')]: olist = None - if list!=None: + if list != None: olist = [] + + # 返回源代码的文件数&最后修改时间 (count, lastModified) = countSrcDir2(srcdir, lastBuild=lastBuild, list=olist) - if count>0: - if list!=None: + if count > 0: + if list != None: list.extend(olist) return (srcdir, count, lastModified) return (None, 0, 0) + def libdir(dir): ddir = os.path.join(dir, 'libs') if os.path.isdir(ddir): @@ -326,6 +393,7 @@ def libdir(dir): else: return None + def is_launchable_project(dir): if is_gradle_project(dir): data = open_as_text(os.path.join(dir, 'build.gradle')) @@ -334,27 +402,35 @@ def is_launchable_project(dir): return True elif os.path.isfile(os.path.join(dir, 'project.properties')): data = open_as_text(os.path.join(dir, 'project.properties')) - if re.findall(r'''^\s*target\s*=.*$''', data, re.MULTILINE) and not re.findall(r'''^\s*android.library\s*=\s*true\s*$''', data, re.MULTILINE): + if re.findall(r'''^\s*target\s*=.*$''', data, re.MULTILINE) and not re.findall( + r'''^\s*android.library\s*=\s*true\s*$''', data, re.MULTILINE): return True return False + def __append_project(list, dir, depth): if package_name(dir): list.append(dir) elif depth > 0: for cname in os.listdir(dir): - if cname=='build' or cname=='bin': + if cname == 'build' or cname == 'bin': continue cdir = os.path.join(dir, cname) if os.path.isdir(cdir): - __append_project(list, cdir, depth-1) + __append_project(list, cdir, depth - 1) + def list_projects(dir): list = [] if os.path.isfile(os.path.join(dir, 'settings.gradle')): data = open_as_text(os.path.join(dir, 'settings.gradle')) + + # 找出所有的 include pattern for line in re.findall(r'''include\s*(.+)''', data): - for proj in re.findall(r'''[\s,]+['"](.*?)['"]''', ','+line): + # 找出所有的project + for proj in re.findall(r'''[\s,]+['"](.*?)['"]''', ',' + line): + # include ":library:common" --> library/common + # include "library" ? dproj = (proj.startswith(':') and proj[1:] or proj).replace(':', os.path.sep) cdir = os.path.join(dir, dproj) if package_name(cdir): @@ -363,20 +439,23 @@ def list_projects(dir): __append_project(list, dir, 2) return list + def list_aar_projects(dir, deps): pnlist = [package_name(i) for i in deps] pnlist.append(package_name(dir)) list1 = [] if os.path.isdir(os.path.join(dir, 'build', 'intermediates', 'incremental', 'mergeResources')): - for dirpath, dirnames, files in os.walk(os.path.join(dir, 'build', 'intermediates', 'incremental', 'mergeResources')): + for dirpath, dirnames, files in os.walk( + os.path.join(dir, 'build', 'intermediates', 'incremental', 'mergeResources')): if re.findall(r'[/\\+]androidTest[/\\+]', dirpath): continue for fn in files: - if fn=='merger.xml': + if fn == 'merger.xml': data = open_as_text(os.path.join(dirpath, fn)) for s in re.findall(r'''path="([^"]+)"''', data): (parent, child) = os.path.split(s) - if child.endswith('.xml') or child.endswith('.png') or child.endswith('.jpg'): + if child.endswith('.xml') or child.endswith('.png') or child.endswith( + '.jpg'): (parent, child) = os.path.split(parent) if isResName(child) and not parent in list1: list1.append(parent) @@ -390,6 +469,8 @@ def list_aar_projects(dir, deps): list2.append(ppath) return list2 + +# 获取android.jar文件的路径 def get_android_jar(path): if not os.path.isdir(path): return None @@ -400,7 +481,9 @@ def get_android_jar(path): result = None for pd in os.listdir(platforms): pd = os.path.join(platforms, pd) - if os.path.isdir(pd) and os.path.isfile(os.path.join(pd, 'source.properties')) and os.path.isfile(os.path.join(pd, 'android.jar')): + if os.path.isdir(pd) and os.path.isfile( + os.path.join(pd, 'source.properties')) and os.path.isfile( + os.path.join(pd, 'android.jar')): s = open_as_text(os.path.join(pd, 'source.properties')) m = re.search(r'^AndroidVersion.ApiLevel\s*[=:]\s*(.*)$', s, re.MULTILINE) if m: @@ -410,15 +493,21 @@ def get_android_jar(path): result = os.path.join(pd, 'android.jar') return result + def get_adb(path): - execname = os.name=='nt' and 'adb.exe' or 'adb' + execname = os.name == 'nt' and 'adb.exe' or 'adb' if os.path.isdir(path) and is_exe(os.path.join(path, 'platform-tools', execname)): return os.path.join(path, 'platform-tools', execname) + def get_aapt(path): - execname = os.name=='nt' and 'aapt.exe' or 'aapt' + # 首先获取: execname + execname = os.name == 'nt' and 'aapt.exe' or 'aapt' + # 给定sdk path if os.path.isdir(path) and os.path.isdir(os.path.join(path, 'build-tools')): btpath = os.path.join(path, 'build-tools') + + # 实现版本号比较 minv = LooseVersion('0') minp = None for pn in os.listdir(btpath): @@ -428,8 +517,10 @@ def get_aapt(path): minp = os.path.join(btpath, pn, execname) return minp + def get_dx(path): - execname = os.name=='nt' and 'dx.bat' or 'dx' + # dx的作用 + execname = os.name == 'nt' and 'dx.bat' or 'dx' if os.path.isdir(path) and os.path.isdir(os.path.join(path, 'build-tools')): btpath = os.path.join(path, 'build-tools') minv = LooseVersion('0') @@ -441,7 +532,13 @@ def get_dx(path): minp = os.path.join(btpath, pn, execname) return minp -def get_android_sdk(dir, condf = get_android_jar): + +def get_android_sdk(dir, condf=get_android_jar): + # 如何获取Android SDK + # 1. local.properties --> sdk.dir + # 2. ANDROID_HOME/ANDROID_SDK + # 3. 其他: ~/Library/Android/sdk + # s = open_as_text(os.path.join(dir, 'local.properties')) m = re.search(r'^sdk.dir\s*[=:]\s*(.*)$', s, re.MULTILINE) if m: @@ -467,8 +564,10 @@ def get_android_sdk(dir, condf = get_android_jar): if path and os.path.isdir(path) and condf(path): return path + def get_javac(dir): - execname = os.name=='nt' and 'javac.exe' or 'javac' + # 如何获取JavaC + execname = os.name == 'nt' and 'javac.exe' or 'javac' if dir and os.path.isfile(os.path.join(dir, 'bin', execname)): return os.path.join(dir, 'bin', execname) @@ -480,7 +579,7 @@ def get_javac(dir): if path and is_exe(os.path.join(path, 'bin', execname)): return os.path.join(path, 'bin', execname) - if os.name=='nt': + if os.name == 'nt': btpath = 'C:\\Program Files\\Java' if os.path.isdir(btpath): minv = '' @@ -493,7 +592,9 @@ def get_javac(dir): minp = path return minp else: - for btpath in ['/Library/Java/JavaVirtualMachines', '/System/Library/Java/JavaVirtualMachines']: + # 如果没有指定,则在默认的路径下搜索 + for btpath in ['/Library/Java/JavaVirtualMachines', + '/System/Library/Java/JavaVirtualMachines']: if os.path.isdir(btpath): minv = '' minp = None @@ -506,16 +607,19 @@ def get_javac(dir): if minp: return minp + def search_path(dir, filename): dir0 = filename if os.path.sep in filename: dir0 = filename[0:filename.index(os.path.sep)] + list = [] for dirpath, dirnames, files in os.walk(dir): if re.findall(r'[/\\+]androidTest[/\\+]', dirpath) or '/.' in dirpath: continue if dir0 in dirnames and os.path.isfile(os.path.join(dirpath, filename)): list.append(dirpath) + if len(list) == 1: return list[0] elif len(list) > 1: @@ -534,31 +638,46 @@ def search_path(dir, filename): else: return os.path.join(dir, 'debug') + def get_maven_libs(projs): maven_deps = [] for proj in projs: str = open_as_text(os.path.join(proj, 'build.gradle')) str = remove_comments(str) + + # 获取maven的 lib 依赖 for m in re.finditer(r'dependencies\s*\{', str): depends = balanced_braces(str[m.start():]) + + # compile的格式: + # compile 'com.facebook.fresco:fresco:0.6.0+' for mvndep in re.findall(r'''compile\s+['"](.+:.+:.+)(?:@*)?['"]''', depends): mvndeps = mvndep.split(':') if not mvndeps in maven_deps: maven_deps.append(mvndeps) return maven_deps + +# 获取 libs 对应的 maven jars def get_maven_jars(libs): if not libs: return [] jars = [] maven_path_prefix = [] - # ~/.gralde/caches + + # 1. ~/.gralde/caches gradle_home = os.path.join(os.path.expanduser('~'), '.gradle', 'caches') + for dirpath, dirnames, files in os.walk(gradle_home): # search in ~/.gradle/**/GROUP_ID/ARTIFACT_ID/VERSION/**/*.jar + # libs的格式? + # ["com.facebook.fresco", "fresco", "0.6.0+"] + # dirpath为当前遍历的路径 + # dirname为 dirpath下类型为DIR的东西 for mvndeps in libs: if mvndeps[0] in dirnames: dir1 = os.path.join(dirpath, mvndeps[0], mvndeps[1]) + if os.path.isdir(dir1): dir2 = os.path.join(dir1, mvndeps[2]) if os.path.isdir(dir2): @@ -569,38 +688,60 @@ def get_maven_jars(libs): prefix = prefix[0:prefix.index('+')] maxdir = '' for subd in os.listdir(dir1): - if subd.startswith(prefix) and subd>maxdir: + if subd.startswith(prefix) and subd > maxdir: maxdir = subd if maxdir: maven_path_prefix.append(os.path.join(dir1, maxdir)) for dirprefix in maven_path_prefix: if dirpath.startswith(dirprefix): for fn in files: - if fn.endswith('.jar') and not fn.startswith('.') and not fn.endswith('-sources.jar') and not fn.endswith('-javadoc.jar'): + if fn.endswith('.jar') and not fn.startswith('.') and not fn.endswith( + '-sources.jar') and not fn.endswith('-javadoc.jar'): jars.append(os.path.join(dirpath, fn)) break return jars + def scan_port(adbpath, pnlist, projlist): + """ + 返回可用的 <端口, project_dir, packagename> + :param adbpath: + :param pnlist: + :param projlist: + :return: + """ port = 0 prodir = None packagename = None - for i in range(0,10): - cexec([adbpath, 'forward', 'tcp:%d'%(41128+i), 'tcp:%d'%(41128+i)]) - output = curl('http://127.0.0.1:%d/packagename'%(41128+i), ignoreError=True) - if output and output in pnlist : - index = pnlist.index(output) # index of this app in projlist - state = curl('http://127.0.0.1:%d/appstate'%(41128+i), ignoreError=True) + for i in range(0, 10): + try_port = (41128 + i) + # 1. 通过adb做端口映射 + cexec([adbpath, 'forward', 'tcp:%d' % try_port, 'tcp:%d' % try_port]) + + # 2. 然后 pnlist + # projlist + output = curl('http://127.0.0.1:%d/packagename' % try_port, ignoreError=True) + if output and output in pnlist: + # 如果返回的 packagename可以接受 + index = pnlist.index(output) # index of this app in projlist + + # 获取 app的状态 + # appstate是如何定义的呢? + state = curl('http://127.0.0.1:%d/appstate' % try_port, ignoreError=True) if state and int(state) >= 2: - port = 41128+i + # starte >= 2 表示界面可见 + port = try_port prodir = projlist[index] packagename = output break + + # 删除多余的端口映射 for i in range(0, 10): - if (41128+i) != port: - cexec([adbpath, 'forward', '--remove', 'tcp:%d'%(41128+i)], callback=None) + if (41128 + i) != port: + cexec([adbpath, 'forward', '--remove', 'tcp:%d' % (41128 + i)], callback=None) return port, prodir, packagename + if __name__ == "__main__": dir = '.' @@ -609,6 +750,7 @@ def scan_port(adbpath, pnlist, projlist): starttime = time.time() + # 1. 手动指定: Android SDK Path/JDK Path以及Project if len(sys.argv) > 1: parser = argparse.ArgumentParser() parser.add_argument('--sdk', help='specify Android SDK path') @@ -622,8 +764,10 @@ def scan_port(adbpath, pnlist, projlist): if args.project: dir = args.project + # 2. 获取有的 project list projlist = [i for i in list_projects(dir) if is_launchable_project(i)] + # 3. 获取默认的android sdk/java sdk if not sdkdir: sdkdir = get_android_sdk(dir) if not sdkdir: @@ -631,22 +775,31 @@ def scan_port(adbpath, pnlist, projlist): exit(2) if not projlist: - print('no valid android project found in '+os.path.abspath(dir)) + print('no valid android project found in ' + os.path.abspath(dir)) exit(3) - pnlist = [package_name_fromapk(i,sdkdir) for i in projlist] + # 4. 如何获取packagename呢? + pnlist = [package_name_fromapk(i, sdkdir) for i in projlist] portlist = [0 for i in pnlist] + + # 获取adb adbpath = get_adb(sdkdir) + if not adbpath: - print('adb not found in %s/platform-tools'%sdkdir) + print('adb not found in %s/platform-tools' % sdkdir) exit(4) + + # 1. 进行端口扫描 + # 如果同时运行多个apk, 如何知道哪一个apk是当前正在调试的呢?) appstate port, dir, packagename = scan_port(adbpath, pnlist, projlist) + # 2. 没有启动(则继续尝试等待) if port == 0: - #launch app + # 启动app, 并且进行端口扫描 + # launch app latest_package = get_latest_packagename(projlist, sdkdir) if latest_package: - cexec([adbpath,'shell','monkey','-p',latest_package,'-c','android.intent.category.LAUNCHER','1'], callback=None) + cexec([adbpath, 'shell', 'monkey', '-p', latest_package, '-c', 'android.intent.category.LAUNCHER', '1'], callback=None) for i in range(0, 6): # try 6 times to wait the application launches port, dir, packagename = scan_port(adbpath, pnlist, projlist) @@ -655,9 +808,25 @@ def scan_port(adbpath, pnlist, projlist): time.sleep(0.25) if port == 0: - print('package %s not found, make sure your project is properly setup and running'%(len(pnlist)==1 and pnlist[0] or pnlist)) + print('package %s not found, make sure your project is properly setup and running' % ( + len(pnlist) == 1 and pnlist[0] or pnlist)) exit(5) + + URL_LCAST = 'http://127.0.0.1:%d/lcast' % port + URL_PUSH_DEX = 'http://127.0.0.1:%d/pushdex' % port + URL_LAUNCH = 'http://127.0.0.1:%d/launcher' % port + URL_PCAST = 'http://127.0.0.1:%d/pcast' % port + + URL_IDS = 'http://127.0.0.1:%d/ids.xml' % port + URL_PUBLIC = 'http://127.0.0.1:%d/public.xml' % port + + # 将资源推送给手机 + URL_PUSH_RES = 'http://127.0.0.1:%d/pushres' % port + + # 用于判断手机是否支持: ART + URL_VM_VERSION = 'http://127.0.0.1:%d/vmversion' % port + is_gradle = is_gradle_project(dir) android_jar = get_android_jar(sdkdir) @@ -665,26 +834,37 @@ def scan_port(adbpath, pnlist, projlist): print('android.jar not found !!!\nUse local.properties or set ANDROID_HOME env') exit(7) deps = deps_list(dir) + + # build/lcast bindir = is_gradle and os.path.join(dir, 'build', 'lcast') or os.path.join(dir, 'bin', 'lcast') # check if the /res and /src has changed lastBuild = 0 + + # 获取apk的路径(fpath, 以及build的时间) rdir = is_gradle and os.path.join(dir, 'build', 'outputs', 'apk') or os.path.join(dir, 'bin') if os.path.isdir(rdir): for fn in os.listdir(rdir): if fn.endswith('.apk') and not '-androidTest' in fn: fpath = os.path.join(rdir, fn) lastBuild = max(lastBuild, os.path.getmtime(fpath)) + + + # dir, deps如何处理呢? adeps = [] adeps.extend(deps) adeps.append(dir) + latestResModified = 0 latestSrcModified = 0 srcs = [] msrclist = [] assetdirs = [] + for dep in adeps: adir = assetdir(dep) + + # A. 获取: asset的变化情况 if adir: latestModified = os.path.getmtime(adir) for dirpath, dirnames, files in os.walk(adir): @@ -697,7 +877,9 @@ def scan_port(adbpath, pnlist, projlist): latestModified = max(latestModified, os.path.getmtime(fpath)) latestResModified = max(latestResModified, latestModified) if latestModified > lastBuild: - assetdirs.append(adir) + assetdirs.append(adir) # 整个asset dir都放在里面 + + # B. 获取 resource 的变化情况 rdir = resdir(dep) if rdir: for subd in os.listdir(rdir): @@ -706,12 +888,16 @@ def scan_port(adbpath, pnlist, projlist): fpath = os.path.join(rdir, subd, fn) if os.path.isfile(fpath) and not fn.startswith('.'): latestResModified = max(latestResModified, os.path.getmtime(fpath)) + + # 返回源码的修改时间,以及文件个数 (sdir, scount, smt) = srcdir2(dep, lastBuild=lastBuild, list=msrclist) if sdir: srcs.append(sdir) latestSrcModified = max(latestSrcModified, smt) resModified = latestResModified > lastBuild srcModified = latestSrcModified > lastBuild + + targets = '' if resModified and srcModified: targets = 'both /res and /src' @@ -720,41 +906,56 @@ def scan_port(adbpath, pnlist, projlist): elif srcModified: targets = '/src' else: - print('%s has no /res or /src changes'%(packagename)) + print('%s has no /res or /src changes' % (packagename)) exit(0) if is_gradle: - print('cast %s:%d as gradle project with %s changed (v%s)'%(packagename, port, targets, __version__)) + print('cast %s:%d as gradle project with %s changed (v%s)' % (packagename, port, targets, __version__)) else: - print('cast %s:%d as eclipse project with %s changed (v%s)'%(packagename, port, targets, __version__)) + print('cast %s:%d as eclipse project with %s changed (v%s)' % (packagename, port, targets, __version__)) # prepare to reset + # 如果代码修改了,直接进行: pcast if srcModified: - curl('http://127.0.0.1:%d/pcast'%port, ignoreError=True) + curl(URL_PCAST, ignoreError=True) if resModified: + + # build/lcast + # build/lcast/res + # build/lcast/res/values + # values/ids.xml + # values/public.xml + # binresdir = os.path.join(bindir, 'res') if not os.path.exists(os.path.join(binresdir, 'values')): os.makedirs(os.path.join(binresdir, 'values')) - data = curl('http://127.0.0.1:%d/ids.xml'%port,exitcode=8) + + + data = curl(URL_IDS, exitcode=8) with open(os.path.join(binresdir, 'values/ids.xml'), 'w') as fp: fp.write(data) - data = curl('http://127.0.0.1:%d/public.xml'%port,exitcode=9) + data = curl(URL_PUBLIC, exitcode=9) with open(os.path.join(binresdir, 'values/public.xml'), 'w') as fp: fp.write(data) - #Get the assets path + # Get the assets path apk_path = get_apk_path(dir) if apk_path: - assets_path = os.path.join(bindir,"assets") + # build/lcast/assets + assets_path = os.path.join(bindir, "assets") if os.path.isdir(assets_path): shutil.rmtree(assets_path) get_asset_from_apk(apk_path, bindir) + aaptpath = get_aapt(sdkdir) if not aaptpath: - print('aapt not found in %s/build-tools'%sdkdir) + print('aapt not found in %s/build-tools' % sdkdir) exit(10) + + + # 生成 res.zip aaptargs = [aaptpath, 'package', '-f', '--auto-add-overlay', '-F', os.path.join(bindir, 'res.zip')] aaptargs.append('-S') aaptargs.append(binresdir) @@ -781,14 +982,18 @@ def scan_port(adbpath, pnlist, projlist): aaptargs.append(manifestpath(dir)) aaptargs.append('-I') aaptargs.append(android_jar) - cexec(aaptargs,exitcode=18) + print("生成 res.zip文件:") + print(" ".join(aaptargs)) + cexec(aaptargs, exitcode=18) + + # 将 res.zip 推送到android手机 with open(os.path.join(bindir, 'res.zip'), 'rb') as fp: - curl('http://127.0.0.1:%d/pushres'%port, body=fp.read(),exitcode=11) - + curl(URL_PUSH_RES, body=fp.read(), exitcode=11) + if srcModified: - vmversion = curl('http://127.0.0.1:%d/vmversion'%port, ignoreError=True) - if vmversion==None: + vmversion = curl(URL_VM_VERSION, ignoreError=True) + if vmversion == None: vmversion = '' if vmversion.startswith('1'): print('cast dex to dalvik vm is not supported, you need ART in Android 5.0') @@ -798,8 +1003,9 @@ def scan_port(adbpath, pnlist, projlist): print('javac is required to compile java code, config your PATH to include javac') exit(12) - launcher = curl('http://127.0.0.1:%d/launcher'%port,exitcode = 13) + launcher = curl(URL_LAUNCH, exitcode=13) + # 获取所有的 jar 文件 classpath = [android_jar] for dep in adeps: dlib = libdir(dep) @@ -818,7 +1024,8 @@ def scan_port(adbpath, pnlist, projlist): maven_libs_cache = json.load(fp) except: pass - if maven_libs_cache.get('version') != 1 or not maven_libs_cache.get('from') or sorted(maven_libs_cache['from']) != sorted(maven_libs): + if maven_libs_cache.get('version') != 1 or not maven_libs_cache.get( + 'from') or sorted(maven_libs_cache['from']) != sorted(maven_libs): if os.path.isfile(maven_libs_cache_file): os.remove(maven_libs_cache_file) maven_libs_cache = {} @@ -827,7 +1034,7 @@ def scan_port(adbpath, pnlist, projlist): maven_jars = maven_libs_cache.get('jars') elif maven_libs: maven_jars = get_maven_jars(maven_libs) - cache = {'version':1, 'from':maven_libs, 'jars':maven_jars} + cache = {'version': 1, 'from': maven_libs, 'jars': maven_jars} try: with open(maven_libs_cache_file, 'w') as fp: json.dump(cache, fp) @@ -845,7 +1052,9 @@ def scan_port(adbpath, pnlist, projlist): if fn.endswith('.jar'): classpath.append(os.path.join(dirpath, fn)) # R.class - classesdir = search_path(os.path.join(dir, 'build', 'intermediates', 'classes'), launcher and launcher.replace('.', os.path.sep)+'.class' or '$') + classesdir = search_path(os.path.join(dir, 'build', 'intermediates', 'classes'), + launcher and launcher.replace('.', + os.path.sep) + '.class' or '$') classpath.append(classesdir) else: # R.class @@ -863,6 +1072,8 @@ def scan_port(adbpath, pnlist, projlist): javacargs.append('-sourcepath') javacargs.append(os.pathsep.join(srcs)) javacargs.extend(msrclist) + + # remove all cache if javac fail def remove_cache_and_exit(args, code, stdout, stderr): if code: @@ -870,11 +1081,15 @@ def remove_cache_and_exit(args, code, stdout, stderr): if os.path.isfile(maven_libs_cache_file): os.remove(maven_libs_cache_file) cexec_fail_exit(args, code, stdout, stderr) - cexec(javacargs, callback=remove_cache_and_exit,exitcode=19) + + print("javacargs: ") + print(" ".join(javacargs)) + + cexec(javacargs, callback=remove_cache_and_exit, exitcode=19) dxpath = get_dx(sdkdir) if not dxpath: - print('dx not found in %s/build-tools'%sdkdir) + print('dx not found in %s/build-tools' % sdkdir) exit(14) dxoutput = os.path.join(bindir, 'classes.dex') if os.path.isfile(dxoutput): @@ -883,10 +1098,16 @@ def remove_cache_and_exit(args, code, stdout, stderr): if os.name == 'nt': # fix system32 java.exe issue addPath = os.path.abspath(os.path.join(javac, os.pardir)) - cexec([dxpath, '--dex', '--output=%s'%dxoutput, binclassesdir], addPath = addPath,exitcode=20) + print("Dex Create: ") + print(" ".join([dxpath, '--dex', '--output=%s' % dxoutput, binclassesdir])) + + cexec([dxpath, '--dex', '--output=%s' % dxoutput, binclassesdir], addPath=addPath, + exitcode=20) + + # 推送代码 with open(dxoutput, 'rb') as fp: - curl('http://127.0.0.1:%d/pushdex'%port, body=fp.read(),exitcode=15) + curl(URL_PUSH_DEX, body=fp.read(), exitcode=15) else: if is_gradle: @@ -894,9 +1115,11 @@ def remove_cache_and_exit(args, code, stdout, stderr): else: print('libs/lcast.jar is out of date, please update') - curl('http://127.0.0.1:%d/lcast'%port, ignoreError=True) + # lcast + curl(URL_LCAST, ignoreError=True) - cexec([adbpath, 'forward', '--remove', 'tcp:%d'%port], callback=None) + # 工作结束 + cexec([adbpath, 'forward', '--remove', 'tcp:%d' % port], callback=None) elapsetime = time.time() - starttime - print('finished in %dms'%(elapsetime*1000)) \ No newline at end of file + print('finished in %dms' % (elapsetime * 1000)) diff --git a/library/src/com/github/mmin18/layoutcast/LayoutCast.java b/library/src/com/github/mmin18/layoutcast/LayoutCast.java index 71cdb9f..82074be 100644 --- a/library/src/com/github/mmin18/layoutcast/LayoutCast.java +++ b/library/src/com/github/mmin18/layoutcast/LayoutCast.java @@ -42,6 +42,7 @@ public static void init(Context context) { opt.mkdirs(); final String vmVersion = System.getProperty("java.vm.version"); if (vmVersion != null && vmVersion.startsWith("2")) { + // 直接将新的dex文件放在class loader的dex列表之前 ArtUtils.overrideClassLoader(app.getClassLoader(), f, opt); } else { Log.e("lcast", "cannot cast dex to daivik, only support ART now."); diff --git a/library/src/com/github/mmin18/layoutcast/ResetActivity.java b/library/src/com/github/mmin18/layoutcast/ResetActivity.java index b911f1f..0c4ce6e 100644 --- a/library/src/com/github/mmin18/layoutcast/ResetActivity.java +++ b/library/src/com/github/mmin18/layoutcast/ResetActivity.java @@ -22,6 +22,8 @@ public class ResetActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // 提示正在Cast DEX TextView tv = new TextView(this); tv.setGravity(Gravity.CENTER); tv.setText("Cast DEX in 2 second.."); @@ -54,6 +56,7 @@ public void reset() { private final Runnable reset = new Runnable() { @Override public void run() { + // 自杀之后呢? android.os.Process.killProcess(Process.myPid()); } }; diff --git a/library/src/com/github/mmin18/layoutcast/server/LcastServer.java b/library/src/com/github/mmin18/layoutcast/server/LcastServer.java index 8644e07..24f28d2 100644 --- a/library/src/com/github/mmin18/layoutcast/server/LcastServer.java +++ b/library/src/com/github/mmin18/layoutcast/server/LcastServer.java @@ -37,273 +37,301 @@ * @author mmin18 */ public class LcastServer extends EmbedHttpServer { - public static final int PORT_FROM = 41128; - public static Application app; - final Context context; - File latestPushFile; + public static final int PORT_FROM = 41128; + public static Application app; + final Context context; + File latestPushFile; - private LcastServer(Context ctx, int port) { - super(port); - context = ctx; - } + private LcastServer(Context ctx, int port) { + super(port); + context = ctx; + } - @Override - protected void handle(String method, String path, - HashMap headers, InputStream input, - ResponseOutputStream response) throws Exception { - if (path.equalsIgnoreCase("/packagename")) { - response.setContentTypeText(); - response.write(context.getPackageName().getBytes("utf-8")); - return; - } - if (path.equalsIgnoreCase("/appstate")) { - response.setContentTypeText(); - response.write(String.valueOf(OverrideContext.getApplicationState()).getBytes("utf-8")); - return; - } - if ("/vmversion".equalsIgnoreCase(path)) { - final String vmVersion = System.getProperty("java.vm.version"); - response.setContentTypeText(); - if (vmVersion == null) { - response.write('0'); - } else { - response.write(vmVersion.getBytes("utf-8")); - } - return; - } - if ("/launcher".equalsIgnoreCase(path)) { - PackageManager pm = app.getPackageManager(); - Intent i = new Intent(Intent.ACTION_MAIN); - i.addCategory(Intent.CATEGORY_LAUNCHER); - i.setPackage(app.getPackageName()); - ResolveInfo ri = pm.resolveActivity(i, 0); - i = new Intent(Intent.ACTION_MAIN); - i.addCategory(Intent.CATEGORY_LAUNCHER); - response.setContentTypeText(); - response.write(ri.activityInfo.name.getBytes("utf-8")); - return; - } - if (("post".equalsIgnoreCase(method) || "put".equalsIgnoreCase(method)) - && path.equalsIgnoreCase("/pushres")) { - File dir = new File(context.getCacheDir(), "lcast"); - dir.mkdir(); - File dex = new File(dir, "dex.ped"); - File file; - if (dex.length() > 0) { - file = new File(dir, "res.ped"); - } else { - file = new File(dir, Integer.toHexString((int) (System - .currentTimeMillis() / 100) & 0xfff) + ".apk"); - } - FileOutputStream fos = new FileOutputStream(file); - byte[] buf = new byte[4096]; - int l; - while ((l = input.read(buf)) != -1) { - fos.write(buf, 0, l); - } - fos.close(); - latestPushFile = file; - response.setStatusCode(201); - Log.d("lcast", "lcast resources file received (" + file.length() - + " bytes): " + file); - return; - } - if (("post".equalsIgnoreCase(method) || "put".equalsIgnoreCase(method)) - && path.equalsIgnoreCase("/pushdex")) { - File dir = new File(context.getCacheDir(), "lcast"); - dir.mkdir(); - File file = new File(dir, "dex.ped"); - FileOutputStream fos = new FileOutputStream(file); - byte[] buf = new byte[4096]; - int l; - while ((l = input.read(buf)) != -1) { - fos.write(buf, 0, l); - } - fos.close(); - response.setStatusCode(201); - Log.d("lcast", "lcast dex file received (" + file.length() + " bytes)"); - return; - } - if ("/pcast".equalsIgnoreCase(path)) { - LayoutCast.restart(false); - response.setStatusCode(200); - return; - } - if ("/lcast".equalsIgnoreCase(path)) { - File dir = new File(context.getCacheDir(), "lcast"); - File dex = new File(dir, "dex.ped"); - if (dex.length() > 0) { - if (latestPushFile != null) { - File f = new File(dir, "res.ped"); - latestPushFile.renameTo(f); - } - Log.i("lcast", "cast with dex changes, need to restart the process (activity stack will be reserved)"); - boolean b = LayoutCast.restart(true); - response.setStatusCode(b ? 200 : 500); - } else { - Resources res = ResUtils.getResources(app, latestPushFile); - OverrideContext.setGlobalResources(res); - response.setStatusCode(200); - response.write(String.valueOf(latestPushFile).getBytes("utf-8")); - Log.i("lcast", "cast with only res changes, just recreate the running activity."); - } - return; - } - if ("/reset".equalsIgnoreCase(path)) { - OverrideContext.setGlobalResources(null); - response.setStatusCode(200); - response.write("OK".getBytes("utf-8")); - return; - } - if ("/ids.xml".equalsIgnoreCase(path)) { - String Rn = app.getPackageName() + ".R"; - Class Rclazz = app.getClassLoader().loadClass(Rn); - String str = new IdProfileBuilder(context.getResources()) - .buildIds(Rclazz); - response.setStatusCode(200); - response.setContentTypeText(); - response.write(str.getBytes("utf-8")); - return; - } - if ("/public.xml".equalsIgnoreCase(path)) { - String Rn = app.getPackageName() + ".R"; - Class Rclazz = app.getClassLoader().loadClass(Rn); - String str = new IdProfileBuilder(context.getResources()) - .buildPublic(Rclazz); - response.setStatusCode(200); - response.setContentTypeText(); - response.write(str.getBytes("utf-8")); - return; - } - if ("/apkinfo".equalsIgnoreCase(path)) { - ApplicationInfo ai = app.getApplicationInfo(); - File apkFile = new File(ai.sourceDir); - JSONObject result = new JSONObject(); - result.put("size", apkFile.length()); - result.put("lastModified", apkFile.lastModified()); + @Override + protected void handle(String method, String path, + HashMap headers, InputStream input, + ResponseOutputStream response) throws Exception { + // 1. 获取Context的pacakgename + if (path.equalsIgnoreCase("/packagename")) { + response.setContentTypeText(); + // 范围当前Context的PackageName + response.write(context.getPackageName().getBytes("utf-8")); + return; + } - FileInputStream fis = new FileInputStream(apkFile); - MessageDigest md5 = MessageDigest.getInstance("MD5"); - byte[] buf = new byte[4096]; - int l; - while ((l = fis.read(buf)) != -1) { - md5.update(buf, 0, l); - } - fis.close(); + // 2. 获取App State + if (path.equalsIgnoreCase("/appstate")) { + response.setContentTypeText(); + response.write(String.valueOf(OverrideContext.getApplicationState()).getBytes("utf-8")); + return; + } - result.put("md5", byteArrayToHex(md5.digest())); - response.setStatusCode(200); - response.setContentTypeJson(); - response.write(result.toString().getBytes("utf-8")); - return; - } - if ("/apkraw".equalsIgnoreCase(path)) { - ApplicationInfo ai = app.getApplicationInfo(); - FileInputStream fis = new FileInputStream(ai.sourceDir); - response.setStatusCode(200); - response.setContentTypeBinary(); - byte[] buf = new byte[4096]; - int l; - while ((l = fis.read(buf)) != -1) { - response.write(buf, 0, l); - } - return; - } - if (path.startsWith("/fileinfo/")) { - ApplicationInfo ai = app.getApplicationInfo(); - File apkFile = new File(ai.sourceDir); + // 2. 获取 + // http://stackoverflow.com/questions/19830342/how-can-i-detect-the-android-runtime-dalvik-or-art + // https://source.android.com/devices/tech/dalvik/ + if ("/vmversion".equalsIgnoreCase(path)) { + final String vmVersion = System.getProperty("java.vm.version"); + response.setContentTypeText(); + if (vmVersion == null) { + response.write('0'); + } else { + response.write(vmVersion.getBytes("utf-8")); + } + return; + } + if ("/launcher".equalsIgnoreCase(path)) { + PackageManager pm = app.getPackageManager(); + Intent i = new Intent(Intent.ACTION_MAIN); + i.addCategory(Intent.CATEGORY_LAUNCHER); + i.setPackage(app.getPackageName()); + ResolveInfo ri = pm.resolveActivity(i, 0); - JarFile jarFile = new JarFile(apkFile); - JarEntry je = jarFile.getJarEntry(path.substring("/fileinfo/".length())); - InputStream ins = jarFile.getInputStream(je); - MessageDigest md5 = MessageDigest.getInstance("MD5"); - byte[] buf = new byte[4096]; - int l, n = 0; - while ((l = ins.read(buf)) != -1) { - md5.update(buf, 0, l); - n += l; - } - ins.close(); - jarFile.close(); + i = new Intent(Intent.ACTION_MAIN); + i.addCategory(Intent.CATEGORY_LAUNCHER); - JSONObject result = new JSONObject(); - result.put("size", n); - result.put("time", je.getTime()); - result.put("crc", je.getCrc()); - result.put("md5", byteArrayToHex(md5.digest())); + // 获取launcher的界面 + response.setContentTypeText(); + response.write(ri.activityInfo.name.getBytes("utf-8")); + return; + } - response.setStatusCode(200); - response.setContentTypeJson(); - response.write(result.toString().getBytes("utf-8")); - return; - } - if (path.startsWith("/fileraw/")) { - ApplicationInfo ai = app.getApplicationInfo(); - File apkFile = new File(ai.sourceDir); + // pushres 如何处理呢? + if (("post".equalsIgnoreCase(method) || "put".equalsIgnoreCase(method)) + && path.equalsIgnoreCase("/pushres")) { + File dir = new File(context.getCacheDir(), "lcast"); + dir.mkdir(); - JarFile jarFile = new JarFile(apkFile); - JarEntry je = jarFile.getJarEntry(path.substring("/fileraw/".length())); - InputStream ins = jarFile.getInputStream(je); + // lcast/ + // dex.ped + // res.ped + // xxxx.apk + File dex = new File(dir, "dex.ped"); + File file; + if (dex.length() > 0) { + file = new File(dir, "res.ped"); + } else { + file = new File(dir, Integer.toHexString((int) (System.currentTimeMillis() / 100) & 0xfff) + ".apk"); + } - response.setStatusCode(200); - response.setContentTypeBinary(); - byte[] buf = new byte[4096]; - int l; - while ((l = ins.read(buf)) != -1) { - response.write(buf, 0, l); - } - return; - } - super.handle(method, path, headers, input, response); - } + // 通过Http Post发送什么信息呢? + // dex.ped + // *.apk + FileOutputStream fos = new FileOutputStream(file); + byte[] buf = new byte[4096]; + int l; + while ((l = input.read(buf)) != -1) { + fos.write(buf, 0, l); + } + fos.close(); + latestPushFile = file; + response.setStatusCode(201); + Log.d("lcast", "lcast resources file received (" + file.length() + " bytes): " + file); + return; + } + if (("post".equalsIgnoreCase(method) || "put".equalsIgnoreCase(method)) && path.equalsIgnoreCase("/pushdex")) { + File dir = new File(context.getCacheDir(), "lcast"); + dir.mkdir(); - private static LcastServer runningServer; + // lcast/ + // dex.ped + // + File file = new File(dir, "dex.ped"); + FileOutputStream fos = new FileOutputStream(file); + byte[] buf = new byte[4096]; + int l; + while ((l = input.read(buf)) != -1) { + fos.write(buf, 0, l); + } + fos.close(); + response.setStatusCode(201); + Log.d("lcast", "lcast dex file received (" + file.length() + " bytes)"); + return; + } - public static void start(Context ctx) { - if (runningServer != null) { - Log.d("lcast", "lcast server is already running"); - return; - } + // 如何重启呢? + if ("/pcast".equalsIgnoreCase(path)) { + LayoutCast.restart(false); + response.setStatusCode(200); + return; + } + if ("/lcast".equalsIgnoreCase(path)) { + File dir = new File(context.getCacheDir(), "lcast"); + File dex = new File(dir, "dex.ped"); + if (dex.length() > 0) { + if (latestPushFile != null) { + File f = new File(dir, "res.ped"); + latestPushFile.renameTo(f); + } + Log.i("lcast", "cast with dex changes, need to restart the process (activity stack will be reserved)"); + boolean b = LayoutCast.restart(true); + response.setStatusCode(b ? 200 : 500); + } else { + Resources res = ResUtils.getResources(app, latestPushFile); + OverrideContext.setGlobalResources(res); + response.setStatusCode(200); + response.write(String.valueOf(latestPushFile).getBytes("utf-8")); + Log.i("lcast", "cast with only res changes, just recreate the running activity."); + } + return; + } + if ("/reset".equalsIgnoreCase(path)) { + OverrideContext.setGlobalResources(null); + response.setStatusCode(200); + response.write("OK".getBytes("utf-8")); + return; + } + if ("/ids.xml".equalsIgnoreCase(path)) { + String Rn = app.getPackageName() + ".R"; + Class Rclazz = app.getClassLoader().loadClass(Rn); + String str = new IdProfileBuilder(context.getResources()) + .buildIds(Rclazz); + response.setStatusCode(200); + response.setContentTypeText(); + response.write(str.getBytes("utf-8")); + return; + } + if ("/public.xml".equalsIgnoreCase(path)) { + String Rn = app.getPackageName() + ".R"; + Class Rclazz = app.getClassLoader().loadClass(Rn); + String str = new IdProfileBuilder(context.getResources()) + .buildPublic(Rclazz); + response.setStatusCode(200); + response.setContentTypeText(); + response.write(str.getBytes("utf-8")); + return; + } + if ("/apkinfo".equalsIgnoreCase(path)) { + ApplicationInfo ai = app.getApplicationInfo(); + File apkFile = new File(ai.sourceDir); + JSONObject result = new JSONObject(); + result.put("size", apkFile.length()); + result.put("lastModified", apkFile.lastModified()); - for (int i = 0; i < 100; i++) { - LcastServer s = new LcastServer(ctx, PORT_FROM + i); - try { - s.start(); - runningServer = s; - Log.d("lcast", "lcast server running on port " - + (PORT_FROM + i)); - break; - } catch (Exception e) { - } - } - } + FileInputStream fis = new FileInputStream(apkFile); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] buf = new byte[4096]; + int l; + while ((l = fis.read(buf)) != -1) { + md5.update(buf, 0, l); + } + fis.close(); - public static void cleanCache(Context ctx) { - File dir = new File(ctx.getCacheDir(), "lcast"); - File[] fs = dir.listFiles(); - if (fs != null) { - for (File f : fs) { - rm(f); - } - } - } + result.put("md5", byteArrayToHex(md5.digest())); + response.setStatusCode(200); + response.setContentTypeJson(); + response.write(result.toString().getBytes("utf-8")); + return; + } + if ("/apkraw".equalsIgnoreCase(path)) { + ApplicationInfo ai = app.getApplicationInfo(); + FileInputStream fis = new FileInputStream(ai.sourceDir); + response.setStatusCode(200); + response.setContentTypeBinary(); + byte[] buf = new byte[4096]; + int l; + while ((l = fis.read(buf)) != -1) { + response.write(buf, 0, l); + } + return; + } + if (path.startsWith("/fileinfo/")) { + ApplicationInfo ai = app.getApplicationInfo(); + File apkFile = new File(ai.sourceDir); - private static void rm(File f) { - if (f.isDirectory()) { - for (File ff : f.listFiles()) { - rm(ff); - } - f.delete(); - } else if (f.getName().endsWith(".apk")) { - f.delete(); - } - } + JarFile jarFile = new JarFile(apkFile); + JarEntry je = jarFile.getJarEntry(path.substring("/fileinfo/".length())); + InputStream ins = jarFile.getInputStream(je); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] buf = new byte[4096]; + int l, n = 0; + while ((l = ins.read(buf)) != -1) { + md5.update(buf, 0, l); + n += l; + } + ins.close(); + jarFile.close(); - private static String byteArrayToHex(byte[] a) { - StringBuilder sb = new StringBuilder(a.length * 2); - for (byte b : a) - sb.append(String.format("%02x", b & 0xff)); - return sb.toString(); - } + JSONObject result = new JSONObject(); + result.put("size", n); + result.put("time", je.getTime()); + result.put("crc", je.getCrc()); + result.put("md5", byteArrayToHex(md5.digest())); + + response.setStatusCode(200); + response.setContentTypeJson(); + response.write(result.toString().getBytes("utf-8")); + return; + } + if (path.startsWith("/fileraw/")) { + ApplicationInfo ai = app.getApplicationInfo(); + File apkFile = new File(ai.sourceDir); + + JarFile jarFile = new JarFile(apkFile); + JarEntry je = jarFile.getJarEntry(path.substring("/fileraw/".length())); + InputStream ins = jarFile.getInputStream(je); + + response.setStatusCode(200); + response.setContentTypeBinary(); + byte[] buf = new byte[4096]; + int l; + while ((l = ins.read(buf)) != -1) { + response.write(buf, 0, l); + } + return; + } + super.handle(method, path, headers, input, response); + } + + private static LcastServer runningServer; + + public static void start(Context ctx) { + if (runningServer != null) { + Log.d("lcast", "lcast server is already running"); + return; + } + + // 如何启动一个Server呢? + // 在指定的返回内监听端口 + for (int i = 0; i < 100; i++) { + LcastServer s = new LcastServer(ctx, PORT_FROM + i); + try { + s.start(); + runningServer = s; + Log.d("lcast", "lcast server running on port " + (PORT_FROM + i)); + break; + } catch (Exception e) { + } + } + } + + // 删除: lcast内的所有的文件 + public static void cleanCache(Context ctx) { + File dir = new File(ctx.getCacheDir(), "lcast"); + File[] fs = dir.listFiles(); + if (fs != null) { + for (File f : fs) { + rm(f); + } + } + } + + // 递归删除文件 + private static void rm(File f) { + if (f.isDirectory()) { + for (File ff : f.listFiles()) { + rm(ff); + } + f.delete(); + } else if (f.getName().endsWith(".apk")) { + f.delete(); + } + } + + private static String byteArrayToHex(byte[] a) { + StringBuilder sb = new StringBuilder(a.length * 2); + for (byte b : a) + sb.append(String.format("%02x", b & 0xff)); + return sb.toString(); + } } diff --git a/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java b/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java index e37bf1f..162e9ba 100644 --- a/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java +++ b/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java @@ -14,33 +14,52 @@ */ public class ArtUtils { - public static boolean overrideClassLoader(ClassLoader cl, File dex, File opt) { - try { - ClassLoader bootstrap = cl.getParent(); - Field fPathList = BaseDexClassLoader.class.getDeclaredField("pathList"); - fPathList.setAccessible(true); - Object pathList = fPathList.get(cl); - Class cDexPathList = bootstrap.loadClass("dalvik.system.DexPathList"); - Field fDexElements = cDexPathList.getDeclaredField("dexElements"); - fDexElements.setAccessible(true); - Object dexElements = fDexElements.get(pathList); - DexClassLoader cl2 = new DexClassLoader(dex.getAbsolutePath(), opt.getAbsolutePath(), null, bootstrap); - Object pathList2 = fPathList.get(cl2); - Object dexElements2 = fDexElements.get(pathList2); - Object element2 = Array.get(dexElements2, 0); - int n = Array.getLength(dexElements) + 1; - Object newDexElements = Array.newInstance(fDexElements.getType().getComponentType(), n); - Array.set(newDexElements, 0, element2); - for (int i = 0; i < n - 1; i++) { - Object element = Array.get(dexElements, i); - Array.set(newDexElements, i + 1, element); - } - fDexElements.set(pathList, newDexElements); - return true; - } catch (Exception e) { - Log.e("lcast", "fail to override classloader " + cl + " with " + dex, e); - return false; - } - } + public static boolean overrideClassLoader(ClassLoader cl, File dex, File opt) { + try { + // 重新定义: Class Loader? + ClassLoader bootstrap = cl.getParent(); + + // 1. 获取第一个 Field: pathList(修改: Accessible) + Field fPathList = BaseDexClassLoader.class.getDeclaredField("pathList"); + fPathList.setAccessible(true); + + Object pathList = fPathList.get(cl); + + + // ClassLoader --> Field pathList --> pathList + + // 2. 获取第二个 Field + Class cDexPathList = bootstrap.loadClass("dalvik.system.DexPathList"); + Field fDexElements = cDexPathList.getDeclaredField("dexElements"); + fDexElements.setAccessible(true); + + + Object dexElements = fDexElements.get(pathList); + + // 加载: dex + DexClassLoader cl2 = new DexClassLoader(dex.getAbsolutePath(), opt.getAbsolutePath(), null, bootstrap); + Object pathList2 = fPathList.get(cl2); + Object dexElements2 = fDexElements.get(pathList2); // 读取: dexElements + + // 读取新的: dexElements + Object element2 = Array.get(dexElements2, 0); + int n = Array.getLength(dexElements) + 1; + + + Object newDexElements = Array.newInstance(fDexElements.getType().getComponentType(), n); + Array.set(newDexElements, 0, element2); + + // 将: dexElements 拷贝到: newDexElements 后面 + for (int i = 0; i < n - 1; i++) { + Object element = Array.get(dexElements, i); + Array.set(newDexElements, i + 1, element); + } + fDexElements.set(pathList, newDexElements); + return true; + } catch (Exception e) { + Log.e("lcast", "fail to override classloader " + cl + " with " + dex, e); + return false; + } + } } diff --git a/library/src/com/github/mmin18/layoutcast/util/EmbedHttpServer.java b/library/src/com/github/mmin18/layoutcast/util/EmbedHttpServer.java index 16319d4..1641b55 100644 --- a/library/src/com/github/mmin18/layoutcast/util/EmbedHttpServer.java +++ b/library/src/com/github/mmin18/layoutcast/util/EmbedHttpServer.java @@ -7,376 +7,379 @@ import java.net.Socket; import java.util.HashMap; +// 嵌入式的Http Server public class EmbedHttpServer implements Runnable { - private int port; - private ServerSocket serverSocket; - - public EmbedHttpServer(int port) { - this.port = port; - } - - public void start() throws IOException { - if (serverSocket == null) { - serverSocket = new ServerSocket(port); - new Thread(this, "embed-http-server").start(); - } - } - - public void stop() throws IOException { - if (serverSocket != null) { - serverSocket.close(); - serverSocket = null; - } - } - - protected void handle(String method, String path, - HashMap headers, InputStream input, - ResponseOutputStream response) throws Exception { - } - - @Override - public void run() { - final ServerSocket ss = serverSocket; - while (ss == serverSocket) { - Socket conn = null; - try { - conn = ss.accept(); - String method = null; - String path = null; - HashMap headers = new HashMap(); - - InputStream ins = conn.getInputStream(); - StringBuilder sb = new StringBuilder(512); - int l; - while ((l = ins.read()) != -1) { - if (l == '\n') { - if (sb.length() > 0 - && sb.charAt(sb.length() - 1) == '\r') - sb.setLength(sb.length() - 1); - if (sb.length() == 0) { - // header end - break; - } else if (method == null) { - int i = sb.indexOf(" "); - method = sb.substring(0, i); - int j = sb.lastIndexOf(" HTTP/"); - path = sb.substring(i + 1, j).trim(); - } else { - int i = sb.indexOf(":"); - String name = sb.substring(0, i).trim(); - String val = sb.substring(i + 1).trim(); - headers.put(name, val); - } - sb.setLength(0); - } else { - sb.append((char) l); - } - } - int contentLength = 0; - String str = headers.get("Content-Length"); - if (str != null) { - contentLength = Integer.parseInt(str); - } - OutputStream os = conn.getOutputStream(); - str = headers.get("Expect"); - if ("100-Continue".equalsIgnoreCase(str)) { - os.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes("ASCII")); - os.flush(); - } - BodyInputStream input = new BodyInputStream(ins, contentLength); - ResponseOutputStream response = new ResponseOutputStream(os); - handle(method, path, headers, input, response); - response.close(); - - conn.close(); - conn = null; - } catch (Exception e) { - if (conn != null) { - try { - conn.close(); - } catch (Exception ee) { - } - } - } - - if (!ss.isBound() || ss.isClosed()) { - serverSocket = null; - } - } - } - - private static class BodyInputStream extends InputStream { - private InputStream ins; - private int n; - - public BodyInputStream(InputStream ins, int n) { - this.ins = ins; - this.n = n; - } - - @Override - public int available() throws IOException { - return n; - } - - @Override - public int read() throws IOException { - if (n <= 0) - return -1; - int r = ins.read(); - if (r != -1) - n--; - return r; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (n <= 0) - return -1; - int l = ins.read(b, off, len < n ? len : n); - if (l != -1) - n -= l; - return l; - } - - @Override - public long skip(long n) throws IOException { - throw new IOException("unsupported"); - } - - @Override - public void close() throws IOException { - ins.close(); - } - - @Override - public synchronized void mark(int readlimit) { - throw new UnsupportedOperationException(); - } - - @Override - public synchronized void reset() throws IOException { - throw new IOException("unsupported"); - } - - @Override - public boolean markSupported() { - return false; - } - } - - public static class ResponseOutputStream extends OutputStream { - private static final byte[] CRLF = { (byte) '\r', (byte) '\n' }; - private OutputStream os; - private int lv; // 0:statusLine, 1:headers, 2:body, 3:closed - - public ResponseOutputStream(OutputStream os) { - this.os = os; - } - - public void setStatusCode(int statusCode) throws IOException { - switch (statusCode) { - case 200: - setStatusLine("200 OK"); - break; - case 201: - setStatusLine("201 Created"); - break; - case 202: - setStatusLine("202 Accepted"); - break; - case 301: - setStatusLine("301 Moved Permanently"); - break; - case 304: - setStatusLine("304 Not Modified"); - break; - case 400: - setStatusLine("400 Bad Request"); - break; - case 401: - setStatusLine("401 Unauthorized"); - break; - case 403: - setStatusLine("403 Forbidden"); - break; - case 404: - setStatusLine("404 Not Found"); - break; - case 405: - setStatusLine("405 Method Not Allowed"); - break; - case 500: - setStatusLine("500 Internal Server Error"); - break; - case 501: - setStatusLine("501 Not Implemented"); - break; - default: - setStatusLine(String.valueOf(statusCode)); - break; - } - } - - /** - * like "200 OK" - */ - public void setStatusLine(String statusLine) throws IOException { - if (lv == 0) { - os.write("HTTP/1.1 ".getBytes("ASCII")); - os.write(statusLine.getBytes("ASCII")); - os.write(CRLF); - lv = 1; - } else { - throw new IOException("status line is already set"); - } - } - - public void setHeader(String name, String value) throws IOException { - if (lv < 1) { - setStatusCode(200); - } - if (lv == 1) { - os.write(name.getBytes("ASCII")); - os.write(':'); - os.write(' '); - os.write(value.getBytes("ASCII")); - os.write(CRLF); - } else { - throw new IOException("headers is already set"); - } - } - - /** - * probably set if has body - */ - public void setContentLength(int value) throws IOException { - setHeader("Content-Length", String.valueOf(value)); - } - - /** - * like gzip - */ - public void setContentEncoding(String value) throws IOException { - setHeader("Content-Encoding", value); - } - - public void setContentType(String value) throws IOException { - setHeader("Content-Type", value); - } - - /** - * Content-Type: text/plain - */ - public void setContentTypeText() throws IOException { - setContentType("text/plain"); - } - - /** - * Content-Type: text/plain; charset=utf-8 - */ - public void setContentTypeTextUtf8() throws IOException { - setContentType("text/plain; charset=utf-8"); - } - - /** - * Content-Type: text/html - */ - public void setContentTypeHtml() throws IOException { - setContentType("text/html"); - } - - /** - * Content-Type: text/html; charset=utf-8 - */ - public void setContentTypeHtmlUtf8() throws IOException { - setContentType("text/html; charset=utf-8"); - } - - /** - * Content-Type: application/octet-stream - */ - public void setContentTypeBinary() throws IOException { - setContentType("application/octet-stream"); - } - - /** - * Content-Type: application/json - */ - public void setContentTypeJson() throws IOException { - setContentType("application/json"); - } - - /** - * Content-Type: text/xml - */ - public void setContentTypeXml() throws IOException { - setContentType("text/xml"); - } - - /** - * Content-Type: application/zip - */ - public void setContentTypeZip() throws IOException { - setContentType("application/zip"); - } - - /** - * Content-Type: image/jpeg - */ - public void setContentTypeJpeg() throws IOException { - setContentType("image/jpeg"); - } - - /** - * Content-Type: image/png - */ - public void setContentTypePng() throws IOException { - setContentType("image/png"); - } - - @Override - public void write(int b) throws IOException { - if (lv < 1) { - setStatusCode(200); - } - if (lv < 2) { - os.write(CRLF); - lv = 2; - } - os.write(b); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - if (lv < 1) { - setStatusCode(200); - } - if (lv < 2) { - os.write(CRLF); - lv = 2; - } - os.write(b, off, len); - } - - @Override - public void flush() throws IOException { - os.flush(); - } - - @Override - public void close() throws IOException { - if (lv < 1) { - setStatusCode(404); - } - if (lv < 2) { - os.write(CRLF); - lv = 2; - } - if (lv < 3) { - os.close(); - lv = 3; - } - } - } + private int port; + private ServerSocket serverSocket; + + public EmbedHttpServer(int port) { + this.port = port; + } + + public void start() throws IOException { + if (serverSocket == null) { + serverSocket = new ServerSocket(port); + new Thread(this, "embed-http-server").start(); + } + } + + public void stop() throws IOException { + if (serverSocket != null) { + serverSocket.close(); + serverSocket = null; + } + } + + protected void handle(String method, String path, + HashMap headers, InputStream input, + ResponseOutputStream response) throws Exception { + // 哪个简单的Method, Path, headers, input, response, 进行简单的Http处理 + } + + @Override + public void run() { + final ServerSocket ss = serverSocket; + while (ss == serverSocket) { + Socket conn = null; + try { + // 1. Accept + conn = ss.accept(); + String method = null; + String path = null; + HashMap headers = new HashMap(); + + InputStream ins = conn.getInputStream(); + StringBuilder sb = new StringBuilder(512); + int l; + while ((l = ins.read()) != -1) { + if (l == '\n') { + if (sb.length() > 0 + && sb.charAt(sb.length() - 1) == '\r') + sb.setLength(sb.length() - 1); + if (sb.length() == 0) { + // header end + break; + } else if (method == null) { + int i = sb.indexOf(" "); + method = sb.substring(0, i); + int j = sb.lastIndexOf(" HTTP/"); + path = sb.substring(i + 1, j).trim(); + } else { + int i = sb.indexOf(":"); + String name = sb.substring(0, i).trim(); + String val = sb.substring(i + 1).trim(); + headers.put(name, val); + } + sb.setLength(0); + } else { + sb.append((char) l); + } + } + int contentLength = 0; + String str = headers.get("Content-Length"); + if (str != null) { + contentLength = Integer.parseInt(str); + } + OutputStream os = conn.getOutputStream(); + str = headers.get("Expect"); + if ("100-Continue".equalsIgnoreCase(str)) { + os.write("HTTP/1.1 100 Continue\r\n\r\n".getBytes("ASCII")); + os.flush(); + } + BodyInputStream input = new BodyInputStream(ins, contentLength); + ResponseOutputStream response = new ResponseOutputStream(os); + handle(method, path, headers, input, response); + response.close(); + + conn.close(); + conn = null; + } catch (Exception e) { + if (conn != null) { + try { + conn.close(); + } catch (Exception ee) { + } + } + } + + if (!ss.isBound() || ss.isClosed()) { + serverSocket = null; + } + } + } + + private static class BodyInputStream extends InputStream { + private InputStream ins; + private int n; + + public BodyInputStream(InputStream ins, int n) { + this.ins = ins; + this.n = n; + } + + @Override + public int available() throws IOException { + return n; + } + + @Override + public int read() throws IOException { + if (n <= 0) + return -1; + int r = ins.read(); + if (r != -1) + n--; + return r; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (n <= 0) + return -1; + int l = ins.read(b, off, len < n ? len : n); + if (l != -1) + n -= l; + return l; + } + + @Override + public long skip(long n) throws IOException { + throw new IOException("unsupported"); + } + + @Override + public void close() throws IOException { + ins.close(); + } + + @Override + public synchronized void mark(int readlimit) { + throw new UnsupportedOperationException(); + } + + @Override + public synchronized void reset() throws IOException { + throw new IOException("unsupported"); + } + + @Override + public boolean markSupported() { + return false; + } + } + + public static class ResponseOutputStream extends OutputStream { + private static final byte[] CRLF = {(byte) '\r', (byte) '\n'}; + private OutputStream os; + private int lv; // 0:statusLine, 1:headers, 2:body, 3:closed + + public ResponseOutputStream(OutputStream os) { + this.os = os; + } + + public void setStatusCode(int statusCode) throws IOException { + switch (statusCode) { + case 200: + setStatusLine("200 OK"); + break; + case 201: + setStatusLine("201 Created"); + break; + case 202: + setStatusLine("202 Accepted"); + break; + case 301: + setStatusLine("301 Moved Permanently"); + break; + case 304: + setStatusLine("304 Not Modified"); + break; + case 400: + setStatusLine("400 Bad Request"); + break; + case 401: + setStatusLine("401 Unauthorized"); + break; + case 403: + setStatusLine("403 Forbidden"); + break; + case 404: + setStatusLine("404 Not Found"); + break; + case 405: + setStatusLine("405 Method Not Allowed"); + break; + case 500: + setStatusLine("500 Internal Server Error"); + break; + case 501: + setStatusLine("501 Not Implemented"); + break; + default: + setStatusLine(String.valueOf(statusCode)); + break; + } + } + + /** + * like "200 OK" + */ + public void setStatusLine(String statusLine) throws IOException { + if (lv == 0) { + os.write("HTTP/1.1 ".getBytes("ASCII")); + os.write(statusLine.getBytes("ASCII")); + os.write(CRLF); + lv = 1; + } else { + throw new IOException("status line is already set"); + } + } + + public void setHeader(String name, String value) throws IOException { + if (lv < 1) { + setStatusCode(200); + } + if (lv == 1) { + os.write(name.getBytes("ASCII")); + os.write(':'); + os.write(' '); + os.write(value.getBytes("ASCII")); + os.write(CRLF); + } else { + throw new IOException("headers is already set"); + } + } + + /** + * probably set if has body + */ + public void setContentLength(int value) throws IOException { + setHeader("Content-Length", String.valueOf(value)); + } + + /** + * like gzip + */ + public void setContentEncoding(String value) throws IOException { + setHeader("Content-Encoding", value); + } + + public void setContentType(String value) throws IOException { + setHeader("Content-Type", value); + } + + /** + * Content-Type: text/plain + */ + public void setContentTypeText() throws IOException { + setContentType("text/plain"); + } + + /** + * Content-Type: text/plain; charset=utf-8 + */ + public void setContentTypeTextUtf8() throws IOException { + setContentType("text/plain; charset=utf-8"); + } + + /** + * Content-Type: text/html + */ + public void setContentTypeHtml() throws IOException { + setContentType("text/html"); + } + + /** + * Content-Type: text/html; charset=utf-8 + */ + public void setContentTypeHtmlUtf8() throws IOException { + setContentType("text/html; charset=utf-8"); + } + + /** + * Content-Type: application/octet-stream + */ + public void setContentTypeBinary() throws IOException { + setContentType("application/octet-stream"); + } + + /** + * Content-Type: application/json + */ + public void setContentTypeJson() throws IOException { + setContentType("application/json"); + } + + /** + * Content-Type: text/xml + */ + public void setContentTypeXml() throws IOException { + setContentType("text/xml"); + } + + /** + * Content-Type: application/zip + */ + public void setContentTypeZip() throws IOException { + setContentType("application/zip"); + } + + /** + * Content-Type: image/jpeg + */ + public void setContentTypeJpeg() throws IOException { + setContentType("image/jpeg"); + } + + /** + * Content-Type: image/png + */ + public void setContentTypePng() throws IOException { + setContentType("image/png"); + } + + @Override + public void write(int b) throws IOException { + if (lv < 1) { + setStatusCode(200); + } + if (lv < 2) { + os.write(CRLF); + lv = 2; + } + os.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (lv < 1) { + setStatusCode(200); + } + if (lv < 2) { + os.write(CRLF); + lv = 2; + } + os.write(b, off, len); + } + + @Override + public void flush() throws IOException { + os.flush(); + } + + @Override + public void close() throws IOException { + if (lv < 1) { + setStatusCode(404); + } + if (lv < 2) { + os.write(CRLF); + lv = 2; + } + if (lv < 3) { + os.close(); + lv = 3; + } + } + } } diff --git a/sample-androidstudio/CastGradleTest/src/main/AndroidManifest.xml b/sample-androidstudio/CastGradleTest/src/main/AndroidManifest.xml index 9b1c717..9199fb6 100644 --- a/sample-androidstudio/CastGradleTest/src/main/AndroidManifest.xml +++ b/sample-androidstudio/CastGradleTest/src/main/AndroidManifest.xml @@ -1,18 +1,18 @@ + package="com.github.mmin18.layoutcast.gradle"> + android:theme="@style/AppTheme"> + android:label="@string/app_name"> diff --git a/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml b/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml index 955d532..a04dc0c 100644 --- a/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml +++ b/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml @@ -12,11 +12,20 @@ android:padding="6dp" android:text="@string/hello_world"/> + + + android:layout_below="@+id/text2"> From 63379bf957ad0bde931333103d9a45dc62f6d79e Mon Sep 17 00:00:00 2001 From: feiwang Date: Mon, 11 Apr 2016 10:55:37 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../layoutcast/context/OverrideContext.java | 3 +- .../layoutcast/server/IdProfileBuilder.java | 7 ++-- .../mmin18/layoutcast/server/LcastServer.java | 38 +++++++++++++------ 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java b/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java index 1412891..8799178 100644 --- a/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java +++ b/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java @@ -79,8 +79,7 @@ public static OverrideContext override(ContextWrapper orig, Resources res) fBase.set(orig, oc); } - Field fResources = ContextThemeWrapper.class - .getDeclaredField("mResources"); + Field fResources = ContextThemeWrapper.class.getDeclaredField("mResources"); fResources.setAccessible(true); fResources.set(orig, null); diff --git a/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java b/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java index a13e752..5fa5cc2 100644 --- a/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java +++ b/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java @@ -33,11 +33,10 @@ public String buildIds(Class Rclazz) throws Exception { } private void buildIds(StringBuilder out, Class clazz) throws Exception { + // 统计Ids的最大最小数值 int start = 0, end = 0; for (Field f : clazz.getDeclaredFields()) { - if (Integer.TYPE.equals(f.getType()) - && java.lang.reflect.Modifier.isStatic(f.getModifiers()) - && java.lang.reflect.Modifier.isPublic(f.getModifiers())) { + if (Integer.TYPE.equals(f.getType()) && java.lang.reflect.Modifier.isStatic(f.getModifiers()) && java.lang.reflect.Modifier.isPublic(f.getModifiers())) { int i = f.getInt(null); if ((i & 0x7f000000) == 0x7f000000) { if (start == 0 || i < start) { @@ -50,6 +49,8 @@ private void buildIds(StringBuilder out, Class clazz) throws Exception { } } + // Type + // EntryName for (int i = start; i > 0 && i <= end; i++) { out.append(" 0) { if (latestPushFile != null) { File f = new File(dir, "res.ped"); @@ -166,8 +174,11 @@ protected void handle(String method, String path, boolean b = LayoutCast.restart(true); response.setStatusCode(b ? 200 : 500); } else { + + // 没有代码的修改 Resources res = ResUtils.getResources(app, latestPushFile); OverrideContext.setGlobalResources(res); + response.setStatusCode(200); response.write(String.valueOf(latestPushFile).getBytes("utf-8")); Log.i("lcast", "cast with only res changes, just recreate the running activity."); @@ -183,23 +194,23 @@ protected void handle(String method, String path, if ("/ids.xml".equalsIgnoreCase(path)) { String Rn = app.getPackageName() + ".R"; Class Rclazz = app.getClassLoader().loadClass(Rn); - String str = new IdProfileBuilder(context.getResources()) - .buildIds(Rclazz); + String str = new IdProfileBuilder(context.getResources()).buildIds(Rclazz); response.setStatusCode(200); response.setContentTypeText(); response.write(str.getBytes("utf-8")); return; } + if ("/public.xml".equalsIgnoreCase(path)) { String Rn = app.getPackageName() + ".R"; Class Rclazz = app.getClassLoader().loadClass(Rn); - String str = new IdProfileBuilder(context.getResources()) - .buildPublic(Rclazz); + String str = new IdProfileBuilder(context.getResources()).buildPublic(Rclazz); response.setStatusCode(200); response.setContentTypeText(); response.write(str.getBytes("utf-8")); return; } + if ("/apkinfo".equalsIgnoreCase(path)) { ApplicationInfo ai = app.getApplicationInfo(); File apkFile = new File(ai.sourceDir); @@ -222,6 +233,8 @@ protected void handle(String method, String path, response.write(result.toString().getBytes("utf-8")); return; } + + // 获取原始的apk数据 if ("/apkraw".equalsIgnoreCase(path)) { ApplicationInfo ai = app.getApplicationInfo(); FileInputStream fis = new FileInputStream(ai.sourceDir); @@ -234,6 +247,7 @@ protected void handle(String method, String path, } return; } + if (path.startsWith("/fileinfo/")) { ApplicationInfo ai = app.getApplicationInfo(); File apkFile = new File(ai.sourceDir); @@ -262,10 +276,12 @@ protected void handle(String method, String path, response.write(result.toString().getBytes("utf-8")); return; } + if (path.startsWith("/fileraw/")) { ApplicationInfo ai = app.getApplicationInfo(); File apkFile = new File(ai.sourceDir); + // 从jarFile中读取raw数据 JarFile jarFile = new JarFile(apkFile); JarEntry je = jarFile.getJarEntry(path.substring("/fileraw/".length())); InputStream ins = jarFile.getInputStream(je); From d3b891471a5af02ef8e0bbd22af27c15d94e8107 Mon Sep 17 00:00:00 2001 From: feiwang Date: Mon, 11 Apr 2016 11:03:14 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cast.py | 2 +- .../CastGradleTest/src/main/res/layout/fragment_main.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 cast.py diff --git a/cast.py b/cast.py old mode 100644 new mode 100755 index 0a8b601..56f6387 --- a/cast.py +++ b/cast.py @@ -1,5 +1,5 @@ +# -*- coding:utf-8 -*- #!/usr/bin/python - __author__ = 'mmin18' __version__ = '1.50922' __plugin__ = '1' diff --git a/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml b/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml index a04dc0c..e51a20e 100644 --- a/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml +++ b/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml @@ -19,7 +19,7 @@ android:layout_below="@id/text" android:layout_marginBottom="10dp" android:padding="6dp" - android:text="大家好, 感觉这个还是不错的"/> + android:text="大家好, 感觉这个还是不错的, 现在测试看看怎么样,开发模式很重要"/> Date: Mon, 11 Apr 2016 11:36:41 +0800 Subject: [PATCH 05/11] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BA=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cast.py | 193 ++++++++++-------- .../layoutcast/gradle/DetailActivity.java | 2 + .../src/main/res/layout/fragment_main.xml | 2 +- 3 files changed, 107 insertions(+), 90 deletions(-) diff --git a/cast.py b/cast.py index 56f6387..82742e9 100755 --- a/cast.py +++ b/cast.py @@ -1,10 +1,6 @@ -# -*- coding:utf-8 -*- #!/usr/bin/python -__author__ = 'mmin18' -__version__ = '1.50922' -__plugin__ = '1' - -from subprocess import Popen, PIPE, check_call +# -*- coding:utf-8 -*- +from subprocess import Popen, PIPE from distutils.version import LooseVersion import argparse import sys @@ -16,6 +12,12 @@ import json import zipfile +# 颜色高亮 +from colorama import init + +init() +from colorama import Fore, Back, Style + # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def is_exe(fpath): @@ -51,9 +53,16 @@ def cexec(args, callback=cexec_fail_exit, addPath=None, exitcode=1): env = None if addPath: import copy - env = copy.copy(os.environ) env['PATH'] = addPath + os.path.pathsep + env['PATH'] + + if args[0].endswith("aapt"): + print("--------\nCMD: %saapt %s%s %s\n--------" % (Fore.GREEN, args[1], Fore.RESET, " ".join(args[2:]))) + elif args[0].endswith("adb"): + print("CMD: %sadb%s %s" % (Fore.GREEN, Fore.RESET, " ".join(args[1:]))) + else: + print("CMD: %s" % (" ".join(args))) + p = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) output, err = p.communicate() code = p.returncode @@ -65,7 +74,7 @@ def cexec(args, callback=cexec_fail_exit, addPath=None, exitcode=1): def curl(url, body=None, ignoreError=False, exitcode=1): - print ("URL: ", url) + print ("URL: %s%s%s" % (Fore.MAGENTA, url, Fore.RESET)) import sys try: @@ -702,7 +711,7 @@ def get_maven_jars(libs): return jars -def scan_port(adbpath, pnlist, projlist): +def scan_port(adbpaths, pnlist, projlist): """ 返回可用的 <端口, project_dir, packagename> :param adbpath: @@ -716,7 +725,10 @@ def scan_port(adbpath, pnlist, projlist): for i in range(0, 10): try_port = (41128 + i) # 1. 通过adb做端口映射 - cexec([adbpath, 'forward', 'tcp:%d' % try_port, 'tcp:%d' % try_port]) + command = [] + command.extend(adbpaths) + command.extend(['forward', 'tcp:%d' % try_port, 'tcp:%d' % try_port]) + cexec(command) # 2. 然后 pnlist # projlist @@ -738,7 +750,10 @@ def scan_port(adbpath, pnlist, projlist): # 删除多余的端口映射 for i in range(0, 10): if (41128 + i) != port: - cexec([adbpath, 'forward', '--remove', 'tcp:%d' % (41128 + i)], callback=None) + command = [] + command.extend(adbpaths) + command.extend(['forward', '--remove', 'tcp:%d' % (41128 + i)]) + cexec(command, callback=None) return port, prodir, packagename @@ -747,6 +762,7 @@ def scan_port(adbpath, pnlist, projlist): dir = '.' sdkdir = None jdkdir = None + device = None starttime = time.time() @@ -755,7 +771,8 @@ def scan_port(adbpath, pnlist, projlist): parser = argparse.ArgumentParser() parser.add_argument('--sdk', help='specify Android SDK path') parser.add_argument('--jdk', help='specify JDK path') - parser.add_argument('project') + parser.add_argument('--device', help='specify device') + parser.add_argument('--project', help="项目的目录, 默认为.") args = parser.parse_args() if args.sdk: sdkdir = args.sdk @@ -763,6 +780,8 @@ def scan_port(adbpath, pnlist, projlist): jdkdir = args.jdk if args.project: dir = args.project + if args.device: + device = args.device # 2. 获取有的 project list projlist = [i for i in list_projects(dir) if is_launchable_project(i)] @@ -789,9 +808,15 @@ def scan_port(adbpath, pnlist, projlist): print('adb not found in %s/platform-tools' % sdkdir) exit(4) + # 增加设备选择 + if device: + adbpaths = [adbpath, "-s", device] + else: + adbpaths = [adbpath] + # 1. 进行端口扫描 # 如果同时运行多个apk, 如何知道哪一个apk是当前正在调试的呢?) appstate - port, dir, packagename = scan_port(adbpath, pnlist, projlist) + port, dir, packagename = scan_port(adbpaths, pnlist, projlist) # 2. 没有启动(则继续尝试等待) if port == 0: @@ -799,20 +824,21 @@ def scan_port(adbpath, pnlist, projlist): # launch app latest_package = get_latest_packagename(projlist, sdkdir) if latest_package: - cexec([adbpath, 'shell', 'monkey', '-p', latest_package, '-c', 'android.intent.category.LAUNCHER', '1'], callback=None) + command = [] + command.extend(adbpaths) + command.extend(['shell', 'monkey', '-p', latest_package, '-c', 'android.intent.category.LAUNCHER', '1']) + cexec(command, callback=None) for i in range(0, 6): # try 6 times to wait the application launches - port, dir, packagename = scan_port(adbpath, pnlist, projlist) + port, dir, packagename = scan_port(adbpaths, pnlist, projlist) if port: break time.sleep(0.25) if port == 0: - print('package %s not found, make sure your project is properly setup and running' % ( - len(pnlist) == 1 and pnlist[0] or pnlist)) + print('package %s not found, make sure your project is properly setup and running' % (len(pnlist) == 1 and pnlist[0] or pnlist)) exit(5) - URL_LCAST = 'http://127.0.0.1:%d/lcast' % port URL_PUSH_DEX = 'http://127.0.0.1:%d/pushdex' % port URL_LAUNCH = 'http://127.0.0.1:%d/launcher' % port @@ -827,7 +853,7 @@ def scan_port(adbpath, pnlist, projlist): # 用于判断手机是否支持: ART URL_VM_VERSION = 'http://127.0.0.1:%d/vmversion' % port - is_gradle = is_gradle_project(dir) + # is_gradle = is_gradle_project(dir) android_jar = get_android_jar(sdkdir) if not android_jar: @@ -836,13 +862,14 @@ def scan_port(adbpath, pnlist, projlist): deps = deps_list(dir) # build/lcast - bindir = is_gradle and os.path.join(dir, 'build', 'lcast') or os.path.join(dir, 'bin', 'lcast') + bindir = os.path.join(dir, 'build', 'lcast') or os.path.join(dir, 'bin', 'lcast') # check if the /res and /src has changed lastBuild = 0 # 获取apk的路径(fpath, 以及build的时间) - rdir = is_gradle and os.path.join(dir, 'build', 'outputs', 'apk') or os.path.join(dir, 'bin') + # rdir = is_gradle and os.path.join(dir, 'build', 'outputs', 'apk') or os.path.join(dir, 'bin') + rdir = os.path.join(dir, 'build', 'outputs', 'apk') or os.path.join(dir, 'bin') if os.path.isdir(rdir): for fn in os.listdir(rdir): if fn.endswith('.apk') and not '-androidTest' in fn: @@ -897,7 +924,6 @@ def scan_port(adbpath, pnlist, projlist): resModified = latestResModified > lastBuild srcModified = latestSrcModified > lastBuild - targets = '' if resModified and srcModified: targets = 'both /res and /src' @@ -909,10 +935,8 @@ def scan_port(adbpath, pnlist, projlist): print('%s has no /res or /src changes' % (packagename)) exit(0) - if is_gradle: - print('cast %s:%d as gradle project with %s changed (v%s)' % (packagename, port, targets, __version__)) - else: - print('cast %s:%d as eclipse project with %s changed (v%s)' % (packagename, port, targets, __version__)) + print('cast %s:%d as gradle project with %s changed' % (packagename, port, targets)) + # prepare to reset # 如果代码修改了,直接进行: pcast @@ -931,8 +955,6 @@ def scan_port(adbpath, pnlist, projlist): if not os.path.exists(os.path.join(binresdir, 'values')): os.makedirs(os.path.join(binresdir, 'values')) - - data = curl(URL_IDS, exitcode=8) with open(os.path.join(binresdir, 'values/ids.xml'), 'w') as fp: fp.write(data) @@ -968,10 +990,11 @@ def scan_port(adbpath, pnlist, projlist): if rdir: aaptargs.append('-S') aaptargs.append(rdir) - if is_gradle: - for dep in reversed(list_aar_projects(dir, deps)): - aaptargs.append('-S') - aaptargs.append(dep) + + # 只处理: gradle 的情况 + for dep in reversed(list_aar_projects(dir, deps)): + aaptargs.append('-S') + aaptargs.append(dep) for assetdir in assetdirs: aaptargs.append('-A') aaptargs.append(assetdir) @@ -983,8 +1006,8 @@ def scan_port(adbpath, pnlist, projlist): aaptargs.append('-I') aaptargs.append(android_jar) - print("生成 res.zip文件:") - print(" ".join(aaptargs)) + + print(Fore.RED + "更新 res.zip 文件..." + Fore.RESET) cexec(aaptargs, exitcode=18) # 将 res.zip 推送到android手机 @@ -1003,6 +1026,7 @@ def scan_port(adbpath, pnlist, projlist): print('javac is required to compile java code, config your PATH to include javac') exit(12) + print(Fore.RED + "更新 classes.dex 文件..." + Fore.RESET) launcher = curl(URL_LAUNCH, exitcode=13) # 获取所有的 jar 文件 @@ -1013,52 +1037,48 @@ def scan_port(adbpath, pnlist, projlist): for fjar in os.listdir(dlib): if fjar.endswith('.jar'): classpath.append(os.path.join(dlib, fjar)) - if is_gradle: - # jars from maven cache - maven_libs = get_maven_libs(adeps) - maven_libs_cache_file = os.path.join(bindir, 'cache-javac-maven.json') - maven_libs_cache = {} + + # jars from maven cache + maven_libs = get_maven_libs(adeps) + maven_libs_cache_file = os.path.join(bindir, 'cache-javac-maven.json') + maven_libs_cache = {} + if os.path.isfile(maven_libs_cache_file): + try: + with open(maven_libs_cache_file, 'r') as fp: + maven_libs_cache = json.load(fp) + except: + pass + if maven_libs_cache.get('version') != 1 or not maven_libs_cache.get( + 'from') or sorted(maven_libs_cache['from']) != sorted(maven_libs): if os.path.isfile(maven_libs_cache_file): - try: - with open(maven_libs_cache_file, 'r') as fp: - maven_libs_cache = json.load(fp) - except: - pass - if maven_libs_cache.get('version') != 1 or not maven_libs_cache.get( - 'from') or sorted(maven_libs_cache['from']) != sorted(maven_libs): - if os.path.isfile(maven_libs_cache_file): - os.remove(maven_libs_cache_file) - maven_libs_cache = {} - maven_jars = [] - if maven_libs_cache: - maven_jars = maven_libs_cache.get('jars') - elif maven_libs: - maven_jars = get_maven_jars(maven_libs) - cache = {'version': 1, 'from': maven_libs, 'jars': maven_jars} - try: - with open(maven_libs_cache_file, 'w') as fp: - json.dump(cache, fp) - except: - pass - if maven_jars: - classpath.extend(maven_jars) - # aars from exploded-aar - darr = os.path.join(dir, 'build', 'intermediates', 'exploded-aar') - # TODO: use the max version - for dirpath, dirnames, files in os.walk(darr): - if re.findall(r'[/\\+]androidTest[/\\+]', dirpath) or '/.' in dirpath: - continue - for fn in files: - if fn.endswith('.jar'): - classpath.append(os.path.join(dirpath, fn)) - # R.class - classesdir = search_path(os.path.join(dir, 'build', 'intermediates', 'classes'), - launcher and launcher.replace('.', - os.path.sep) + '.class' or '$') - classpath.append(classesdir) - else: - # R.class - classpath.append(os.path.join(dir, 'bin', 'classes')) + os.remove(maven_libs_cache_file) + maven_libs_cache = {} + maven_jars = [] + if maven_libs_cache: + maven_jars = maven_libs_cache.get('jars') + elif maven_libs: + maven_jars = get_maven_jars(maven_libs) + cache = {'version': 1, 'from': maven_libs, 'jars': maven_jars} + try: + with open(maven_libs_cache_file, 'w') as fp: + json.dump(cache, fp) + except: + pass + if maven_jars: + classpath.extend(maven_jars) + # aars from exploded-aar + darr = os.path.join(dir, 'build', 'intermediates', 'exploded-aar') + # TODO: use the max version + for dirpath, dirnames, files in os.walk(darr): + if re.findall(r'[/\\+]androidTest[/\\+]', dirpath) or '/.' in dirpath: + continue + for fn in files: + if fn.endswith('.jar'): + classpath.append(os.path.join(dirpath, fn)) + # R.class + classesdir = search_path(os.path.join(dir, 'build', 'intermediates', 'classes'), + launcher and launcher.replace('.', os.path.sep) + '.class' or '$') + classpath.append(classesdir) binclassesdir = os.path.join(bindir, 'classes') shutil.rmtree(binclassesdir, ignore_errors=True) @@ -1082,8 +1102,6 @@ def remove_cache_and_exit(args, code, stdout, stderr): os.remove(maven_libs_cache_file) cexec_fail_exit(args, code, stdout, stderr) - print("javacargs: ") - print(" ".join(javacargs)) cexec(javacargs, callback=remove_cache_and_exit, exitcode=19) @@ -1099,9 +1117,6 @@ def remove_cache_and_exit(args, code, stdout, stderr): # fix system32 java.exe issue addPath = os.path.abspath(os.path.join(javac, os.pardir)) - print("Dex Create: ") - print(" ".join([dxpath, '--dex', '--output=%s' % dxoutput, binclassesdir])) - cexec([dxpath, '--dex', '--output=%s' % dxoutput, binclassesdir], addPath=addPath, exitcode=20) @@ -1110,16 +1125,16 @@ def remove_cache_and_exit(args, code, stdout, stderr): curl(URL_PUSH_DEX, body=fp.read(), exitcode=15) else: - if is_gradle: - print('LayoutCast library out of date, please sync your project with gradle') - else: - print('libs/lcast.jar is out of date, please update') + print('LayoutCast library out of date, please sync your project with gradle') # lcast curl(URL_LCAST, ignoreError=True) # 工作结束 - cexec([adbpath, 'forward', '--remove', 'tcp:%d' % port], callback=None) + command = [] + command.extend(adbpaths) + command.extend(['forward', '--remove', 'tcp:%d' % port]) + cexec(command, callback=None) elapsetime = time.time() - starttime print('finished in %dms' % (elapsetime * 1000)) diff --git a/sample-androidstudio/CastGradleTest/src/main/java/com/github/mmin18/layoutcast/gradle/DetailActivity.java b/sample-androidstudio/CastGradleTest/src/main/java/com/github/mmin18/layoutcast/gradle/DetailActivity.java index ef96931..8e891f8 100644 --- a/sample-androidstudio/CastGradleTest/src/main/java/com/github/mmin18/layoutcast/gradle/DetailActivity.java +++ b/sample-androidstudio/CastGradleTest/src/main/java/com/github/mmin18/layoutcast/gradle/DetailActivity.java @@ -14,6 +14,8 @@ public class DetailActivity extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Fresco.initialize(this); + // 添加注释 + // 独山大道 setContentView(R.layout.detail_view); ((SimpleDraweeView) findViewById(R.id.image)).setImageURI(getIntent().getData()); diff --git a/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml b/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml index e51a20e..3ed60c9 100644 --- a/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml +++ b/sample-androidstudio/CastGradleTest/src/main/res/layout/fragment_main.xml @@ -19,7 +19,7 @@ android:layout_below="@id/text" android:layout_marginBottom="10dp" android:padding="6dp" - android:text="大家好, 感觉这个还是不错的, 现在测试看看怎么样,开发模式很重要"/> + android:text="大家好, 感觉这个还是不错的, 现在测试看看怎么样"/> Date: Wed, 13 Apr 2016 10:59:01 +0800 Subject: [PATCH 06/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E9=98=85?= =?UTF-8?q?=E8=AF=BB=E7=AC=94=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- library/AndroidManifest.xml | 2 +- .../github/mmin18/layoutcast/LayoutCast.java | 17 ++++- .../layoutcast/context/OverrideContext.java | 64 +++++++++++++++---- .../layoutcast/inflater/BaseInflater.java | 6 ++ .../layoutcast/inflater/BootInflater.java | 43 +++++++------ .../mmin18/layoutcast/server/LcastServer.java | 3 +- .../mmin18/layoutcast/util/ArtUtils.java | 8 +++ 7 files changed, 104 insertions(+), 39 deletions(-) diff --git a/library/AndroidManifest.xml b/library/AndroidManifest.xml index 8b98372..e43c0cc 100644 --- a/library/AndroidManifest.xml +++ b/library/AndroidManifest.xml @@ -5,7 +5,7 @@ android:versionName="1.0" > \ No newline at end of file diff --git a/library/src/com/github/mmin18/layoutcast/LayoutCast.java b/library/src/com/github/mmin18/layoutcast/LayoutCast.java index 82074be..3390d86 100644 --- a/library/src/com/github/mmin18/layoutcast/LayoutCast.java +++ b/library/src/com/github/mmin18/layoutcast/LayoutCast.java @@ -19,30 +19,39 @@ public class LayoutCast { - private static boolean inited; + private static boolean inited; // 是否已经初始化 private static Context appContext; public static void init(Context context) { if (inited) return; - Application app = context instanceof Application ? (Application) context - : (Application) context.getApplicationContext(); + // 1. 如何获取Application呢? + // Application本身就是一个Context + // 一般的Context也和ApplicationContext有关联 + Application app = context instanceof Application ? (Application) context : (Application) context.getApplicationContext(); appContext = app; + // 2. 全新的启动,删除cache + // 删除全部的: cache_dir/lcast 目录下的apk文件 LcastServer.cleanCache(app); + File dir = new File(app.getCacheDir(), "lcast"); File dex = new File(dir, "dex.ped"); File res = new File(dir, "res.ped"); + // 1. 如果存在有效的: lcast/dex.ped 文件, 则修改为: lcast/dex.apk if (dex.length() > 0) { File f = new File(dir, "dex.apk"); dex.renameTo(f); + + // lcast/opt/ File opt = new File(dir, "opt"); opt.mkdirs(); final String vmVersion = System.getProperty("java.vm.version"); if (vmVersion != null && vmVersion.startsWith("2")) { // 直接将新的dex文件放在class loader的dex列表之前 + // 必须支持ART(Android 5.0以上吧) ArtUtils.overrideClassLoader(app.getClassLoader(), f, opt); } else { Log.e("lcast", "cannot cast dex to daivik, only support ART now."); @@ -50,6 +59,8 @@ public static void init(Context context) { } OverrideContext.initApplication(app); + + // 修改系统的InlaterService BootInflater.initApplication(app); if (res.length() > 0) { diff --git a/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java b/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java index 8799178..fa71579 100644 --- a/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java +++ b/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java @@ -15,6 +15,9 @@ import java.util.Map.Entry; import java.util.WeakHashMap; +// Context是什么概念呢? +// ContextWrapper又是如何工作的呢? ContextProxy: 相同的接口 +// public class OverrideContext extends ContextWrapper { private static final int STATE_REQUIRE_RECREATE = 5; @@ -24,17 +27,21 @@ public class OverrideContext extends ContextWrapper { private Theme theme; private int state; + // ContextWrapper(Context base) + // 这里的Context需要进行资源的托管 protected OverrideContext(Context base, Resources res) { super(base); this.base = base; this.resources = res; } + // AssetManager 的管理 @Override public AssetManager getAssets() { return resources == null ? base.getAssets() : resources.getAssets(); } + // Resource的管理(似乎Resource没有增量处理,都是全量的?) @Override public Resources getResources() { return resources == null ? base.getResources() : resources; @@ -45,6 +52,8 @@ public Theme getTheme() { if (resources == null) { return base.getTheme(); } + + // Theme的级联 if (theme == null) { theme = resources.newTheme(); theme.setTo(base.getTheme()); @@ -53,10 +62,13 @@ public Theme getTheme() { } protected void setResources(Resources res) { + // 替换资源 if (this.resources != res) { this.resources = res; this.theme = null; - // TODO: + + // 修改资源之后, + // TODO: this.state = STATE_REQUIRE_RECREATE; } } @@ -64,8 +76,13 @@ protected void setResources(Resources res) { /** * @param res set null to reset original resources */ - public static OverrideContext override(ContextWrapper orig, Resources res) + public static OverrideContext override(ContextThemeWrapper orig, Resources res) throws Exception { + + // 如何将当前的ContextWrapper override呢? + // ContextWrapper 本身我们不打算变化,而且还有外部引用 + // 只期待修改被它proxy的对象 + // Context base = orig.getBaseContext(); OverrideContext oc; if (base instanceof OverrideContext) { @@ -74,11 +91,16 @@ public static OverrideContext override(ContextWrapper orig, Resources res) } else { oc = new OverrideContext(base, res); + // orig.mBase = oc Field fBase = ContextWrapper.class.getDeclaredField("mBase"); fBase.setAccessible(true); fBase.set(orig, oc); } + // ContextThemeWrapper ? + // origin 的真实类型? + // 将orig.mResources 设置为 null origin.mTheme 设置为 null + // Field fResources = ContextThemeWrapper.class.getDeclaredField("mResources"); fResources.setAccessible(true); fResources.set(orig, null); @@ -93,17 +115,18 @@ public static OverrideContext override(ContextWrapper orig, Resources res) // // Activities // + public static void initApplication(Application app) { + // 监听App的各种LifeCycle + app.registerActivityLifecycleCallbacks(lifecycleCallback); + } public static final int ACTIVITY_NONE = 0; public static final int ACTIVITY_CREATED = 1; public static final int ACTIVITY_STARTED = 2; public static final int ACTIVITY_RESUMED = 3; - private static final WeakHashMap activities = new WeakHashMap(); - - public static void initApplication(Application app) { - app.registerActivityLifecycleCallbacks(lifecycleCallback); - } + // 自己维持一套: activities 的状态 + private static final WeakHashMap activities = new WeakHashMap(); public static Activity[] getAllActivities() { ArrayList list = new ArrayList(); for (Entry e : activities.entrySet()) { @@ -127,9 +150,10 @@ public static Activity getTopActivity() { } /** - * @return 0: no activities
+ * @return + * 0: no activities
* 1: activities has been paused
- * 2: activities is visible + * 2: activities is visible (有Activity可见,才能和cast.py交互) */ public static int getApplicationState() { int createdCount = 0; @@ -161,6 +185,10 @@ public static int getActivityState(Activity a) { } } + /** + * ACTIVITY_CREATED ---> ACTIVITY_STARTED ---> ACTIVITY_RESUMED + * + */ private static final Application.ActivityLifecycleCallbacks lifecycleCallback = new Application.ActivityLifecycleCallbacks() { @Override public void onActivityStopped(Activity activity) { @@ -191,12 +219,14 @@ public void onActivityPaused(Activity activity) { @Override public void onActivityDestroyed(Activity activity) { + // 不再监管activity activities.remove(activity); } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + // 只要不是: Started/Resumed, 就是Created activities.put(activity, ACTIVITY_CREATED); } }; @@ -204,11 +234,15 @@ public void onActivityCreated(Activity activity, // // State // - private static void checkActivityState(Activity activity) { if (activity.getBaseContext() instanceof OverrideContext) { + // 工作逻辑: + // 我们修改代码资源,一般是为了对TopMost Activity进行Debug OverrideContext oc = (OverrideContext) activity.getBaseContext(); - if (oc.state == STATE_REQUIRE_RECREATE) { + + // 如何recreate呢? + if (oc.state == STATE_REQUIRE_RECREATE) { + // 重建 activity.recreate(); } } @@ -217,14 +251,15 @@ private static void checkActivityState(Activity activity) { // // Global // - private static Resources overrideResources; public static void setGlobalResources(Resources res) throws Exception { overrideResources = res; - Exception err = null; + + Exception err = null; for (Activity a : getAllActivities()) { try { + // 修改所有的Acitivty的Resources override(a, res); } catch (Exception e) { err = e; @@ -239,13 +274,14 @@ public static void setGlobalResources(Resources res) throws Exception { a.runOnUiThread(new Runnable() { @Override public void run() { + // 修改资源之后只修改最新的状态 checkActivityState(a); } }); } } - public static OverrideContext overrideDefault(ContextWrapper orig) + public static OverrideContext overrideDefault(ContextThemeWrapper orig) throws Exception { return override(orig, overrideResources); } diff --git a/library/src/com/github/mmin18/layoutcast/inflater/BaseInflater.java b/library/src/com/github/mmin18/layoutcast/inflater/BaseInflater.java index e5776ff..5e6cfaa 100644 --- a/library/src/com/github/mmin18/layoutcast/inflater/BaseInflater.java +++ b/library/src/com/github/mmin18/layoutcast/inflater/BaseInflater.java @@ -20,8 +20,13 @@ protected BaseInflater(LayoutInflater original, Context newContext) { @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { + + // 如何创建View + // 例如给定: + // 这里的prefix作用? for (String prefix : sClassPrefixList) { try { + // 控制: prefix? View view = createView(name, prefix, attrs); if (view != null) { return view; @@ -35,6 +40,7 @@ protected View onCreateView(String name, AttributeSet attrs) return super.onCreateView(name, attrs); } + @Override public LayoutInflater cloneInContext(Context newContext) { return new BaseInflater(this, newContext); } diff --git a/library/src/com/github/mmin18/layoutcast/inflater/BootInflater.java b/library/src/com/github/mmin18/layoutcast/inflater/BootInflater.java index 36c2796..1bb5587 100644 --- a/library/src/com/github/mmin18/layoutcast/inflater/BootInflater.java +++ b/library/src/com/github/mmin18/layoutcast/inflater/BootInflater.java @@ -8,6 +8,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.util.Log; +import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import com.github.mmin18.layoutcast.context.OverrideContext; @@ -31,24 +32,27 @@ public BootInflater(Context context) { @Override public LayoutInflater cloneInContext(Context newContext) { - if (newContext instanceof ContextWrapper) { + if (newContext instanceof ContextThemeWrapper) { + // 修改: 默认的 LayoutInflater + // 也就是在调用的时候设置: Resource等 try { - OverrideContext.overrideDefault((ContextWrapper) newContext); + OverrideContext.overrideDefault((ContextThemeWrapper) newContext); } catch (Exception e) { - Log.e("lcast", "fail to override resource in context " - + newContext, e); + Log.e("lcast", "fail to override resource in context " + newContext, e); } } return super.cloneInContext(newContext); } public static void initApplication(Application app) { - LayoutInflater inflater = (LayoutInflater) app - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + // 1. 修改系统的: InflaterService + LayoutInflater inflater = (LayoutInflater) app.getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (inflater instanceof BootInflater) { // already inited return; } + + // 2. systemInflater = inflater; Class cCtxImpl = app.getBaseContext().getClass(); if ("android.app.ContextImpl".equals(cCtxImpl.getName())) { @@ -62,12 +66,14 @@ public static void initApplication(Application app) { } try { - Class cStaticFetcher = cl.loadClass( - androidM ? "android.app.SystemServiceRegistry$StaticServiceFetcher" : - "android.app.ContextImpl$StaticServiceFetcher"); + String fetcherStr = androidM ? "android.app.SystemServiceRegistry$StaticServiceFetcher" : "android.app.ContextImpl$StaticServiceFetcher"; + String fetcherImpl = (androidM ? "android.app.SystemServiceRegistry$" : "android.app.ContextImpl$"); + Class cStaticFetcher = cl.loadClass(fetcherStr); Class cFetcherContainer = null; + + // 寻找一个: fetcherImpl for (int i = 1; i < 50; i++) { - String cn = (androidM ? "android.app.SystemServiceRegistry$" : "android.app.ContextImpl$") + i; + String cn = fetcherImpl + i; try { Class c = cl.loadClass(cn); if (cStaticFetcher.isAssignableFrom(c)) { @@ -77,8 +83,7 @@ public static void initApplication(Application app) { } catch (Exception e) { } } - Constructor cFetcherConstructor = cFetcherContainer - .getDeclaredConstructor(); + Constructor cFetcherConstructor = cFetcherContainer.getDeclaredConstructor(); cFetcherConstructor.setAccessible(true); Object fetcher = cFetcherConstructor.newInstance(); Field f = cStaticFetcher.getDeclaredField("mCachedInstance"); @@ -86,21 +91,19 @@ public static void initApplication(Application app) { f.set(fetcher, new BootInflater(app)); f = cSer.getDeclaredField(androidM ? "SYSTEM_SERVICE_FETCHERS" : "SYSTEM_SERVICE_MAP"); f.setAccessible(true); - HashMap map = (HashMap) f - .get(null); + + // 通过反射来修改系统的: InflatorService + HashMap map = (HashMap) f.get(null); map.put(Context.LAYOUT_INFLATER_SERVICE, fetcher); } catch (Exception e) { - throw new RuntimeException( - "unable to initialize application for BootInflater"); + throw new RuntimeException("unable to initialize application for BootInflater"); } } else { - throw new RuntimeException("application base context class " - + cCtxImpl.getName() + " is not expected"); + throw new RuntimeException("application base context class " + cCtxImpl.getName() + " is not expected"); } if (!(app.getSystemService(Context.LAYOUT_INFLATER_SERVICE) instanceof BootInflater)) { - throw new RuntimeException( - "unable to initialize application for BootInflater"); + throw new RuntimeException("unable to initialize application for BootInflater"); } } } diff --git a/library/src/com/github/mmin18/layoutcast/server/LcastServer.java b/library/src/com/github/mmin18/layoutcast/server/LcastServer.java index 67147b4..f15dfe0 100644 --- a/library/src/com/github/mmin18/layoutcast/server/LcastServer.java +++ b/library/src/com/github/mmin18/layoutcast/server/LcastServer.java @@ -331,7 +331,8 @@ public static void cleanCache(Context ctx) { } } - // 递归删除文件 + // 递归删除文件其中的apk文件 + // private static void rm(File f) { if (f.isDirectory()) { for (File ff : f.listFiles()) { diff --git a/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java b/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java index 162e9ba..a349e43 100644 --- a/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java +++ b/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java @@ -25,6 +25,8 @@ public static boolean overrideClassLoader(ClassLoader cl, File dex, File opt) { Object pathList = fPathList.get(cl); + // XXX: ClassLoader: cl.pathList + // ClassLoader --> Field pathList --> pathList @@ -36,10 +38,13 @@ public static boolean overrideClassLoader(ClassLoader cl, File dex, File opt) { Object dexElements = fDexElements.get(pathList); + // XXX: ClassLoader: cl.pathList.dexElements + // 加载: dex DexClassLoader cl2 = new DexClassLoader(dex.getAbsolutePath(), opt.getAbsolutePath(), null, bootstrap); Object pathList2 = fPathList.get(cl2); Object dexElements2 = fDexElements.get(pathList2); // 读取: dexElements + // 读取: DexClassLoader的: cl2.pathList.dexElements // 读取新的: dexElements Object element2 = Array.get(dexElements2, 0); @@ -54,6 +59,9 @@ public static boolean overrideClassLoader(ClassLoader cl, File dex, File opt) { Object element = Array.get(dexElements, i); Array.set(newDexElements, i + 1, element); } + // dexElements = [dexElements2[0], dexElements...] + + // 修改系统的: dexElements fDexElements.set(pathList, newDexElements); return true; } catch (Exception e) { From ead897da7486872783eac6fcfc54090662e63301 Mon Sep 17 00:00:00 2001 From: feiwang Date: Wed, 13 Apr 2016 15:57:18 +0800 Subject: [PATCH 07/11] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cast.py | 223 +++++++++--------- library/cast.sh | 8 + .../github/mmin18/layoutcast/LayoutCast.java | 3 + .../mmin18/layoutcast/ResetActivity.java | 2 + .../layoutcast/server/IdProfileBuilder.java | 7 + .../mmin18/layoutcast/server/LcastServer.java | 41 +++- 6 files changed, 167 insertions(+), 117 deletions(-) diff --git a/cast.py b/cast.py index 82742e9..366d179 100755 --- a/cast.py +++ b/cast.py @@ -1,5 +1,6 @@ #!/usr/bin/python # -*- coding:utf-8 -*- +import glob from subprocess import Popen, PIPE from distutils.version import LooseVersion import argparse @@ -11,6 +12,7 @@ import shutil import json import zipfile +import urllib2 # 颜色高亮 from colorama import init @@ -18,6 +20,7 @@ init() from colorama import Fore, Back, Style +MAX_ANDROID_API = 20 # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def is_exe(fpath): @@ -57,7 +60,8 @@ def cexec(args, callback=cexec_fail_exit, addPath=None, exitcode=1): env['PATH'] = addPath + os.path.pathsep + env['PATH'] if args[0].endswith("aapt"): - print("--------\nCMD: %saapt %s%s %s\n--------" % (Fore.GREEN, args[1], Fore.RESET, " ".join(args[2:]))) + print("--------\nCMD: %saapt %s%s %s\n--------" % ( + Fore.GREEN, args[1], Fore.RESET, " ".join(args[2:]))) elif args[0].endswith("adb"): print("CMD: %sadb%s %s" % (Fore.GREEN, Fore.RESET, " ".join(args[1:]))) else: @@ -73,19 +77,11 @@ def cexec(args, callback=cexec_fail_exit, addPath=None, exitcode=1): return output +# 访问URL: GET/POST def curl(url, body=None, ignoreError=False, exitcode=1): print ("URL: %s%s%s" % (Fore.MAGENTA, url, Fore.RESET)) - import sys - try: - if sys.version_info >= (3, 0): - import urllib.request - - return urllib.request.urlopen(url, data=body).read().decode('utf-8').strip() - else: - import urllib2 - - return urllib2.urlopen(url, data=body).read().decode('utf-8').strip() + return urllib2.urlopen(url, data=body).read().decode('utf-8').strip() except Exception as e: if ignoreError: return None @@ -150,8 +146,9 @@ def __deps_list_eclipse(list, project): list.append(absdep) -# def __deps_list_gradle(list, project): + # 获取gradle的依赖? + # 这些项目有依赖的顺序的问题吗? str = open_as_text(os.path.join(project, 'build.gradle')) str = remove_comments(str) ideps = [] @@ -162,20 +159,27 @@ def __deps_list_gradle(list, project): depends = balanced_braces(str[m.start():]) for proj in re.findall(r'''compile\s+project\s*\(.*['"]:(.+)['"].*\)''', depends): ideps.append(proj.replace(':', os.path.sep)) + if len(ideps) == 0: return + # 格式: + # comile project(":hello") + # 目录结构: + # root/ + # ChunyuYuer + # CYUtils + # libs/commons/ + # + # 也就是我们这里最多考虑3层的Project嵌套(这里应该是对root进行定位吧) path = project for i in range(1, 3): - # 这是什么意思? - # /xxx/path0/path1/path2/ - # /xxx/path0/path1/path2/.. - # /xxx/path0/path1/path2/../.. path = os.path.abspath(os.path.join(path, os.path.pardir)) b = True deps = [] + for idep in ideps: - # 定位: deps + # 获取 idep的完整路径 dep = os.path.join(path, idep) if not os.path.isdir(dep): b = False @@ -191,15 +195,11 @@ def __deps_list_gradle(list, project): break +# 获取工程内的所有依赖的项目的路径 def deps_list(dir): - if is_gradle_project(dir): - list = [] - __deps_list_gradle(list, dir) - return list - else: - list = [] - __deps_list_eclipse(list, dir) - return list + list = [] + __deps_list_gradle(list, dir) + return list def manifestpath(dir): @@ -228,18 +228,15 @@ def get_apk_path(dir): # bin/*.apk # build/outputs/apk/*.apk # - if not is_gradle_project(dir): - apkpath = os.path.join(dir, 'bin') - else: - apkpath = os.path.join(dir, 'build', 'outputs', 'apk') - + apkpath = os.path.join(dir, 'build', 'outputs', 'apk') # Get the lastmodified *.apk file maxt = 0 maxd = None for dirpath, dirnames, files in os.walk(apkpath): for fn in files: - if fn.endswith('.apk') and not fn.endswith('-unaligned.apk') and not fn.endswith('-unsigned.apk'): + if fn.endswith('.apk') and not fn.endswith('-unaligned.apk') and not fn.endswith( + '-unsigned.apk'): lastModified = os.path.getmtime(os.path.join(dirpath, fn)) if lastModified > maxt: maxt = lastModified @@ -380,6 +377,7 @@ def countSrcDir2(dir, lastBuild=0, list=None): return (count, lastModified) +# 返回项目: dir 对应的 src的路径,count, 最后修改时间 def srcdir2(dir, lastBuild=0, list=None): for srcdir in [os.path.join(dir, 'src', 'main', 'java'), os.path.join(dir, 'src')]: olist = None @@ -417,6 +415,7 @@ def is_launchable_project(dir): return False +# 直接按照目录结构来寻找: project dir def __append_project(list, dir, depth): if package_name(dir): list.append(dir) @@ -453,23 +452,24 @@ def list_aar_projects(dir, deps): pnlist = [package_name(i) for i in deps] pnlist.append(package_name(dir)) list1 = [] - if os.path.isdir(os.path.join(dir, 'build', 'intermediates', 'incremental', 'mergeResources')): - for dirpath, dirnames, files in os.walk( - os.path.join(dir, 'build', 'intermediates', 'incremental', 'mergeResources')): - if re.findall(r'[/\\+]androidTest[/\\+]', dirpath): - continue - for fn in files: - if fn == 'merger.xml': - data = open_as_text(os.path.join(dirpath, fn)) - for s in re.findall(r'''path="([^"]+)"''', data): - (parent, child) = os.path.split(s) - if child.endswith('.xml') or child.endswith('.png') or child.endswith( - '.jpg'): - (parent, child) = os.path.split(parent) - if isResName(child) and not parent in list1: - list1.append(parent) - elif os.path.isdir(s) and not s in list1 and countResDir(s) > 0: - list1.append(s) + + # 如何获取 aar 呢? + incr_dir = os.path.join(dir, 'build', 'intermediates', 'incremental') + + files = glob.glob(os.path.join(incr_dir, "mergeResources*/*/merger.xml")) + + for file in files: + data = open_as_text(file) + for s in re.findall(r'''path="([^"]+)"''', data): + (parent, child) = os.path.split(s) + if child.endswith('.xml') or child.endswith('.png') or child.endswith('.jpg'): + (parent, child) = os.path.split(parent) + if isResName(child) and not parent in list1: + list1.append(parent) + elif os.path.isdir(s) and not s in list1 and countResDir(s) > 0: + list1.append(s) + + list2 = [] for ppath in list1: parpath = os.path.abspath(os.path.join(ppath, os.pardir)) @@ -486,20 +486,23 @@ def get_android_jar(path): platforms = os.path.join(path, 'platforms') if not os.path.isdir(platforms): return None + api = 0 result = None for pd in os.listdir(platforms): pd = os.path.join(platforms, pd) - if os.path.isdir(pd) and os.path.isfile( - os.path.join(pd, 'source.properties')) and os.path.isfile( - os.path.join(pd, 'android.jar')): + if os.path.isdir(pd) and os.path.isfile(os.path.join(pd, 'source.properties')) and os.path.isfile(os.path.join(pd, 'android.jar')): s = open_as_text(os.path.join(pd, 'source.properties')) m = re.search(r'^AndroidVersion.ApiLevel\s*[=:]\s*(.*)$', s, re.MULTILINE) if m: a = int(m.group(1)) - if a > api: + if a > api: # 选择API最大的一个版本 api = a result = os.path.join(pd, 'android.jar') + + # 停止选择 + if api == MAX_ANDROID_API: + break return result @@ -576,7 +579,7 @@ def get_android_sdk(dir, condf=get_android_jar): def get_javac(dir): # 如何获取JavaC - execname = os.name == 'nt' and 'javac.exe' or 'javac' + execname = 'javac' if dir and os.path.isfile(os.path.join(dir, 'bin', execname)): return os.path.join(dir, 'bin', execname) @@ -588,33 +591,20 @@ def get_javac(dir): if path and is_exe(os.path.join(path, 'bin', execname)): return os.path.join(path, 'bin', execname) - if os.name == 'nt': - btpath = 'C:\\Program Files\\Java' + # 如果没有指定,则在默认的路径下搜索 + for btpath in ['/Library/Java/JavaVirtualMachines', + '/System/Library/Java/JavaVirtualMachines']: if os.path.isdir(btpath): minv = '' minp = None for pn in os.listdir(btpath): - path = os.path.join(btpath, pn, 'bin', execname) + path = os.path.join(btpath, pn, 'Contents', 'Home', 'bin', execname) if is_exe(path): if pn > minv: minv = pn minp = path - return minp - else: - # 如果没有指定,则在默认的路径下搜索 - for btpath in ['/Library/Java/JavaVirtualMachines', - '/System/Library/Java/JavaVirtualMachines']: - if os.path.isdir(btpath): - minv = '' - minp = None - for pn in os.listdir(btpath): - path = os.path.join(btpath, pn, 'Contents', 'Home', 'bin', execname) - if is_exe(path): - if pn > minv: - minv = pn - minp = path - if minp: - return minp + if minp: + return minp def search_path(dir, filename): @@ -719,6 +709,8 @@ def scan_port(adbpaths, pnlist, projlist): :param projlist: :return: """ + URL_PACKAGE = 'http://127.0.0.1:%d/packagename' + URL_STATE = 'http://127.0.0.1:%d/appstate' port = 0 prodir = None packagename = None @@ -732,14 +724,14 @@ def scan_port(adbpaths, pnlist, projlist): # 2. 然后 pnlist # projlist - output = curl('http://127.0.0.1:%d/packagename' % try_port, ignoreError=True) + output = curl(URL_PACKAGE % try_port, ignoreError=True) if output and output in pnlist: # 如果返回的 packagename可以接受 index = pnlist.index(output) # index of this app in projlist # 获取 app的状态 # appstate是如何定义的呢? - state = curl('http://127.0.0.1:%d/appstate' % try_port, ignoreError=True) + state = curl(URL_STATE % try_port, ignoreError=True) if state and int(state) >= 2: # starte >= 2 表示界面可见 port = try_port @@ -757,6 +749,20 @@ def scan_port(adbpaths, pnlist, projlist): return port, prodir, packagename +def get_dir_mtime(adir): + latestModified = os.path.getmtime(adir) + for dirpath, dirnames, files in os.walk(adir): + for dirname in dirnames: + if not dirname.startswith('.'): + latestModified = max(latestModified, + os.path.getmtime(os.path.join(dirpath, dirname))) + for fn in files: + if not fn.startswith('.'): + fpath = os.path.join(dirpath, fn) + latestModified = max(latestModified, os.path.getmtime(fpath)) + return latestModified + + if __name__ == "__main__": dir = '.' @@ -784,6 +790,7 @@ def scan_port(adbpaths, pnlist, projlist): device = args.device # 2. 获取有的 project list + # list_projects: settings.gradle projlist = [i for i in list_projects(dir) if is_launchable_project(i)] # 3. 获取默认的android sdk/java sdk @@ -826,7 +833,8 @@ def scan_port(adbpaths, pnlist, projlist): if latest_package: command = [] command.extend(adbpaths) - command.extend(['shell', 'monkey', '-p', latest_package, '-c', 'android.intent.category.LAUNCHER', '1']) + command.extend(['shell', 'monkey', '-p', latest_package, '-c', + 'android.intent.category.LAUNCHER', '1']) cexec(command, callback=None) for i in range(0, 6): # try 6 times to wait the application launches @@ -836,9 +844,11 @@ def scan_port(adbpaths, pnlist, projlist): time.sleep(0.25) if port == 0: - print('package %s not found, make sure your project is properly setup and running' % (len(pnlist) == 1 and pnlist[0] or pnlist)) + print('package %s not found, make sure your project is properly setup and running' % ( + len(pnlist) == 1 and pnlist[0] or pnlist)) exit(5) + # 3. LcastServer的各种API URL_LCAST = 'http://127.0.0.1:%d/lcast' % port URL_PUSH_DEX = 'http://127.0.0.1:%d/pushdex' % port URL_LAUNCH = 'http://127.0.0.1:%d/launcher' % port @@ -846,29 +856,25 @@ def scan_port(adbpaths, pnlist, projlist): URL_IDS = 'http://127.0.0.1:%d/ids.xml' % port URL_PUBLIC = 'http://127.0.0.1:%d/public.xml' % port - - # 将资源推送给手机 - URL_PUSH_RES = 'http://127.0.0.1:%d/pushres' % port - - # 用于判断手机是否支持: ART - URL_VM_VERSION = 'http://127.0.0.1:%d/vmversion' % port - - # is_gradle = is_gradle_project(dir) + URL_PUSH_RES = 'http://127.0.0.1:%d/pushres' % port # 将资源推送给手机 + URL_VM_VERSION = 'http://127.0.0.1:%d/vmversion' % port # 用于判断手机是否支持: ART android_jar = get_android_jar(sdkdir) if not android_jar: print('android.jar not found !!!\nUse local.properties or set ANDROID_HOME env') exit(7) + + # 4. 获取工程内的所有依赖的项目的路径 deps = deps_list(dir) - # build/lcast - bindir = os.path.join(dir, 'build', 'lcast') or os.path.join(dir, 'bin', 'lcast') + # 5. build/lcast(表示本地有哪些资源id已经被占用) + bindir = os.path.join(dir, 'build', 'lcast') # check if the /res and /src has changed lastBuild = 0 - # 获取apk的路径(fpath, 以及build的时间) - # rdir = is_gradle and os.path.join(dir, 'build', 'outputs', 'apk') or os.path.join(dir, 'bin') + # 6. 获取apk的路径(fpath, 以及build的时间) + # apk是什么东西呢? rdir = os.path.join(dir, 'build', 'outputs', 'apk') or os.path.join(dir, 'bin') if os.path.isdir(rdir): for fn in os.listdir(rdir): @@ -876,8 +882,7 @@ def scan_port(adbpaths, pnlist, projlist): fpath = os.path.join(rdir, fn) lastBuild = max(lastBuild, os.path.getmtime(fpath)) - - # dir, deps如何处理呢? + # 7. dir, deps如何处理呢? adeps = [] adeps.extend(deps) adeps.append(dir) @@ -892,20 +897,14 @@ def scan_port(adbpaths, pnlist, projlist): adir = assetdir(dep) # A. 获取: asset的变化情况 + if adir: - latestModified = os.path.getmtime(adir) - for dirpath, dirnames, files in os.walk(adir): - for dirname in dirnames: - if not dirname.startswith('.'): - latestModified = max(latestModified, os.path.getmtime(os.path.join(dirpath, dirname))) - for fn in files: - if not fn.startswith('.'): - fpath = os.path.join(dirpath, fn) - latestModified = max(latestModified, os.path.getmtime(fpath)) - latestResModified = max(latestResModified, latestModified) + latestModified = get_dir_mtime(adir) if latestModified > lastBuild: assetdirs.append(adir) # 整个asset dir都放在里面 + latestResModified = max(latestResModified, latestModified) + # B. 获取 resource 的变化情况 rdir = resdir(dep) if rdir: @@ -918,9 +917,11 @@ def scan_port(adbpaths, pnlist, projlist): # 返回源码的修改时间,以及文件个数 (sdir, scount, smt) = srcdir2(dep, lastBuild=lastBuild, list=msrclist) + if sdir: srcs.append(sdir) latestSrcModified = max(latestSrcModified, smt) + resModified = latestResModified > lastBuild srcModified = latestSrcModified > lastBuild @@ -937,10 +938,9 @@ def scan_port(adbpaths, pnlist, projlist): print('cast %s:%d as gradle project with %s changed' % (packagename, port, targets)) - # prepare to reset - # 如果代码修改了,直接进行: pcast if srcModified: + # 告知用户要重启服务 curl(URL_PCAST, ignoreError=True) if resModified: @@ -962,13 +962,14 @@ def scan_port(adbpaths, pnlist, projlist): with open(os.path.join(binresdir, 'values/public.xml'), 'w') as fp: fp.write(data) - # Get the assets path + # Get the assets path: apk_path = get_apk_path(dir) if apk_path: # build/lcast/assets assets_path = os.path.join(bindir, "assets") if os.path.isdir(assets_path): shutil.rmtree(assets_path) + # 从apk中解压缩: assets get_asset_from_apk(apk_path, bindir) aaptpath = get_aapt(sdkdir) @@ -976,16 +977,19 @@ def scan_port(adbpaths, pnlist, projlist): print('aapt not found in %s/build-tools' % sdkdir) exit(10) - # 生成 res.zip - aaptargs = [aaptpath, 'package', '-f', '--auto-add-overlay', '-F', os.path.join(bindir, 'res.zip')] + aaptargs = [aaptpath, 'package', '-f', '--auto-add-overlay', '-F', + os.path.join(bindir, 'res.zip')] + aaptargs.append('-S') aaptargs.append(binresdir) + rdir = resdir(dir) if rdir: aaptargs.append('-S') aaptargs.append(rdir) - for dep in reversed(deps): + + for dep in reversed(deps): # 注意: deps的顺序(这里似乎不太科学, 当然手动配置可以是可以的) rdir = resdir(dep) if rdir: aaptargs.append('-S') @@ -995,6 +999,7 @@ def scan_port(adbpaths, pnlist, projlist): for dep in reversed(list_aar_projects(dir, deps)): aaptargs.append('-S') aaptargs.append(dep) + for assetdir in assetdirs: aaptargs.append('-A') aaptargs.append(assetdir) @@ -1002,11 +1007,10 @@ def scan_port(adbpaths, pnlist, projlist): aaptargs.append('-A') aaptargs.append(assets_path) aaptargs.append('-M') - aaptargs.append(manifestpath(dir)) + aaptargs.append(manifestpath(dir)) # 如果存在多个Manifest合并的情况该如何处理呢? aaptargs.append('-I') aaptargs.append(android_jar) - print(Fore.RED + "更新 res.zip 文件..." + Fore.RESET) cexec(aaptargs, exitcode=18) @@ -1077,7 +1081,8 @@ def scan_port(adbpaths, pnlist, projlist): classpath.append(os.path.join(dirpath, fn)) # R.class classesdir = search_path(os.path.join(dir, 'build', 'intermediates', 'classes'), - launcher and launcher.replace('.', os.path.sep) + '.class' or '$') + launcher and launcher.replace('.', + os.path.sep) + '.class' or '$') classpath.append(classesdir) binclassesdir = os.path.join(bindir, 'classes') diff --git a/library/cast.sh b/library/cast.sh index 4d03f05..40720fd 100755 --- a/library/cast.sh +++ b/library/cast.sh @@ -1,10 +1,18 @@ #!/bin/bash set -e +# 一般有效(当个app开发的时候不存在端口冲突) adb forward tcp:41128 tcp:41128 mkdir -p bin/lcast/values + +# 下载资源 curl --silent --output bin/lcast/values/ids.xml http://127.0.0.1:41128/ids.xml curl --silent --output bin/lcast/values/public.xml http://127.0.0.1:41128/public.xml + +# 这就是为什么使用: -S bin/lcast(表示这些资源已经被占用了) aapt package -f --auto-add-overlay -F bin/res.zip -S bin/lcast -S res/ -M AndroidManifest.xml -I /Applications/android-sdk-mac_86/platforms/android-19/android.jar + +# 上传文件 curl -T bin/res.zip http://localhost:41128/pushres +# 重启系统 curl http://localhost:41128/lcast diff --git a/library/src/com/github/mmin18/layoutcast/LayoutCast.java b/library/src/com/github/mmin18/layoutcast/LayoutCast.java index 3390d86..49f5794 100644 --- a/library/src/com/github/mmin18/layoutcast/LayoutCast.java +++ b/library/src/com/github/mmin18/layoutcast/LayoutCast.java @@ -63,6 +63,7 @@ public static void init(Context context) { // 修改系统的InlaterService BootInflater.initApplication(app); + // 启动完毕了,则修改: res.ped --> res.apk(下次重启就不要了) if (res.length() > 0) { try { File f = new File(dir, "res.apk"); @@ -82,6 +83,8 @@ public static void init(Context context) { public static boolean restart(boolean confirm) { Context top = OverrideContext.getTopActivity(); + + // 如何重启? if (top instanceof ResetActivity) { ((ResetActivity) top).reset(); return true; diff --git a/library/src/com/github/mmin18/layoutcast/ResetActivity.java b/library/src/com/github/mmin18/layoutcast/ResetActivity.java index 0c4ce6e..f9206bf 100644 --- a/library/src/com/github/mmin18/layoutcast/ResetActivity.java +++ b/library/src/com/github/mmin18/layoutcast/ResetActivity.java @@ -30,6 +30,7 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(tv); createTime = SystemClock.uptimeMillis(); + // 直接关闭, 不用考虑启动的问题,直接手动启动也可以,无碍大局 ready = getIntent().getBooleanExtra("reset", false); if (ready) { reset(); @@ -63,6 +64,7 @@ public void run() { @Override public void onBackPressed() { + // 如果多次返回就直接关闭 if (back++ > 0) { if (ready) { reset.run(); diff --git a/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java b/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java index 5fa5cc2..24b2da5 100644 --- a/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java +++ b/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java @@ -20,10 +20,12 @@ public String buildIds(Class Rclazz) throws Exception { sb.append("\n"); Class clazz = null; try { + // 注意资源文件的定义: me.chunyu.ChunyuYuer.R$id String n = Rclazz.getName() + "$id"; clazz = cl.loadClass(n); } catch (ClassNotFoundException e) { } + if (clazz != null) { buildIds(sb, clazz); } @@ -36,6 +38,8 @@ private void buildIds(StringBuilder out, Class clazz) throws Exception { // 统计Ids的最大最小数值 int start = 0, end = 0; for (Field f : clazz.getDeclaredFields()) { + // 如何导出资源文件呢? + // Integer类型的数据,静态的,可访问的 if (Integer.TYPE.equals(f.getType()) && java.lang.reflect.Modifier.isStatic(f.getModifiers()) && java.lang.reflect.Modifier.isPublic(f.getModifiers())) { int i = f.getInt(null); if ((i & 0x7f000000) == 0x7f000000) { @@ -70,6 +74,8 @@ public String buildPublic(Class Rclazz) throws Exception { StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append("\n"); + + // 处理各类资源 for (String type : types) { Class clazz = null; try { @@ -105,6 +111,7 @@ private void buildPublic(StringBuilder out, Class clazz, String type) } } + // 每一类资源都有: type, name, id for (int i = start; i > 0 && i <= end; i++) { out.append(" 0) { file = new File(dir, "res.ped"); @@ -127,7 +129,7 @@ protected void handle(String method, String path, fos.write(buf, 0, l); } fos.close(); - latestPushFile = file; + latestPushResFile = file; response.setStatusCode(201); Log.d("lcast", "lcast resources file received (" + file.length() + " bytes): " + file); return; @@ -156,19 +158,30 @@ protected void handle(String method, String path, // 如何重启呢? if ("/pcast".equalsIgnoreCase(path)) { + // 告知用户要重启(注意参数: false) + // 用户可以选择关闭 LayoutCast.restart(false); response.setStatusCode(200); return; } + /** + * dex.ped res.ped 之间的关系 + * 1. 启动前, 如果dex.ped存在,变成: dex.apk, 然后通过class loader加载;res.apk等文件要被删除 + * 2. 如果res.ped存在,则变成: res.apk 进行加载 + * 3. 上传资源时如果: dex.ped存在,则以 res.ped保存;说明接下来马上就要重启,dex.ped加载之后,再加载: res.ped + * 4. 如果dex.ped不存在,则直接保存为 res.apk + */ if ("/lcast".equalsIgnoreCase(path)) { File dir = new File(context.getCacheDir(), "lcast"); File dex = new File(dir, "dex.ped"); + // 1. 如果存在: dex 文件, 则将: latestPushResFile 修改为: res.ped; 然后重启 if (dex.length() > 0) { - if (latestPushFile != null) { + // 代码修改了,临时修改: latestPushResFile, 然后重启代码, 防止: res.ped被删除 + if (latestPushResFile != null) { File f = new File(dir, "res.ped"); - latestPushFile.renameTo(f); + latestPushResFile.renameTo(f); } Log.i("lcast", "cast with dex changes, need to restart the process (activity stack will be reserved)"); boolean b = LayoutCast.restart(true); @@ -176,25 +189,32 @@ protected void handle(String method, String path, } else { // 没有代码的修改 - Resources res = ResUtils.getResources(app, latestPushFile); + Resources res = ResUtils.getResources(app, latestPushResFile); OverrideContext.setGlobalResources(res); response.setStatusCode(200); - response.write(String.valueOf(latestPushFile).getBytes("utf-8")); + response.write(String.valueOf(latestPushResFile).getBytes("utf-8")); Log.i("lcast", "cast with only res changes, just recreate the running activity."); } return; } + + // 资源恢复默认的资源 if ("/reset".equalsIgnoreCase(path)) { OverrideContext.setGlobalResources(null); response.setStatusCode(200); response.write("OK".getBytes("utf-8")); return; } + + // 读取: 最终apk的资源 + // 例如: me.chunyu.ChunyuYuer.R (由于Android的Resource Union, 它基本上包含多有的id信息) if ("/ids.xml".equalsIgnoreCase(path)) { String Rn = app.getPackageName() + ".R"; + Class Rclazz = app.getClassLoader().loadClass(Rn); String str = new IdProfileBuilder(context.getResources()).buildIds(Rclazz); + response.setStatusCode(200); response.setContentTypeText(); response.write(str.getBytes("utf-8")); @@ -214,6 +234,8 @@ protected void handle(String method, String path, if ("/apkinfo".equalsIgnoreCase(path)) { ApplicationInfo ai = app.getApplicationInfo(); File apkFile = new File(ai.sourceDir); + // 获取Apk的信息: + // size, lastModified, md5 JSONObject result = new JSONObject(); result.put("size", apkFile.length()); result.put("lastModified", apkFile.lastModified()); @@ -237,6 +259,8 @@ protected void handle(String method, String path, // 获取原始的apk数据 if ("/apkraw".equalsIgnoreCase(path)) { ApplicationInfo ai = app.getApplicationInfo(); + + // 将apk直接下载下来 FileInputStream fis = new FileInputStream(ai.sourceDir); response.setStatusCode(200); response.setContentTypeBinary(); @@ -253,6 +277,7 @@ protected void handle(String method, String path, File apkFile = new File(ai.sourceDir); JarFile jarFile = new JarFile(apkFile); + // 获取其中的fileinfo JarEntry je = jarFile.getJarEntry(path.substring("/fileinfo/".length())); InputStream ins = jarFile.getInputStream(je); MessageDigest md5 = MessageDigest.getInstance("MD5"); @@ -320,7 +345,7 @@ public static void start(Context ctx) { } } - // 删除: lcast内的所有的文件 + // 删除: lcast内的所有的apk文件 public static void cleanCache(Context ctx) { File dir = new File(ctx.getCacheDir(), "lcast"); File[] fs = dir.listFiles(); From 04001e5a8fea1ab688221a4122fb1d57bd789804 Mon Sep 17 00:00:00 2001 From: feiwang Date: Wed, 13 Apr 2016 16:44:02 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 201 ++++++++++++++++++++++++++---------------------------- 1 file changed, 96 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 15829f7..6f2e094 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,134 @@ -*Android SDK sucks. It's so slow to build and run which waste me a lot of time every day.* - -## Motivation - -Facebook Buck build is fast. However, the biggest problem with Buck is, it requires you to change a lot of codes, and restructs your project in small modules. Indeed, it is troublesome to just make it work properly on the existing android project, especially if you have big project. I have tried using Buck build system instead of Gradle on my test project. However, it took me a week just to make it work. - -What I needs is a build tool that is easy to setup, fast as Buck, and provide a Run button in Android Studio. So I created LayoutCast. - -**LayoutCast** is a little tool to help with that, it will cast every changes in your Java source code or resources (including library project) to your phone or emulator within 5 sec, and does not restart your application. - -把代码和资源文件的改动直接同步到手机上,应用不需要重启。省去了编译运行漫长的等待,比较适合真机调试的时候使用。 - -![GIF](images/cast_res.gif) -![GIF](images/cast_code.gif) - -Youtube demo video: - -优酷: - -## Features - -- Fast cast code and resource changes, usually less than 5 sec. -- Cast does not reset your application. The running activity stack will be kept. -- Easy to setup, only add few lines of code. -- Support both eclipse and AndroidStudio project. -- Provide a AndroidStudio plugin to click and cast. - -## Limitations - -- ~~LayoutCast only support Mac (for now)~~ -- Cast Java code only support ART runtime (Android 5.0) - -## Benchmarks - -Here is how it compared to Gradle and Facebook Buck: - -![BENCHMARK](images/benchmark1.png) - -The test machine is a 2015 MBP with a 2014 MotoX. - -The test project's apk is about 14.3MB, which contains 380k lines of java code and 86k lines of xml files. - -## Getting Started for Android Studio / Intellij - -### 1. Install Plugin - -*If you have already done that, you can skip this step.* - -1. Download Android Studio / Intellij plugin -2. In Android Studio, go to `Preferences` > `Plugins` > `Install plugin from disk...` -3. Choose the downloaded file from step #1 to install the plugin. - -After restart, you should find a button at right of the run section: ![TOOLBAR](images/sc_toolbar.png) - -### 2. Android Project & Build System Changes - -**First,** you need to setup your project. Add below dependency in your build.gradle: - +# LayoutCast +是什么东西,工作原理见最后面! + +## 1. 特色 +* Fast cast code and resource changes, usually less than 5 sec. + * `快速处理代码和Resource的变化,5s内生效` +* Cast does not reset your application. The running activity stack will be kept. + * `保留当前的Activity Stack`, 一般情况下只更新当前的Activity +* Easy to setup, only add few lines of code. + * `容易使用` +* Support both eclipse and AndroidStudio project. + * `其实基本和IDE无关`,只是用到了IDE的某个目录结构的特点 +* Provide a AndroidStudio plugin to click and cast. + * 要求 `adb devices` 只有一个设备, 否则plugin就傻了,不过也不影响开发 + + +## 2. 如何和Android Studio集成呢? + +* 安装插件(安装一次就OK) + 1. Download Android Studio / Intellij plugin + 2. In Android Studio, go to `Preferences` > `Plugins` > `Install plugin from disk...` + 3. Choose the downloaded file from step #1 to install the plugin. + * After restart, you should find a button at right of the run section: + * ![TOOLBAR](images/sc_toolbar.png) + +* Android Project & Build System Changes + * 添加依赖 +```gradle dependencies { compile 'com.github.mmin18.layoutcast:library:1.+@aar' ... } +``` + + * 修改Application的代码 +```java +public class MyApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + + // 只在测试时有效 + if (BuildConfig.DEBUG) { + LayoutCast.init(this); + } + } +} +``` + * 修改AndroidManifest.xml +```xml + + +``` - public class MyApplication extends Application { - @Override - public void onCreate() { - super.onCreate(); +* 运行 + * 正常运行Android项目 + * 修改资源或者Java代码, 然后: `./cast.py` 或点击: Android Studio`工具栏上的按钮` +```bash + cd 或项目root目录 + python cast.py + python cast.py --device=xxxx - if (BuildConfig.DEBUG) { - LayoutCast.init(this); - } - } - } + Or you can specify the path in args: + python cast.py +``` -**Thrid,** don't forget to check if your Application class is registered in `AndroidManifest.xml`: - +## Troubleshootings(一定要看) +* `这只是一个工具,保证能用,但是不保证任何场合都能用`(理论上OK, 但是没有经过完整测试) +* 目录结构的约定: + * It can only find `/src` folder under `/src` or `/src/main/java` + * It can only find `/res` folder under `/res` or `/src/main/res` +* 可以添加和修改资源,但是不能删除 + * You can add or replace resources, but you can't delete or rename resources (for now) + * 删除之后重新使用gradle`编译运行即可`, 也就是放弃gradle的状态 +* 异常如何处理: + * If cast failed, clean your project, remove `/bin` and `/build` and rebuild again may solve the problem -And make sure you have the network permission in your `AndroidManifest.xml`: +----- - +## How it Works -### 3. Run and cast +When **LayoutCast.init(context);** called, the application will start tiny http server in the background, and receive certain commands. Later on, the cast script running on your computer will communicate with your running app which is running through ADB TCP forward. -Run the application (in device or emulator), then try to make some changes for resources file or java file. +When the cast script runs, it will scan all possible ports on your phone to find the running LayoutCast server, and get the running application's resource list with its id, then compiled to `public.xml`. In which, it will be used later to keep resource id index consistent with the running application. -After that, click the LayoutCast button in toolbar (on the right of Run button) / go to menu `Tools`> `Layout Cast`. +The cast script scans your project folder to find the `/res` folder, and all dependencies inside `/res` folder. You can run the **aapt** command to package all resources into **res.zip**, and then upload the zip file to the LayoutCast server to replace the resources of the running process. Then, it calls the **Activity.recreate()** to restart the visible activity. -It will show the result above status bar: +Usually the activity will keep its running state in **onSaveInstanceState()** and restore after coming back later. -![SUCCESS](images/sc_success.png) -![FAIL](images/sc_fail.png) -## Getting started for Eclipse +`Android SDK sucks. It's so slow to build and run which waste me a lot of time every day.` -### 1. Prepare the cast script +## Motivation -I haven't written the Eclipse plugin yet, so if you need to use it on a eclipse project, you can try to use the command line. +Facebook Buck build is fast. However, the biggest problem with Buck is, it requires you to change a lot of codes, and restructs your project in small modules. Indeed, it is troublesome to just make it work properly on the existing android project, especially if you have big project. I have tried using Buck build system instead of Gradle on my test project. However, it took me a week just to make it work. -You can get the script here . Put the script in project root dir or anywhere you like. Since it is written in Python 2.7 (make sure you have installed the right version). +What I needs is a build tool that is easy to setup, fast as Buck, and provide a Run button in Android Studio. So I created LayoutCast. -### 2. Android Project & Build System Changes +**LayoutCast** is a little tool to help with that, it will cast every changes in your Java source code or resources (including library project) to your phone or emulator within 5 sec, and does not restart your application. -To get it work, we will need to download the LayoutCast library and put it to your `/libs` folder. +把代码和资源文件的改动直接同步到手机上,应用不需要重启。省去了编译运行漫长的等待,比较适合真机调试的时候使用。 -The project structure will remain the same. +![GIF](images/cast_res.gif) +![GIF](images/cast_code.gif) -### 3. Run and Cast +Youtube demo video: -Run the application first, and open terminal and execute **python cast.py** under your project's folder: +优酷: - cd - python cast.py -Or you can specify the path in args: +## Limitations - python cast.py +- ~~LayoutCast only support Mac (for now)~~ +- Cast Java code only support ART runtime (Android 5.0) -## How it Works +## Benchmarks -When **LayoutCast.init(context);** called, the application will start tiny http server in the background, and receive certain commands. Later on, the cast script running on your computer will communicate with your running app which is running through ADB TCP forward. +Here is how it compared to Gradle and Facebook Buck: -When the cast script runs, it will scan all possible ports on your phone to find the running LayoutCast server, and get the running application's resource list with its id, then compiled to `public.xml`. In which, it will be used later to keep resource id index consistent with the running application. +![BENCHMARK](images/benchmark1.png) -The cast script scans your project folder to find the `/res` folder, and all dependencies inside `/res` folder. You can run the **aapt** command to package all resources into **res.zip**, and then upload the zip file to the LayoutCast server to replace the resources of the running process. Then, it calls the **Activity.recreate()** to restart the visible activity. +The test machine is a 2015 MBP with a 2014 MotoX. -Usually the activity will keep its running state in **onSaveInstanceState()** and restore after coming back later. +The test project's apk is about 14.3MB, which contains 380k lines of java code and 86k lines of xml files. -## Troubleshootings -- It can only find `/src` folder under `/src` or `/src/main/java` -- It can only find `/res` folder under `/res` or `/src/main/res` -- You can add or replace resources, but you can't delete or rename resources (for now) -- If cast failed, clean your project, remove `/bin` and `/build` and rebuild again may solve the problem From b0350b414c2a93208d0926374748c9641d41ac4e Mon Sep 17 00:00:00 2001 From: feiwang Date: Wed, 13 Apr 2016 22:35:52 +0800 Subject: [PATCH 09/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86cast.py?= =?UTF-8?q?=E5=AF=B9=E4=BB=A3=E7=A0=81=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cast.py | 83 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/cast.py b/cast.py index 366d179..568bc24 100755 --- a/cast.py +++ b/cast.py @@ -21,6 +21,7 @@ from colorama import Fore, Back, Style MAX_ANDROID_API = 20 +ANDROID_ANNOTATION_SUPPORT = "20.0.0" # http://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def is_exe(fpath): @@ -401,18 +402,14 @@ def libdir(dir): return None +# project是否可以编译成为apk(通过plugin来判断) def is_launchable_project(dir): - if is_gradle_project(dir): - data = open_as_text(os.path.join(dir, 'build.gradle')) - data = remove_comments(data) - if re.findall(r'''apply\s+plugin:\s*['"]com.android.application['"]''', data, re.MULTILINE): - return True - elif os.path.isfile(os.path.join(dir, 'project.properties')): - data = open_as_text(os.path.join(dir, 'project.properties')) - if re.findall(r'''^\s*target\s*=.*$''', data, re.MULTILINE) and not re.findall( - r'''^\s*android.library\s*=\s*true\s*$''', data, re.MULTILINE): - return True - return False + data = open_as_text(os.path.join(dir, 'build.gradle')) + data = remove_comments(data) + if re.findall(r'''apply\s+plugin:\s*['"]com.android.application['"]''', data, re.MULTILINE): + return True + else: + return False # 直接按照目录结构来寻找: project dir @@ -456,6 +453,7 @@ def list_aar_projects(dir, deps): # 如何获取 aar 呢? incr_dir = os.path.join(dir, 'build', 'intermediates', 'incremental') + # 注意路径的选择 files = glob.glob(os.path.join(incr_dir, "mergeResources*/*/merger.xml")) for file in files: @@ -500,21 +498,33 @@ def get_android_jar(path): api = a result = os.path.join(pd, 'android.jar') - # 停止选择 + # 停止选择(设置 android sdk的版本) if api == MAX_ANDROID_API: break return result +def get_support_annotation_jar(sdk_path): + path = os.path.join(sdk_path, "extras/android/m2repository/com/android/support/support-annotations") + + path = os.path.join(path, ANDROID_ANNOTATION_SUPPORT, "support-annotations-%s.jar" % ANDROID_ANNOTATION_SUPPORT) + if os.path.exists(path): + return path + else: + return None + + + + def get_adb(path): - execname = os.name == 'nt' and 'adb.exe' or 'adb' + execname = 'adb' if os.path.isdir(path) and is_exe(os.path.join(path, 'platform-tools', execname)): return os.path.join(path, 'platform-tools', execname) def get_aapt(path): # 首先获取: execname - execname = os.name == 'nt' and 'aapt.exe' or 'aapt' + execname = 'aapt' # 给定sdk path if os.path.isdir(path) and os.path.isdir(os.path.join(path, 'build-tools')): btpath = os.path.join(path, 'build-tools') @@ -532,7 +542,7 @@ def get_aapt(path): def get_dx(path): # dx的作用 - execname = os.name == 'nt' and 'dx.bat' or 'dx' + execname = 'dx' if os.path.isdir(path) and os.path.isdir(os.path.join(path, 'build-tools')): btpath = os.path.join(path, 'build-tools') minv = LooseVersion('0') @@ -641,6 +651,8 @@ def search_path(dir, filename): def get_maven_libs(projs): maven_deps = [] for proj in projs: + print("---> Current Project: %s" % proj) + str = open_as_text(os.path.join(proj, 'build.gradle')) str = remove_comments(str) @@ -650,9 +662,15 @@ def get_maven_libs(projs): # compile的格式: # compile 'com.facebook.fresco:fresco:0.6.0+' + # compile 'me.chunyu.android g7json 0.1.1@jar' for mvndep in re.findall(r'''compile\s+['"](.+:.+:.+)(?:@*)?['"]''', depends): + if mvndep.endswith("@jar"): + mvndep = mvndep[:-4] + # compile 'me.chunyu.android g7json 0.1.1@jar' -- compile 'me.chunyu.android g7json 0.1.1' mvndeps = mvndep.split(':') + if not mvndeps in maven_deps: + print("---> Deps: %s" % (" ".join(mvndeps))) maven_deps.append(mvndeps) return maven_deps @@ -667,6 +685,11 @@ def get_maven_jars(libs): # 1. ~/.gralde/caches gradle_home = os.path.join(os.path.expanduser('~'), '.gradle', 'caches') + # extras/android/m2repository + # com.android.support appcompat-v7 20.0.0 + # me.chunyu.android g7anno-lib 0.4.1.4@aar + # me.chunyu.android countly 15.6.2 + # me.chunyu.android cyauth 0.2.2 for dirpath, dirnames, files in os.walk(gradle_home): # search in ~/.gradle/**/GROUP_ID/ARTIFACT_ID/VERSION/**/*.jar # libs的格式? @@ -691,11 +714,11 @@ def get_maven_jars(libs): maxdir = subd if maxdir: maven_path_prefix.append(os.path.join(dir1, maxdir)) + for dirprefix in maven_path_prefix: if dirpath.startswith(dirprefix): for fn in files: - if fn.endswith('.jar') and not fn.startswith('.') and not fn.endswith( - '-sources.jar') and not fn.endswith('-javadoc.jar'): + if fn.endswith('.jar') and not fn.startswith('.') and not fn.endswith('-sources.jar') and not fn.endswith('-javadoc.jar'): jars.append(os.path.join(dirpath, fn)) break return jars @@ -864,6 +887,8 @@ def get_dir_mtime(adir): print('android.jar not found !!!\nUse local.properties or set ANDROID_HOME env') exit(7) + support_annotation_jar = get_support_annotation_jar(sdkdir) + # 4. 获取工程内的所有依赖的项目的路径 deps = deps_list(dir) @@ -1018,6 +1043,7 @@ def get_dir_mtime(adir): with open(os.path.join(bindir, 'res.zip'), 'rb') as fp: curl(URL_PUSH_RES, body=fp.read(), exitcode=11) + srcModified = True if srcModified: vmversion = curl(URL_VM_VERSION, ignoreError=True) if vmversion == None: @@ -1033,7 +1059,7 @@ def get_dir_mtime(adir): print(Fore.RED + "更新 classes.dex 文件..." + Fore.RESET) launcher = curl(URL_LAUNCH, exitcode=13) - # 获取所有的 jar 文件 + # 获取所有的 jar 文件(添加到classpath中) classpath = [android_jar] for dep in adeps: dlib = libdir(dep) @@ -1044,6 +1070,10 @@ def get_dir_mtime(adir): # jars from maven cache maven_libs = get_maven_libs(adeps) + print("Mvn Libs:") + for l in maven_libs: + print (" ".join(l)) + maven_libs_cache_file = os.path.join(bindir, 'cache-javac-maven.json') maven_libs_cache = {} if os.path.isfile(maven_libs_cache_file): @@ -1052,11 +1082,12 @@ def get_dir_mtime(adir): maven_libs_cache = json.load(fp) except: pass - if maven_libs_cache.get('version') != 1 or not maven_libs_cache.get( - 'from') or sorted(maven_libs_cache['from']) != sorted(maven_libs): + + if maven_libs_cache.get('version') != 1 or not maven_libs_cache.get('from') or sorted(maven_libs_cache['from']) != sorted(maven_libs): if os.path.isfile(maven_libs_cache_file): os.remove(maven_libs_cache_file) maven_libs_cache = {} + maven_jars = [] if maven_libs_cache: maven_jars = maven_libs_cache.get('jars') @@ -1070,6 +1101,14 @@ def get_dir_mtime(adir): pass if maven_jars: classpath.extend(maven_jars) + + # 添加注解 + if support_annotation_jar: + classpath.append(support_annotation_jar) + + print("Mvn maven_jars:") + print ("\n".join(maven_jars)) + # aars from exploded-aar darr = os.path.join(dir, 'build', 'intermediates', 'exploded-aar') # TODO: use the max version @@ -1080,9 +1119,7 @@ def get_dir_mtime(adir): if fn.endswith('.jar'): classpath.append(os.path.join(dirpath, fn)) # R.class - classesdir = search_path(os.path.join(dir, 'build', 'intermediates', 'classes'), - launcher and launcher.replace('.', - os.path.sep) + '.class' or '$') + classesdir = search_path(os.path.join(dir, 'build', 'intermediates', 'classes'), launcher and launcher.replace('.', os.path.sep) + '.class' or '$') classpath.append(classesdir) binclassesdir = os.path.join(bindir, 'classes') From c74935377ef1b155c86ba344820890b30ef14254 Mon Sep 17 00:00:00 2001 From: feiwang Date: Wed, 13 Apr 2016 22:37:31 +0800 Subject: [PATCH 10/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86cast.py?= =?UTF-8?q?=E5=AF=B9=E4=BB=A3=E7=A0=81=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cast.py b/cast.py index 568bc24..9a0e42a 100755 --- a/cast.py +++ b/cast.py @@ -1043,7 +1043,7 @@ def get_dir_mtime(adir): with open(os.path.join(bindir, 'res.zip'), 'rb') as fp: curl(URL_PUSH_RES, body=fp.read(), exitcode=11) - srcModified = True + # srcModified = True if srcModified: vmversion = curl(URL_VM_VERSION, ignoreError=True) if vmversion == None: From 6150847220cda6d076f77f43d7503ee1919a9359 Mon Sep 17 00:00:00 2001 From: feiwang Date: Wed, 13 Apr 2016 22:57:20 +0800 Subject: [PATCH 11/11] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86cast.py?= =?UTF-8?q?=E5=AF=B9=E4=BB=A3=E7=A0=81=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index 6f2e094..deddc3a 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,6 @@ ## 2. 如何和Android Studio集成呢? -* 安装插件(安装一次就OK) - 1. Download Android Studio / Intellij plugin - 2. In Android Studio, go to `Preferences` > `Plugins` > `Install plugin from disk...` - 3. Choose the downloaded file from step #1 to install the plugin. - * After restart, you should find a button at right of the run section: - * ![TOOLBAR](images/sc_toolbar.png) - * Android Project & Build System Changes * 添加依赖 ```gradle @@ -58,7 +51,7 @@ public class MyApplication extends Application { * 运行 * 正常运行Android项目 - * 修改资源或者Java代码, 然后: `./cast.py` 或点击: Android Studio`工具栏上的按钮` + * 修改资源或者Java代码, 然后: `./cast.py` (`不建议安装Android Studio插件`,用起来不爽) ```bash cd 或项目root目录 python cast.py