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/README.md b/README.md index 15829f7..deddc3a 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,127 @@ -*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集成呢? + +* 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 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() } } diff --git a/cast.py b/cast.py old mode 100644 new mode 100755 index 3025272..9a0e42a --- a/cast.py +++ b/cast.py @@ -1,10 +1,7 @@ #!/usr/bin/python - -__author__ = 'mmin18' -__version__ = '1.50922' -__plugin__ = '1' - -from subprocess import Popen, PIPE, check_call +# -*- coding:utf-8 -*- +import glob +from subprocess import Popen, PIPE from distutils.version import LooseVersion import argparse import sys @@ -15,12 +12,25 @@ import shutil import json import zipfile +import urllib2 + +# 颜色高亮 +from colorama import init + +init() +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): 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,37 +44,45 @@ 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'] + + 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 if code and exitcode: code = exitcode if callback: - callback(args, code, output, err) + callback(args, code, output, err) return output + +# 访问URL: GET/POST def curl(url, body=None, ignoreError=False, exitcode=1): - import sys + print ("URL: %s%s%s" % (Fore.MAGENTA, url, Fore.RESET)) 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 @@ -72,20 +90,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 +130,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,29 +146,48 @@ def __deps_list_eclipse(list, project): if not absdep in list: 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 = [] + # 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): 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): path = os.path.abspath(os.path.join(path, os.path.pardir)) b = True deps = [] + for idep in ideps: + # 获取 idep的完整路径 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,146 +195,172 @@ def __deps_list_gradle(list, project): list.append(dep) 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): + # 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): - if not is_gradle_project(dir): - apkpath = os.path.join(dir,'bin') - else: - apkpath = os.path.join(dir,'build','outputs','apk') - #Get the lastmodified *.apk file + # 获取最新的apk + # apk的路径: + # bin/*.apk + # build/outputs/apk/*.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 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 +373,27 @@ 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) + +# 返回项目: 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 - 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,35 +401,41 @@ def libdir(dir): else: 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 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,25 +444,30 @@ 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')): - 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)) @@ -390,12 +476,15 @@ 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 platforms = os.path.join(path, 'platforms') if not os.path.isdir(platforms): return None + api = 0 result = None for pd in os.listdir(platforms): @@ -405,20 +494,42 @@ def get_android_jar(path): 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') + + # 停止选择(设置 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 = os.name=='nt' and 'aapt.exe' or 'aapt' + # 首先获取: execname + 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') + + # 实现版本号比较 minv = LooseVersion('0') minp = None for pn in os.listdir(btpath): @@ -428,8 +539,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 = '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 +554,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 +586,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 = 'javac' if dir and os.path.isfile(os.path.join(dir, 'bin', execname)): return os.path.join(dir, 'bin', execname) @@ -480,42 +601,34 @@ 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): 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 +647,59 @@ def search_path(dir, filename): else: return os.path.join(dir, 'debug') + 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) + + # 获取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+' + # 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 + +# 获取 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') + + # 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的格式? + # ["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,10 +710,11 @@ 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: @@ -581,39 +723,85 @@ def get_maven_jars(libs): break return jars -def scan_port(adbpath, pnlist, projlist): + +def scan_port(adbpaths, pnlist, projlist): + """ + 返回可用的 <端口, project_dir, packagename> + :param adbpath: + :param pnlist: + :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 - 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做端口映射 + command = [] + command.extend(adbpaths) + command.extend(['forward', 'tcp:%d' % try_port, 'tcp:%d' % try_port]) + cexec(command) + + # 2. 然后 pnlist + # projlist + 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(URL_STATE % 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: + command = [] + command.extend(adbpaths) + command.extend(['forward', '--remove', 'tcp:%d' % (41128 + i)]) + cexec(command, callback=None) 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 = '.' sdkdir = None jdkdir = None + device = None 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') 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 @@ -621,9 +809,14 @@ def scan_port(adbpath, pnlist, projlist): jdkdir = args.jdk if args.project: dir = args.project + if args.device: + 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 if not sdkdir: sdkdir = get_android_sdk(dir) if not sdkdir: @@ -631,73 +824,113 @@ 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) - port, dir, packagename = scan_port(adbpath, pnlist, projlist) + # 增加设备选择 + if device: + adbpaths = [adbpath, "-s", device] + else: + adbpaths = [adbpath] + + # 1. 进行端口扫描 + # 如果同时运行多个apk, 如何知道哪一个apk是当前正在调试的呢?) appstate + port, dir, packagename = scan_port(adbpaths, 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) + 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) - is_gradle = is_gradle_project(dir) + # 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 + 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 # 将资源推送给手机 + 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) + + support_annotation_jar = get_support_annotation_jar(sdkdir) + + # 4. 获取工程内的所有依赖的项目的路径 deps = deps_list(dir) - bindir = is_gradle and 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 - 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): if fn.endswith('.apk') and not '-androidTest' in fn: fpath = os.path.join(rdir, fn) lastBuild = max(lastBuild, os.path.getmtime(fpath)) + + # 7. 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): - 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) + assetdirs.append(adir) # 整个asset dir都放在里面 + + latestResModified = max(latestResModified, latestModified) + + # B. 获取 resource 的变化情况 rdir = resdir(dep) if rdir: for subd in os.listdir(rdir): @@ -706,12 +939,17 @@ 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,57 +958,73 @@ 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__)) - 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 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) + # 从apk中解压缩: assets 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) - aaptargs = [aaptpath, 'package', '-f', '--auto-add-overlay', '-F', os.path.join(bindir, 'res.zip')] + + # 生成 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') 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) @@ -778,17 +1032,21 @@ def scan_port(adbpath, 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) - cexec(aaptargs,exitcode=18) + print(Fore.RED + "更新 res.zip 文件..." + Fore.RESET) + 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) + + # srcModified = True 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 +1056,10 @@ 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) + print(Fore.RED + "更新 classes.dex 文件..." + Fore.RESET) + launcher = curl(URL_LAUNCH, exitcode=13) + # 获取所有的 jar 文件(添加到classpath中) classpath = [android_jar] for dep in adeps: dlib = libdir(dep) @@ -807,49 +1067,60 @@ 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) + 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): + 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) + + # 添加注解 + 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 + 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) @@ -863,6 +1134,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 +1143,13 @@ 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) + + + 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,20 +1158,25 @@ 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) + 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: - 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') - 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) + # 工作结束 + 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)) \ No newline at end of file + print('finished in %dms' % (elapsetime * 1000)) 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/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 71cdb9f..49f5794 100644 --- a/library/src/com/github/mmin18/layoutcast/LayoutCast.java +++ b/library/src/com/github/mmin18/layoutcast/LayoutCast.java @@ -19,29 +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."); @@ -49,8 +59,11 @@ public static void init(Context context) { } OverrideContext.initApplication(app); + + // 修改系统的InlaterService BootInflater.initApplication(app); + // 启动完毕了,则修改: res.ped --> res.apk(下次重启就不要了) if (res.length() > 0) { try { File f = new File(dir, "res.apk"); @@ -70,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 b911f1f..f9206bf 100644 --- a/library/src/com/github/mmin18/layoutcast/ResetActivity.java +++ b/library/src/com/github/mmin18/layoutcast/ResetActivity.java @@ -22,12 +22,15 @@ 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.."); setContentView(tv); createTime = SystemClock.uptimeMillis(); + // 直接关闭, 不用考虑启动的问题,直接手动启动也可以,无碍大局 ready = getIntent().getBooleanExtra("reset", false); if (ready) { reset(); @@ -54,12 +57,14 @@ public void reset() { private final Runnable reset = new Runnable() { @Override public void run() { + // 自杀之后呢? android.os.Process.killProcess(Process.myPid()); } }; @Override public void onBackPressed() { + // 如果多次返回就直接关闭 if (back++ > 0) { if (ready) { reset.run(); diff --git a/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java b/library/src/com/github/mmin18/layoutcast/context/OverrideContext.java index 1412891..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,13 +91,17 @@ 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); } - Field fResources = ContextThemeWrapper.class - .getDeclaredField("mResources"); + // ContextThemeWrapper ? + // origin 的真实类型? + // 将orig.mResources 设置为 null origin.mTheme 设置为 null + // + Field fResources = ContextThemeWrapper.class.getDeclaredField("mResources"); fResources.setAccessible(true); fResources.set(orig, null); @@ -94,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()) { @@ -128,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; @@ -162,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) { @@ -192,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); } }; @@ -205,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(); } } @@ -218,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; @@ -240,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/IdProfileBuilder.java b/library/src/com/github/mmin18/layoutcast/server/IdProfileBuilder.java index a13e752..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); } @@ -33,11 +35,12 @@ 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())) { + // 如何导出资源文件呢? + // 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) { if (start == 0 || i < start) { @@ -50,6 +53,8 @@ private void buildIds(StringBuilder out, Class clazz) throws Exception { } } + // Type + // EntryName for (int i = start; i > 0 && i <= end; i++) { out.append(" Rclazz) throws Exception { StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append("\n"); + + // 处理各类资源 for (String type : types) { Class clazz = null; try { @@ -104,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(" 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()); - - 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(); - - 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); - - 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(); - - 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; - } - - 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) { - } - } - } - - 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(); - } + public static final int PORT_FROM = 41128; + public static Application app; + final Context context; + + File latestPushResFile; // 最新的资源文件 + + 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 { + // 1. 获取Context的pacakgename + if (path.equalsIgnoreCase("/packagename")) { + response.setContentTypeText(); + // 范围当前Context的PackageName + response.write(context.getPackageName().getBytes("utf-8")); + return; + } + + // 2. 获取App State: 2 表示正常前台运行, 只有处于前台运行的程序才能修改代码和资源 + if (path.equalsIgnoreCase("/appstate")) { + response.setContentTypeText(); + response.write(String.valueOf(OverrideContext.getApplicationState()).getBytes("utf-8")); + return; + } + + // 2. 获取vm版本: 2.xx 表示支持 ART + // 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; + } + + // 3. 获取: + 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); + + // 获取launcher的界面 + response.setContentTypeText(); + response.write(ri.activityInfo.name.getBytes("utf-8")); + return; + } + + // pushres 如何处理呢? + if (("post".equalsIgnoreCase(method) || "put".equalsIgnoreCase(method)) && path.equalsIgnoreCase("/pushres")) { + File dir = new File(context.getCacheDir(), "lcast"); + dir.mkdir(); + + // lcast/ + // dex.ped + // res.ped + // xxxx.apk + File dex = new File(dir, "dex.ped"); + + // 如果存在: dex.ped, 那么采用: res.ped, 否则采用: apk(什么逻辑呢?) + 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"); + } + + // 通过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(); + latestPushResFile = 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(); + + // 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; + } + + // 如何重启呢? + 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) { + // 代码修改了,临时修改: latestPushResFile, 然后重启代码, 防止: res.ped被删除 + if (latestPushResFile != null) { + File f = new File(dir, "res.ped"); + 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); + response.setStatusCode(b ? 200 : 500); + } else { + + // 没有代码的修改 + Resources res = ResUtils.getResources(app, latestPushResFile); + OverrideContext.setGlobalResources(res); + + response.setStatusCode(200); + 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")); + 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); + // 获取Apk的信息: + // size, lastModified, md5 + JSONObject result = new JSONObject(); + result.put("size", apkFile.length()); + result.put("lastModified", apkFile.lastModified()); + + 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(); + + result.put("md5", byteArrayToHex(md5.digest())); + response.setStatusCode(200); + response.setContentTypeJson(); + response.write(result.toString().getBytes("utf-8")); + return; + } + + // 获取原始的apk数据 + if ("/apkraw".equalsIgnoreCase(path)) { + ApplicationInfo ai = app.getApplicationInfo(); + + // 将apk直接下载下来 + 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); + + JarFile jarFile = new JarFile(apkFile); + // 获取其中的fileinfo + 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(); + + 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中读取raw数据 + 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内的所有的apk文件 + 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); + } + } + } + + // 递归删除文件其中的apk文件 + // + 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..a349e43 100644 --- a/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java +++ b/library/src/com/github/mmin18/layoutcast/util/ArtUtils.java @@ -14,33 +14,60 @@ */ 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); + + // XXX: ClassLoader: cl.pathList + + + // 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); + + // 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); + 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); + } + // dexElements = [dexElements2[0], dexElements...] + + // 修改系统的: dexElements + 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/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 955d532..3ed60c9 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">