diff --git a/aapt_linux b/aapt_linux new file mode 100755 index 0000000..f6cadbb Binary files /dev/null and b/aapt_linux differ diff --git a/aapt_mac b/aapt_mac new file mode 100755 index 0000000..92fc631 Binary files /dev/null and b/aapt_mac differ diff --git a/aapt_win.exe b/aapt_win.exe new file mode 100755 index 0000000..a9ca930 Binary files /dev/null and b/aapt_win.exe differ diff --git a/apk_module_config.xml b/apk_module_config.xml new file mode 100644 index 0000000..348ca2f --- /dev/null +++ b/apk_module_config.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d28c38c --- /dev/null +++ b/build.gradle @@ -0,0 +1,52 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + //jcenter() + maven { url "http://mirrors.ibiblio.org/maven2"} + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +ext{ + + //这是工程根目录 + ctripRoot = project(":").projectDir + + // local.properties 来自于AS自动创建 + // 可以保存所有本地工程配置 + // 不允许上传git库 + Properties properties = new Properties() + properties.load(project.rootProject.file('local.properties').newDataInputStream()) + + // 从系统环境变量中或者local.properties配置文件中读取SDK位置 + if(System.getenv("ANDROID_HOME")!=null){ + sdkDir = System.getenv("ANDROID_HOME") + } + else{ // 在local.properties中定义 + sdkDir = properties.getProperty('sdk.dir') + } + + //Debug代码:开发人员可以手动改为false,这样工程就是标准Android工程,可供开发调试。 + //改为true,请使用 gradle assembleRelease bundleRelease repackAll 命令打出多apk的release包。 + solidMode = true + //可以在local.properties里修改这个值。添加一行 solidMode=false 即可。 + solidModeConfigValue = properties.getProperty('solidMode') + if('true'.equalsIgnoreCase(solidModeConfigValue)){ + solidMode = true + } + else if ('false'.equalsIgnoreCase(solidModeConfigValue)){ + solidMode = false + } +} + +allprojects { + repositories { + jcenter() + } +} diff --git a/bundle/.gitignore b/bundle/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/bundle/.gitignore @@ -0,0 +1 @@ +/build diff --git a/bundle/AndroidManifest.xml b/bundle/AndroidManifest.xml new file mode 100644 index 0000000..382fa2c --- /dev/null +++ b/bundle/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/bundle/assets/.gitignore b/bundle/assets/.gitignore new file mode 100644 index 0000000..eb03f4b --- /dev/null +++ b/bundle/assets/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore + diff --git a/bundle/build.gradle b/bundle/build.gradle new file mode 100644 index 0000000..3540858 --- /dev/null +++ b/bundle/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.library' +apply from: '../global_config.gradle' +version "1.0" +android { + defaultConfig { + versionCode 1 + versionName project.version + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/bundle/proguard-rules.pro b/bundle/proguard-rules.pro new file mode 100644 index 0000000..f0baf96 --- /dev/null +++ b/bundle/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/yb.wang/Downloads/android_SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/bundle/res/values/strings.xml b/bundle/res/values/strings.xml new file mode 100644 index 0000000..85a73ab --- /dev/null +++ b/bundle/res/values/strings.xml @@ -0,0 +1,3 @@ + + bundle + diff --git a/bundle/src/ctrip/android/bundle/framework/Bundle.java b/bundle/src/ctrip/android/bundle/framework/Bundle.java new file mode 100755 index 0000000..9c44fa0 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/framework/Bundle.java @@ -0,0 +1,18 @@ +package ctrip.android.bundle.framework; + +import java.io.InputStream; + +public interface Bundle { + + + long getBundleId(); + + + String getLocation(); + + + int getState(); + + + void update(InputStream inputStream) throws BundleException; +} diff --git a/bundle/src/ctrip/android/bundle/framework/BundleCore.java b/bundle/src/ctrip/android/bundle/framework/BundleCore.java new file mode 100644 index 0000000..7cd6ef4 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/framework/BundleCore.java @@ -0,0 +1,190 @@ +package ctrip.android.bundle.framework; + +import android.app.Application; +import android.content.res.Resources; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import ctrip.android.bundle.hack.AndroidHack; +import ctrip.android.bundle.hack.SysHacks; +import ctrip.android.bundle.log.Logger; +import ctrip.android.bundle.log.LoggerFactory; +import ctrip.android.bundle.runtime.BundleInstalledListener; +import ctrip.android.bundle.runtime.DelegateResources; +import ctrip.android.bundle.runtime.InstrumentationHook; +import ctrip.android.bundle.runtime.RuntimeArgs; + +/** + * Created by yb.wang on 15/1/5. + * Bundle机制外部核心类 + * 采用单例模式封装了外部调用方法 + */ +public class BundleCore { + + public static final String LIB_PATH = "assets/baseres/"; + protected static BundleCore instance; + static final Logger log; + private List bundleDelayListeners; + private List bundleSyncListeners; + + static { + log = LoggerFactory.getLogcatLogger("BundleCore"); + + } + + private BundleCore() { + bundleDelayListeners = new ArrayList(); + bundleSyncListeners = new ArrayList(); + } + + public static synchronized BundleCore getInstance() { + if (instance == null) + instance = new BundleCore(); + return instance; + } + + + public void ConfigLogger(boolean isOpenLog, int level) { + LoggerFactory.isNeedLog = isOpenLog; + LoggerFactory.minLevel = Logger.LogLevel.getValue(level); + } + + public void init(Application application) throws Exception { + SysHacks.defineAndVerify(); + RuntimeArgs.androidApplication = application; + RuntimeArgs.delegateResources = application.getResources(); + AndroidHack.injectInstrumentationHook(new InstrumentationHook(AndroidHack.getInstrumentation(), application.getBaseContext())); + } + + public void startup(Properties properties) { + try { + Framework.startup(properties); + } catch (Exception e) { + log.log("Bundle Dex installation failure", Logger.LogLevel.ERROR, e); + throw new RuntimeException("Bundle dex installation failed (" + e.getMessage() + ")."); + } + } + + public void run() { + try { + + log.log("run", Logger.LogLevel.ERROR); + for (Bundle bundle : BundleCore.getInstance().getBundles()) { + + BundleImpl bundleImpl = (BundleImpl) bundle; + try { + bundleImpl.optDexFile(); + } catch (Exception e) { + e.printStackTrace(); + log.log("Error while dexopt >>>", Logger.LogLevel.ERROR, e); + } + } + notifySyncBundleListers(); + DelegateResources.newDelegateResources(RuntimeArgs.androidApplication, RuntimeArgs.delegateResources); + + } catch (Exception e) { + Log.e("Bundleinstall", "Bundle Dex installation failure", e); + throw new RuntimeException("Bundle dex installation failed (" + e.getMessage() + ")."); + } + System.setProperty("BUNDLES_INSTALLED", "true"); + } + + + private void notifyDelayBundleListers() { + if (!bundleDelayListeners.isEmpty()) { + for (BundleInstalledListener bundleInstalledListener : bundleDelayListeners) { + bundleInstalledListener.onBundleInstalled(); + } + } + + } + + private void notifySyncBundleListers() { + if (!bundleSyncListeners.isEmpty()) { + for (BundleInstalledListener bundleInstalledListener : bundleSyncListeners) { + bundleInstalledListener.onBundleInstalled(); + } + } + } + + public Bundle getBundle(String bundleName) { + return Framework.getBundle(bundleName); + } + + public Bundle installBundle(String location, InputStream inputStream) throws BundleException { + return Framework.installNewBundle(location, inputStream); + } + + + public void updateBundle(String location, InputStream inputStream) throws BundleException { + Bundle bundle = Framework.getBundle(location); + if (bundle != null) { + bundle.update(inputStream); + return; + } + throw new BundleException("Could not update bundle " + location + ", because could not find it"); + } + + + public void uninstallBundle(String location) throws BundleException { + Bundle bundle = Framework.getBundle(location); + if (bundle != null) { + BundleImpl bundleImpl = (BundleImpl) bundle; + try { + bundleImpl.getArchive().purge(); + + } catch (Exception e) { + log.log("uninstall bundle error: " + location + e.getMessage(), Logger.LogLevel.ERROR); + } + } + } + + public List getBundles() { + return Framework.getBundles(); + } + + public Resources getDelegateResources() { + return RuntimeArgs.delegateResources; + } + + + + public File getBundleFile(String location) { + Bundle bundle = Framework.getBundle(location); + return bundle != null ? ((BundleImpl) bundle).archive.getArchiveFile() : null; + } + + public InputStream openAssetInputStream(String location, String fileName) throws IOException { + Bundle bundle = Framework.getBundle(location); + return bundle != null ? ((BundleImpl) bundle).archive.openAssetInputStream(fileName) : null; + } + + public InputStream openNonAssetInputStream(String location, String str2) throws IOException { + Bundle bundle = Framework.getBundle(location); + return bundle != null ? ((BundleImpl) bundle).archive.openNonAssetInputStream(str2) : null; + } + + public void addBundleDelayListener(BundleInstalledListener bundleListener) { + bundleDelayListeners.add(bundleListener); + } + + public void removeBundleDelayListener(BundleInstalledListener bundleListener) { + bundleDelayListeners.remove(bundleListener); + } + + public void addBundleSyncListener(BundleInstalledListener bundleListener) { + bundleSyncListeners.add(bundleListener); + } + + public void removeBundleSyncListener(BundleInstalledListener bundleListener) { + bundleSyncListeners.remove(bundleListener); + } + + +} diff --git a/bundle/src/ctrip/android/bundle/framework/BundleException.java b/bundle/src/ctrip/android/bundle/framework/BundleException.java new file mode 100755 index 0000000..461a175 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/framework/BundleException.java @@ -0,0 +1,19 @@ +package ctrip.android.bundle.framework; + +public class BundleException extends Exception { + private transient Throwable throwable; + + public BundleException(String str, Throwable th) { + super(str); + this.throwable = th; + } + + public BundleException(String str) { + super(str); + this.throwable = null; + } + + public Throwable getNestedException() { + return this.throwable; + } +} diff --git a/bundle/src/ctrip/android/bundle/framework/BundleImpl.java b/bundle/src/ctrip/android/bundle/framework/BundleImpl.java new file mode 100644 index 0000000..c7af0a4 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/framework/BundleImpl.java @@ -0,0 +1,163 @@ +package ctrip.android.bundle.framework; + + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import ctrip.android.bundle.framework.storage.Archive; +import ctrip.android.bundle.framework.storage.BundleAchive; +import ctrip.android.bundle.log.Logger; +import ctrip.android.bundle.log.LoggerFactory; + +/** + * Created by yb.wang on 14/12/31. + * Bundle接口实现类,管理Bundle的生命周期。 + * meta文件存储BundleId,Location等 + */ +public final class BundleImpl implements Bundle { + static final Logger log; + Archive archive; + final File bundleDir; + final String location; + final long bundleID; + int state; + //是否dex优化 + volatile boolean isOpt; + + static { + log = LoggerFactory.getLogcatLogger("BundleImpl"); + } + + BundleImpl(File bundleDir) throws Exception { + this.isOpt = false; + + + DataInputStream dataInputStream = new DataInputStream(new FileInputStream(new File(bundleDir, "meta"))); + this.bundleID = dataInputStream.readLong(); + this.location = dataInputStream.readUTF(); + + dataInputStream.close(); + + + this.bundleDir = bundleDir; + try { + this.archive = new BundleAchive(bundleDir); + Framework.bundles.put(this.location, this); + + + } catch (Exception e) { + new BundleException("Could not load bundle " + this.location, e.getCause()); + } + } + + BundleImpl(File bundleDir, String location, long bundleID, InputStream inputStream) throws BundleException { + this.isOpt = false; + this.bundleID = bundleID; + this.location = location; + this.bundleDir = bundleDir; + if (inputStream == null) { + throw new BundleException("Arg InputStream is null.Bundle:" + location); + + } else { + try { + this.archive = new BundleAchive(bundleDir, inputStream); + } catch (Exception e) { + Framework.deleteDirectory(bundleDir); + throw new BundleException("Can not install bundle " + location, e); + } + } + this.updateMetadata(); + Framework.bundles.put(location, this); + + } + + + public boolean getIsOpt() { + return this.isOpt; + } + + public Archive getArchive() { + return this.archive; + } + + + @Override + public long getBundleId() { + return this.bundleID; + } + + @Override + public String getLocation() { + return this.location; + } + + + @Override + public int getState() { + return this.state; + } + + + @Override + public synchronized void update(InputStream inputStream) throws BundleException { + try { + this.archive.newRevision(this.bundleDir, inputStream); + } catch (Throwable e) { + throw new BundleException("Could not update bundle " + toString(), e); + } + } + + + public synchronized void optDexFile() throws Exception { + if (!isOpt) { + long startTime = System.currentTimeMillis(); + getArchive().optDexFile(); + isOpt = true; + log.log("执行:" + getLocation() + ",时间-----" + String.valueOf(System.currentTimeMillis() - startTime), Logger.LogLevel.ERROR); + } + } + + public synchronized void purge() throws BundleException { + try { + getArchive().purge(); + } catch (Throwable e) { + throw new BundleException("Could not purge bundle " + toString(), e); + } + } + + void updateMetadata() { + File file = new File(this.bundleDir, "meta"); + DataOutputStream dataOutputStream; + try { + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + FileOutputStream fileOutputStream = new FileOutputStream(file); + dataOutputStream = new DataOutputStream(fileOutputStream); + dataOutputStream.writeLong(this.bundleID); + dataOutputStream.writeUTF(this.location); + + dataOutputStream.flush(); + fileOutputStream.getFD().sync(); + if (dataOutputStream != null) + try { + dataOutputStream.close(); + } catch (IOException ex) { + ex.printStackTrace(); + } + } catch (Throwable e) { + log.log("Could not save meta data " + file.getAbsolutePath(), Logger.LogLevel.ERROR, e); + } + + } + + public String toString() { + return "Bundle [" + this.bundleID + "]: " + this.location; + } + +} diff --git a/bundle/src/ctrip/android/bundle/framework/Framework.java b/bundle/src/ctrip/android/bundle/framework/Framework.java new file mode 100644 index 0000000..ab79fcf --- /dev/null +++ b/bundle/src/ctrip/android/bundle/framework/Framework.java @@ -0,0 +1,234 @@ +package ctrip.android.bundle.framework; + +import android.os.Build; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +import ctrip.android.bundle.log.Logger; +import ctrip.android.bundle.log.LoggerFactory; +import ctrip.android.bundle.runtime.RuntimeArgs; + + +/** + * Created by yb.wang on 14/12/31. + * 框架包含自身SystemBundle + * 1.管理各个Bundle的启动,更新,卸载 + * 2.提供框架启动Runtime + */ +public final class Framework { + static final Logger log; + + public static final String SYMBOL_SEMICOLON = ";"; + + private static String BASEDIR = null; + private static String BUNDLE_LOCATION = null; + static String STORAGE_LOCATION; + static Map bundles; + + private static long nextBundleID; + static Properties properties; + + static { + log = LoggerFactory.getLogcatLogger("Framework"); + bundles = new ConcurrentHashMap<>(); + nextBundleID = 1; + + } + + + private Framework() { + } + + static void startup(Properties pros) throws BundleException { + if (properties == null) { + properties = new Properties(); + } + properties = pros; + startup(); + } + + static void startup() throws BundleException { + + log.log("*------------------------------------*", Logger.LogLevel.DBUG); + log.log(" Ctrip Bundle on " + Build.MODEL + "|" + Build.CPU_ABI + "starting...", Logger.LogLevel.DBUG); + log.log("*------------------------------------*", Logger.LogLevel.DBUG); + + long currentTimeMillis = System.currentTimeMillis(); + initialize(); + launch(); + boolean isInit = getProperty("ctrip.bundle.init", false); + if (isInit) { + File file = new File(STORAGE_LOCATION); + if (file.exists()) { + log.log("Purging Storage ...", Logger.LogLevel.DBUG); + deleteDirectory(file); + } + file.mkdirs(); + + } else { + storeProfile(); + } + + long endTimeMillis = System.currentTimeMillis() - currentTimeMillis; + + log.log("*------------------------------------*", Logger.LogLevel.DBUG); + log.log(" Framework " + (isInit ? "restarted" : "start") + " in " + endTimeMillis + " ms", Logger.LogLevel.DBUG); + log.log("*------------------------------------*", Logger.LogLevel.DBUG); + + + } + + public static List getBundles() { + List arrayList = new ArrayList(bundles.size()); + synchronized (bundles) { + arrayList.addAll(bundles.values()); + } + return arrayList; + } + + public static Bundle getBundle(String str) { + return bundles.get(str); + } + + public static Bundle getBundle(long id) { + synchronized (bundles) { + for (Bundle bundle : bundles.values()) { + if (bundle.getBundleId() == id) { + return bundle; + } + } + return null; + } + } + + + private static void initialize() { + File filesDir = RuntimeArgs.androidApplication.getFilesDir(); + BASEDIR = properties.getProperty("ctrip.android.bundle.basedir", filesDir.getAbsolutePath()); + + } + + private static void launch() { + + STORAGE_LOCATION = properties.getProperty("ctrip.android.bundle.storage", properties.getProperty("ctrip.android.bundle.framework.dir", BASEDIR + File.separatorChar + "storage")) + File.separatorChar; + } + + + public static boolean getProperty(String str, boolean defaultValue) { + if (properties == null) { + return defaultValue; + } + String str2 = (String) properties.get(str); + return str2 != null ? Boolean.valueOf(str2).booleanValue() : defaultValue; + } + + public static int getProperty(String str, int defaultValue) { + if (properties == null) return defaultValue; + String str2 = (String) properties.get(str); + return str2 != null ? Integer.parseInt(str2) : defaultValue; + } + + public static String getProperty(String str) { + return properties == null ? null : (String) properties.get(str); + } + + public static String getProperty(String str, String defaultValue) { + return properties == null ? defaultValue : (String) properties.get(str); + } + + private static void storeProfile() { + BundleImpl[] bundleImplArr = getBundles().toArray(new BundleImpl[bundles.size()]); + for (BundleImpl bundleImpl : bundleImplArr) { + bundleImpl.updateMetadata(); + } + storeMetadata(); + } + + private static void storeMetadata() { + try { + DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream(new File(STORAGE_LOCATION, "meta"))); + + dataOutputStream.writeLong(nextBundleID); + + dataOutputStream.flush(); + dataOutputStream.close(); + } catch (Throwable e) { + log.log("Could not save meta data.", Logger.LogLevel.ERROR, e); + } + } + + private static int restoreProfile() { + try { + log.log("Restoring profile", Logger.LogLevel.DBUG); + File file = new File(STORAGE_LOCATION, "meta"); + if (file.exists()) { + DataInputStream dataInputStream = new DataInputStream(new FileInputStream(file)); + int readInt = dataInputStream.readInt(); + nextBundleID = dataInputStream.readLong(); + dataInputStream.close(); + File file2 = new File(STORAGE_LOCATION); + File[] listFiles = file2.listFiles(); + int i = 0; + while (i < listFiles.length) { + if (listFiles[i].isDirectory() && new File(listFiles[i], "meta").exists()) { + try { + String location = new BundleImpl(listFiles[i]).location; + log.log("RESTORED BUNDLE " + location, Logger.LogLevel.DBUG); + } catch (Exception e) { + log.log(e.getMessage(), Logger.LogLevel.ERROR, e.getCause()); + } + } + i++; + } + return readInt; + } +// System.out.println("Profile not found, performing clean start ..."); + log.log("Profile not found, performing clean start ...", Logger.LogLevel.DBUG); + return -1; + } catch (Exception e2) { + e2.printStackTrace(); + return 0; + } + } + + + public static void deleteDirectory(File file) { + if (file != null) { + File[] listFiles = file.listFiles(); + for (int i = 0; i < listFiles.length; i++) { + if (listFiles[i].isDirectory()) { + deleteDirectory(listFiles[i]); + } else { + listFiles[i].delete(); + } + } + file.delete(); + } + } + + + + static BundleImpl installNewBundle(String location, InputStream inputStream) throws BundleException { + BundleImpl bundleImpl = (BundleImpl) getBundle(location); + if (bundleImpl != null) { + return bundleImpl; + } + long j = nextBundleID; + nextBundleID = 1 + j; + bundleImpl = new BundleImpl(new File(STORAGE_LOCATION, String.valueOf(j)), location, j, inputStream); + storeMetadata(); + return bundleImpl; + } + + +} diff --git a/bundle/src/ctrip/android/bundle/framework/storage/Archive.java b/bundle/src/ctrip/android/bundle/framework/storage/Archive.java new file mode 100644 index 0000000..6442820 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/framework/storage/Archive.java @@ -0,0 +1,81 @@ +package ctrip.android.bundle.framework.storage; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * Created by yb.wang on 14/12/31. + */ + + +/** + * Bundle 存储接口 + */ +public interface Archive { + + /** + * 关闭存储 + */ + void close(); + + + /** + * 获取Bundle下的文件 + * + * @return File类型 + */ + File getArchiveFile(); + + + BundleArchiveRevision getCurrentRevision(); + + + boolean isBundleInstalled(); + + /** + * 是否已经被Dex优化过 + * + * @return boolean + */ + boolean isDexOpted(); + + /** + * 优化dex文件 + */ + void optDexFile() throws Exception; + + /** + * 清理文件 + * @throws Exception + */ + void purge() throws Exception; + + + /** + * 创建新Bundle存储 + * @param storageFile 存储文件 + * @param inputStream 需要新建的目标文件流 + * @return + * @throws IOException + */ + BundleArchiveRevision newRevision(File storageFile, InputStream inputStream) throws IOException; + + /** + * 打开Asset目录文件 + * @param fileName + * @return 文件流 + * @throws IOException + */ + InputStream openAssetInputStream(String fileName) throws IOException; + + /** + * 打开非Asset目录文件:如 res/drawable-mdpi/icon.png + * @param fileName 文件相对路径:res/drawable-mdpi/icon.png + * @return 文件流 + * @throws IOException + */ + InputStream openNonAssetInputStream(String fileName) throws IOException; + + +} diff --git a/bundle/src/ctrip/android/bundle/framework/storage/BundleAchive.java b/bundle/src/ctrip/android/bundle/framework/storage/BundleAchive.java new file mode 100644 index 0000000..375a7ae --- /dev/null +++ b/bundle/src/ctrip/android/bundle/framework/storage/BundleAchive.java @@ -0,0 +1,127 @@ +package ctrip.android.bundle.framework.storage; + + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.SortedMap; +import java.util.TreeMap; + +import ctrip.android.bundle.framework.Framework; +import ctrip.android.bundle.util.StringUtil; + +/** + * Created by yb.wang on 14/12/31. + *

+ * Bundle 目录结构:version_1,version_2 + */ +public class BundleAchive implements Archive { + + public static final String REVISION_DIREXTORY = "version"; + private static final Long BENGIN_VERSION = 1L; + private File bundleDir; + private final BundleArchiveRevision currentRevision; + private final SortedMap revisionSortedMap; + + public BundleAchive(File file) throws IOException { + this.revisionSortedMap = new TreeMap(); + String[] lists = file.list(); + if (lists != null) { + for (String str : lists) { + if (str.startsWith(REVISION_DIREXTORY)) { + long parseLong = Long.parseLong(StringUtil.subStringAfter(str, "_")); + if (parseLong > 0) { + this.revisionSortedMap.put(parseLong, null); + } + } + } + + } + if (revisionSortedMap.isEmpty()) { + throw new IOException("No Valid revisions in bundle archive directory"); + } + this.bundleDir = file; + long longValue = this.revisionSortedMap.lastKey(); + BundleArchiveRevision bundleArchiveRevision = new BundleArchiveRevision(longValue, new File(file, REVISION_DIREXTORY + "_" + String.valueOf(longValue))); + this.revisionSortedMap.put(longValue, bundleArchiveRevision); + this.currentRevision = bundleArchiveRevision; + } + + public BundleAchive(File file, InputStream inputStream) throws IOException { + this.revisionSortedMap = new TreeMap(); + this.bundleDir = file; + BundleArchiveRevision bundleArchiveRevision = new BundleArchiveRevision(BENGIN_VERSION, new File(file, REVISION_DIREXTORY + "_" + String.valueOf(BENGIN_VERSION)), inputStream); + this.revisionSortedMap.put(BENGIN_VERSION, bundleArchiveRevision); + this.currentRevision = bundleArchiveRevision; + } + + + @Override + public BundleArchiveRevision newRevision(File storageFile, InputStream inputStream) throws IOException { + long version = this.revisionSortedMap.lastKey() + 1; + BundleArchiveRevision bundleArchiveRevision = new BundleArchiveRevision(version, new File(storageFile, REVISION_DIREXTORY + "_" + String.valueOf(version)), inputStream); + this.revisionSortedMap.put(version, bundleArchiveRevision); + return bundleArchiveRevision; + + } + + + public BundleArchiveRevision getCurrentRevision() { + return this.currentRevision; + } + + public File getBundleDir() { + return this.bundleDir; + } + + @Override + public void close() { + + } + + @Override + public File getArchiveFile() { + return this.currentRevision.getRevisionFile(); + } + + + @Override + public boolean isBundleInstalled() { + return this.currentRevision.isBundleInstalled(); + } + + @Override + public boolean isDexOpted() { + return this.currentRevision.isDexOpted(); + } + + @Override + public void optDexFile() throws Exception { + this.currentRevision.optDexFile(); + } + + @Override + public void purge() throws Exception { + + Framework.deleteDirectory(this.currentRevision.getRevisionDir()); + long l = this.revisionSortedMap.lastKey(); + this.revisionSortedMap.clear(); + if (l < 1) { + this.revisionSortedMap.put(0L, this.currentRevision); + } else { + this.revisionSortedMap.put(l - 1, this.currentRevision); + } + } + + + @Override + public InputStream openAssetInputStream(String fileName) throws IOException { + return this.currentRevision.openAssetInputStream(fileName); + } + + @Override + public InputStream openNonAssetInputStream(String fileName) throws IOException { + return this.currentRevision.openNonAssetInputStream(fileName); + } + +} diff --git a/bundle/src/ctrip/android/bundle/framework/storage/BundleArchiveRevision.java b/bundle/src/ctrip/android/bundle/framework/storage/BundleArchiveRevision.java new file mode 100644 index 0000000..e901ae8 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/framework/storage/BundleArchiveRevision.java @@ -0,0 +1,219 @@ +package ctrip.android.bundle.framework.storage; + +import android.content.res.AssetManager; +import android.os.Build; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +import ctrip.android.bundle.hack.SysHacks; +import ctrip.android.bundle.loader.BundlePathLoader; +import ctrip.android.bundle.log.Logger; +import ctrip.android.bundle.log.LoggerFactory; +import ctrip.android.bundle.runtime.RuntimeArgs; +import ctrip.android.bundle.util.APKUtil; +import ctrip.android.bundle.util.StringUtil; + +/** + * Created by yb.wang on 14/12/31. + *

+ * Bundle 存储文件:bundle.zip,bundle.dex + * 采用DexFile 加载 dex文件,并opt释放优化后的dex + * findClass会被BundleClassLoader调用 + */ +public class BundleArchiveRevision { + static final Logger log; + static final String BUNDLE_FILE_NAME = "bundle.zip"; + static final String BUNDEL_DEX_FILE = "bundle.dex"; + static final String FILE_PROTOCOL = "file:"; + static final String REFERENCE_PROTOCOL = "reference:"; + private final long revisionNum; + private File revisionDir; + private File bundleFile; + private String revisionLocation; + + static { + log = LoggerFactory.getLogcatLogger("BundleArchiveRevision"); + } + + + BundleArchiveRevision(long revisionNumber, File file, InputStream inputStream) throws IOException { + this.revisionNum = revisionNumber; + this.revisionDir = file; + if (!this.revisionDir.exists()) { + this.revisionDir.mkdirs(); + } + this.revisionLocation = FILE_PROTOCOL; + this.bundleFile = new File(file, BUNDLE_FILE_NAME); + APKUtil.copyInputStreamToFile(inputStream, this.bundleFile); + updateMetaData(); + + } + + BundleArchiveRevision(long revisionNumber, File file, File file2) throws IOException { + this.revisionNum = revisionNumber; + this.revisionDir = file; + if (!this.revisionDir.exists()) { + this.revisionDir.mkdirs(); + } + if (file2.canWrite()) { + if (isSameDir(file, file2)) { + this.revisionLocation = FILE_PROTOCOL; + this.bundleFile = new File(file, BUNDLE_FILE_NAME); + file2.renameTo(this.bundleFile); + } else { + this.revisionLocation = FILE_PROTOCOL; + this.bundleFile = new File(file, BUNDLE_FILE_NAME); + APKUtil.copyInputStreamToFile(new FileInputStream(file2), this.bundleFile); + } + } else if (Build.HARDWARE.toLowerCase().contains("mt6592") && file2.getName().endsWith(".apk")) { + this.revisionLocation = FILE_PROTOCOL; + this.bundleFile = new File(file, BUNDLE_FILE_NAME); + Runtime.getRuntime().exec(String.format("ln -s %s %s", new Object[]{file2.getAbsolutePath(), this.bundleFile.getAbsolutePath()})); + } else if (SysHacks.LexFile == null || SysHacks.LexFile.getmClass() == null) { + this.revisionLocation = REFERENCE_PROTOCOL + file2.getAbsolutePath(); + this.bundleFile = file2; + } else { + this.revisionLocation = FILE_PROTOCOL; + this.bundleFile = new File(file, BUNDLE_FILE_NAME); + APKUtil.copyInputStreamToFile(new FileInputStream(file2), this.bundleFile); + } + updateMetaData(); + } + + BundleArchiveRevision(long revisionNumber, File file) throws IOException { + File fileMeta = new File(file, "meta"); + if (fileMeta.exists()) { + DataInputStream dataInputStream = new DataInputStream(new FileInputStream(fileMeta)); + this.revisionLocation = dataInputStream.readUTF(); + dataInputStream.close(); + this.revisionNum = revisionNumber; + this.revisionDir = file; + if (!this.revisionDir.exists()) { + this.revisionDir.mkdirs(); + } + if (this.revisionLocation.startsWith(REFERENCE_PROTOCOL)) { + this.bundleFile = new File(StringUtil.subStringAfter(this.revisionLocation, REFERENCE_PROTOCOL)); + return; + } else { + this.bundleFile = new File(file, BUNDLE_FILE_NAME); + return; + } + } + throw new IOException("Can not find meta file in " + file.getAbsolutePath()); + } + + void updateMetaData() throws IOException { + + File file = new File(this.revisionDir, "meta"); + DataOutputStream dataOutputStream = null; + try { + if (!file.getParentFile().exists()) { + file.getParentFile().mkdirs(); + } + dataOutputStream = new DataOutputStream(new FileOutputStream(file)); + dataOutputStream.writeUTF(this.revisionLocation); + dataOutputStream.flush(); + + } catch (IOException ex) { + throw new IOException("Can not save meta data " + file.getAbsolutePath()); + } finally { + if (dataOutputStream != null) dataOutputStream.close(); + } + } + + private boolean isSameDir(File file, File file2) { + return StringUtil.equals(StringUtil.subStringBetween(file.getAbsolutePath(), File.separator, File.separator), + StringUtil.subStringBetween(file2.getAbsolutePath(), File.separator, File.separator)); + } + + public long getRevisionNum() { + return this.revisionNum; + } + + public File getRevisionDir() { + return this.revisionDir; + } + + public File getRevisionFile() { + return this.bundleFile; + } + + public boolean isDexOpted() { + return new File(this.revisionDir, BUNDEL_DEX_FILE).exists(); + } + + public boolean isBundleInstalled(){ + if(bundleFile.exists()){ + return verifyZipFile(bundleFile); + } + return false; + } + private boolean verifyZipFile(File file) { + try { + ZipFile zipFile = new ZipFile(file); + try { + zipFile.close(); + return true; + } catch (IOException e) { + log.log("Failed to close zip file: " + file.getAbsolutePath(), Logger.LogLevel.ERROR,e); + } + } catch (ZipException ex) { + log.log("File " + file.getAbsolutePath() + " is not a valid zip file.", Logger.LogLevel.ERROR,ex); + } catch (IOException ex) { + log.log("Got an IOException trying to open zip file: " + file.getAbsolutePath(), Logger.LogLevel.ERROR,ex); + } + return false; + } + + public void optDexFile() throws Exception{ + List files = new ArrayList(); + files.add(this.bundleFile); + BundlePathLoader.installBundleDexs(RuntimeArgs.androidApplication.getClassLoader(), revisionDir, files,false); + } + + public InputStream openAssetInputStream(String fileName) throws IOException { + try { + AssetManager assetManager = AssetManager.class.newInstance(); + if (((Integer) SysHacks.AssetManager_addAssetPath.invoke(assetManager, this.bundleFile.getAbsoluteFile())).intValue() != 0) { + return assetManager.open(fileName); + } + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + public InputStream openNonAssetInputStream(String fileName) throws IOException { + try { + AssetManager assetManager = AssetManager.class.newInstance(); + int intValue = ((Integer) SysHacks.AssetManager_addAssetPath.invoke(assetManager, this.bundleFile.getAbsoluteFile())).intValue(); + if (intValue != 0) { + return assetManager.openNonAssetFd(intValue, fileName).createInputStream(); + } + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + +} diff --git a/bundle/src/ctrip/android/bundle/hack/AndroidHack.java b/bundle/src/ctrip/android/bundle/hack/AndroidHack.java new file mode 100644 index 0000000..51af9e4 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/hack/AndroidHack.java @@ -0,0 +1,139 @@ +package ctrip.android.bundle.hack; + +import android.app.Activity; +import android.app.Application; +import android.app.Instrumentation; +import android.content.ContextWrapper; +import android.content.res.Resources; +import android.os.Handler; +import android.os.Looper; + +import java.lang.ref.WeakReference; +import java.util.Map; + +/** + * Created by yb.wang on 15/1/5. + * Android中的ClassLoader Hack + */ +public class AndroidHack { + private static Object _mLoadedApk; + private static Object _sActivityThread; + + static class ActivityThreadGetter implements Runnable { + ActivityThreadGetter() { + } + + public void run() { + try { + _sActivityThread = SysHacks.ActivityThread_currentActivityThread.invoke(SysHacks.ActivityThread.getmClass(), new Object[0]); + } catch (Exception e) { + e.printStackTrace(); + } + synchronized (SysHacks.ActivityThread_currentActivityThread) { + SysHacks.ActivityThread_currentActivityThread.notify(); + } + } + } + + static { + _sActivityThread = null; + _mLoadedApk = null; + } + + public static Object getActivityThread() throws Exception { + if (_sActivityThread == null) { + if (Thread.currentThread().getId() == Looper.getMainLooper().getThread().getId()) { + _sActivityThread = SysHacks.ActivityThread_currentActivityThread.invoke(null, new Object[0]); + } else { + Handler handler = new Handler(Looper.getMainLooper()); + synchronized (SysHacks.ActivityThread_currentActivityThread) { + handler.post(new ActivityThreadGetter()); + SysHacks.ActivityThread_currentActivityThread.wait(); + } + } + } + return _sActivityThread; + } + + public static Object getLoadedApk(Object obj, String str) throws Exception { + if (_mLoadedApk == null) { + WeakReference weakReference = (WeakReference) ((Map) SysHacks.ActivityThread_mPackages.get(obj)).get(str); + if (weakReference != null) { + _mLoadedApk = weakReference.get(); + } + } + return _mLoadedApk; + } + + public static void injectClassLoader(String str, ClassLoader classLoader) throws Exception { + Object activityThread = getActivityThread(); + if (activityThread == null) { + throw new Exception("Failed to get ActivityThread.sCurrentActivityThread"); + } + activityThread = getLoadedApk(activityThread, str); + if (activityThread == null) { + throw new Exception("Failed to get ActivityThread.mLoadedApk"); + } + SysHacks.LoadedApk_mClassLoader.set(activityThread, classLoader); + } + + public static void injectResources(Application application, Resources resources) throws Exception { + Object activityThread = getActivityThread(); + if (activityThread == null) { + throw new Exception("Failed to get ActivityThread.sCurrentActivityThread"); + } + Object loadedApk = getLoadedApk(activityThread, application.getPackageName()); + if (loadedApk == null) { + throw new Exception("Failed to get ActivityThread.mLoadedApk"); + } + SysHacks.LoadedApk_mResources.set(loadedApk, resources); + SysHacks.ContextImpl_mResources.set(application.getBaseContext(), resources); + SysHacks.ContextImpl_mTheme.set(application.getBaseContext(), null); + } + + public static void injectActivityResources(Activity activity, Resources resources) throws Exception { + Object activityThread = getActivityThread(); + if (activityThread == null) { + throw new Exception("Failed to get ActivityThread.sCurrentActivityThread"); + } + Object loadedApk = getLoadedApk(activityThread, activity.getPackageName()); + if (loadedApk == null) { + throw new Exception("Failed to get ActivityThread.mLoadedApk"); + } + SysHacks.LoadedApk_mResources.set(loadedApk, resources); + SysHacks.ContextImpl_mResources.set(activity.getBaseContext(), resources); + SysHacks.ContextImpl_mTheme.set(activity.getBaseContext(), null); + } + + public static ClassLoader currentClassLoader(String str) throws Exception { + Object activityThread = getActivityThread(); + if (activityThread == null) { + throw new Exception("Failed to get ActivityThread.sCurrentActivityThread"); + } + activityThread = getLoadedApk(activityThread, str); + if (activityThread != null) { + return SysHacks.LoadedApk_mClassLoader.get(activityThread); + } + throw new Exception("Failed to get ActivityThread.mLoadedApk"); + } + + public static Instrumentation getInstrumentation() throws Exception { + Object activityThread = getActivityThread(); + if (activityThread != null) { + return SysHacks.ActivityThread_mInstrumentation.get(activityThread); + } + throw new Exception("Failed to get ActivityThread.sCurrentActivityThread"); + } + + public static void injectInstrumentationHook(Instrumentation instrumentation) throws Exception { + Object activityThread = getActivityThread(); + if (activityThread == null) { + throw new Exception("Failed to get ActivityThread.sCurrentActivityThread"); + } + SysHacks.ActivityThread_mInstrumentation.set(activityThread, instrumentation); + } + + public static void injectContextHook(ContextWrapper contextWrapper, ContextWrapper contextWrapper2) { + SysHacks.ContextWrapper_mBase.set(contextWrapper, contextWrapper2); + } +} diff --git a/bundle/src/ctrip/android/bundle/hack/AssertionArrayException.java b/bundle/src/ctrip/android/bundle/hack/AssertionArrayException.java new file mode 100644 index 0000000..60d8f54 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/hack/AssertionArrayException.java @@ -0,0 +1,75 @@ +package ctrip.android.bundle.hack; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import ctrip.android.bundle.framework.Framework; + +/** + * Created by yb.wang on 15/1/5. + */ +public class AssertionArrayException extends Exception { + private static final long serialVersionUID = 1; + private List mAssertionErr; + + public AssertionArrayException(String str) { + super(str); + this.mAssertionErr = new ArrayList(); + } + + public void addException(Hack.HackDeclaration.HackAssertionException hackAssertionException) { + this.mAssertionErr.add(hackAssertionException); + } + + public void addException(List list) { + this.mAssertionErr.addAll(list); + } + + public List getExceptions() { + return this.mAssertionErr; + } + + public static AssertionArrayException mergeException(AssertionArrayException assertionArrayException, AssertionArrayException assertionArrayException2) { + if (assertionArrayException == null) { + return assertionArrayException2; + } + if (assertionArrayException2 == null) { + return assertionArrayException; + } + AssertionArrayException assertionArrayException3 = new AssertionArrayException(assertionArrayException.getMessage() + Framework.SYMBOL_SEMICOLON + assertionArrayException2.getMessage()); + assertionArrayException3.addException(assertionArrayException.getExceptions()); + assertionArrayException3.addException(assertionArrayException2.getExceptions()); + return assertionArrayException3; + } + + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + for (Hack.HackDeclaration.HackAssertionException hackAssertionException : this.mAssertionErr) { + stringBuilder.append(hackAssertionException.toString()).append(Framework.SYMBOL_SEMICOLON); + try { + if (hackAssertionException.getCause() instanceof NoSuchFieldException) { + Field[] declaredFields = hackAssertionException.getHackedClass().getDeclaredFields(); + stringBuilder.append(hackAssertionException.getHackedClass().getName()).append(".").append(hackAssertionException.getHackedFieldName()).append(Framework.SYMBOL_SEMICOLON); + for (Field field : declaredFields) { + stringBuilder.append(field.getName()).append(File.separator); + } + } else if (hackAssertionException.getCause() instanceof NoSuchMethodException) { + Method[] declaredMethods = hackAssertionException.getHackedClass().getDeclaredMethods(); + stringBuilder.append(hackAssertionException.getHackedClass().getName()).append("->").append(hackAssertionException.getHackedMethodName()).append(Framework.SYMBOL_SEMICOLON); + for (int i = 0; i < declaredMethods.length; i++) { + if (hackAssertionException.getHackedMethodName().equals(declaredMethods[i].getName())) { + stringBuilder.append(declaredMethods[i].toGenericString()).append(File.separator); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + stringBuilder.append("@@@@"); + } + return stringBuilder.toString(); + } +} diff --git a/bundle/src/ctrip/android/bundle/hack/Hack.java b/bundle/src/ctrip/android/bundle/hack/Hack.java new file mode 100644 index 0000000..86f5771 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/hack/Hack.java @@ -0,0 +1,269 @@ +package ctrip.android.bundle.hack; + + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Created by yb.wang on 14/12/31. + * Hack--反射机制反射后包装的形式:类,方法,字段 + */ +public class Hack { + private static AssertionFailureHandler sFailureHandler; + + public static interface AssertionFailureHandler { + boolean onAssertionFailure(HackDeclaration.HackAssertionException hackAssertionException); + } + + public static abstract class HackDeclaration { + + public static class HackAssertionException extends Throwable { + private static final long serialVersionUID = 1; + private Class mHackedClass; + private String mHackedFieldName; + private String mHackedMethodName; + + public HackAssertionException(String str) { + super(str); + } + + public HackAssertionException(Exception exception) { + super(exception); + } + + public String toString() { + return getCause() != null ? getClass().getName() + ": " + getCause() : super.toString(); + } + + public Class getHackedClass() { + return this.mHackedClass; + } + + public void setHackedClass(Class cls) { + this.mHackedClass = cls; + } + + public String getHackedMethodName() { + return this.mHackedMethodName; + } + + public void setHackedMethodName(String str) { + this.mHackedMethodName = str; + } + + public String getHackedFieldName() { + return this.mHackedFieldName; + } + + public void setHackedFieldName(String str) { + this.mHackedFieldName = str; + } + } + } + + public static class HackedClass { + protected Class mClass; + + public HackedField staticField(String str) throws HackDeclaration.HackAssertionException { + return new HackedField(this.mClass, str, 8); + } + + public HackedField field(String str) throws HackDeclaration.HackAssertionException { + return new HackedField(this.mClass, str, 0); + } + + public HackedMethod staticMethod(String str, Class... clsArr) throws HackDeclaration.HackAssertionException { + return new HackedMethod(this.mClass, str, clsArr, 8); + } + + public HackedMethod method(String str, Class... clsArr) throws HackDeclaration.HackAssertionException { + return new HackedMethod(this.mClass, str, clsArr, 0); + } + + public HackedConstructor constructor(Class... clsArr) throws HackDeclaration.HackAssertionException { + return new HackedConstructor(this.mClass, clsArr); + } + + public HackedClass(Class cls) { + this.mClass = cls; + } + + public Class getmClass() { + return this.mClass; + } + } + + public static class HackedConstructor { + protected Constructor mConstructor; + + HackedConstructor(Class cls, Class[] clsArr) throws HackDeclaration.HackAssertionException { + if (cls != null) { + try { + this.mConstructor = cls.getDeclaredConstructor(clsArr); + } catch (Exception e) { + HackDeclaration.HackAssertionException hackAssertionException = new HackDeclaration.HackAssertionException(e); + hackAssertionException.setHackedClass(cls); + Hack.fail(hackAssertionException); + } + } + } + + public Object getInstance(Object... objArr) throws IllegalArgumentException { + Object obj = null; + this.mConstructor.setAccessible(true); + try { + obj = this.mConstructor.newInstance(objArr); + } catch (Exception e) { + e.printStackTrace(); + } + return obj; + } + } + + public static class HackedField { + private final Field mField; + + public HackedField ofGenericType(Class cls) throws HackDeclaration.HackAssertionException { + if (!(this.mField == null || cls.isAssignableFrom(this.mField.getType()))) { + Hack.fail(new HackDeclaration.HackAssertionException(new ClassCastException(this.mField + " is not of type " + cls))); + } + return this; + } + + public HackedField ofType(Class cls) throws HackDeclaration.HackAssertionException { + if (!(this.mField == null || cls.isAssignableFrom(this.mField.getType()))) { + Hack.fail(new HackDeclaration.HackAssertionException(new ClassCastException(this.mField + " is not of type " + cls))); + } + return this; + } + + public HackedField ofType(String str) throws HackDeclaration.HackAssertionException { + HackedField ofType = null; + try { + ofType = ofType((Class) Class.forName(str)); + } catch (Exception e) { + Hack.fail(new HackDeclaration.HackAssertionException(e)); + } + return ofType; + } + + public T get(C c) { + try { + return (T) this.mField.get(c); + } catch (IllegalAccessException e) { + e.printStackTrace(); + return null; + } + } + + public void set(C c, Object obj) { + try { + this.mField.set(c, obj); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + public void hijack(C c, Interception.InterceptionHandler interceptionHandler) { + T obj = get(c); + if (obj == null) { + throw new IllegalStateException("Cannot hijack null"); + } + set(c, Interception.proxy(obj, (Interception.InterceptionHandler) interceptionHandler, obj.getClass().getInterfaces())); + } + + HackedField(Class cls, String str, int i) throws HackDeclaration.HackAssertionException { + Field field = null; + if (cls == null) { + this.mField = null; + return; + } + try { + field = cls.getDeclaredField(str); + if (i > 0 && (field.getModifiers() & i) != i) { + Hack.fail(new HackDeclaration.HackAssertionException(field + " does not match modifiers: " + i)); + } + field.setAccessible(true); + } catch (Exception e) { + HackDeclaration.HackAssertionException hackAssertionException = new HackDeclaration.HackAssertionException(e); + hackAssertionException.setHackedClass(cls); + hackAssertionException.setHackedFieldName(str); + Hack.fail(hackAssertionException); + } finally { + this.mField = field; + } + } + + public Field getField() { + return this.mField; + } + } + + public static class HackedMethod { + protected final Method mMethod; + + public Object invoke(Object obj, Object... objArr) throws IllegalArgumentException, InvocationTargetException { + Object obj2 = null; + try { + obj2 = this.mMethod.invoke(obj, objArr); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return obj2; + } + + HackedMethod(Class cls, String str, Class[] clsArr, int i) throws HackDeclaration.HackAssertionException { + Method method = null; + if (cls == null) { + this.mMethod = null; + return; + } + try { + method = cls.getDeclaredMethod(str, clsArr); + if (i > 0 && (method.getModifiers() & i) != i) { + Hack.fail(new HackDeclaration.HackAssertionException(method + " does not match modifiers: " + i)); + } + method.setAccessible(true); + } catch (Exception e) { + HackDeclaration.HackAssertionException hackAssertionException = new HackDeclaration.HackAssertionException(e); + hackAssertionException.setHackedClass(cls); + hackAssertionException.setHackedMethodName(str); + Hack.fail(hackAssertionException); + } finally { + this.mMethod = method; + } + } + + public Method getMethod() { + return this.mMethod; + } + } + + public static HackedClass into(Class cls) { + return new HackedClass(cls); + } + + public static HackedClass into(String str) throws HackDeclaration.HackAssertionException { + try { + return new HackedClass(Class.forName(str)); + } catch (Exception e) { + fail(new HackDeclaration.HackAssertionException(e)); + return new HackedClass(null); + } + } + + private static void fail(HackDeclaration.HackAssertionException hackAssertionException) throws HackDeclaration.HackAssertionException { + if (sFailureHandler == null || !sFailureHandler.onAssertionFailure(hackAssertionException)) { + throw hackAssertionException; + } + } + + public static void setAssertionFailureHandler(AssertionFailureHandler assertionFailureHandler) { + sFailureHandler = assertionFailureHandler; + } + + private Hack() { + } +} diff --git a/bundle/src/ctrip/android/bundle/hack/Interception.java b/bundle/src/ctrip/android/bundle/hack/Interception.java new file mode 100644 index 0000000..efbe15b --- /dev/null +++ b/bundle/src/ctrip/android/bundle/hack/Interception.java @@ -0,0 +1,57 @@ +package ctrip.android.bundle.hack; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * Created by yb.wang on 15/1/5. + */ +public class Interception { + + private static interface Intercepted { + } + + public static abstract class InterceptionHandler implements InvocationHandler { + private T mDelegatee; + + public Object invoke(Object obj, Method method, Object[] objArr) throws Throwable { + Object obj2 = null; + try { + obj2 = method.invoke(delegatee(), objArr); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e2) { + e2.printStackTrace(); + } catch (InvocationTargetException e3) { + throw e3.getTargetException(); + } + return obj2; + } + + protected T delegatee() { + return this.mDelegatee; + } + + void setDelegatee(T t) { + this.mDelegatee = t; + } + } + + public static T proxy(Object obj, Class cls, InterceptionHandler interceptionHandler) throws IllegalArgumentException { + if (obj instanceof Intercepted) { + return (T) obj; + } + interceptionHandler.setDelegatee((T) obj); + return (T) Proxy.newProxyInstance(Interception.class.getClassLoader(), new Class[]{cls, Intercepted.class}, interceptionHandler); + } + + public static T proxy(Object obj, InterceptionHandler interceptionHandler, Class... clsArr) throws IllegalArgumentException { + interceptionHandler.setDelegatee((T) obj); + return (T) Proxy.newProxyInstance(Interception.class.getClassLoader(), clsArr, interceptionHandler); + } + + private Interception() { + } +} diff --git a/bundle/src/ctrip/android/bundle/hack/SysHacks.java b/bundle/src/ctrip/android/bundle/hack/SysHacks.java new file mode 100644 index 0000000..49f42e0 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/hack/SysHacks.java @@ -0,0 +1,211 @@ +package ctrip.android.bundle.hack; + +import android.app.Application; +import android.app.Instrumentation; +import android.app.Service; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.os.Build; +import android.view.ContextThemeWrapper; + +import java.util.ArrayList; +import java.util.Map; + +import ctrip.android.bundle.hack.Hack.HackedClass; +import ctrip.android.bundle.hack.Hack.HackedField; +import ctrip.android.bundle.hack.Hack.HackedMethod; +import ctrip.android.bundle.log.Logger; +import ctrip.android.bundle.log.LoggerFactory; +import dalvik.system.DexClassLoader; + +/** + * Created by yb.wang on 14/12/31. + * Hack 系统的功能:包括类加载机制,资源加载,Context等 + */ +public class SysHacks extends Hack.HackDeclaration implements Hack.AssertionFailureHandler { + public static HackedClass ActivityThread; + public static HackedMethod ActivityThread_currentActivityThread; + public static HackedField> ActivityThread_mAllApplications; + public static HackedField ActivityThread_mInstrumentation; + public static HackedField> ActivityThread_mPackages; + public static HackedField ActivityThread_sPackageManager; + public static HackedClass Application; + public static HackedMethod Application_attach; + public static HackedClass AssetManager; + public static HackedMethod AssetManager_addAssetPath; + public static HackedClass ClassLoader; + public static HackedMethod ClassLoader_findLibrary; + public static HackedClass ContextImpl; + public static HackedField ContextImpl_mResources; + public static HackedField ContextImpl_mTheme; + public static HackedClass ContextThemeWrapper; + public static HackedField ContextThemeWrapper_mBase; + public static HackedField ContextThemeWrapper_mResources; + public static HackedField ContextThemeWrapper_mTheme; + public static HackedClass ContextWrapper; + public static HackedField ContextWrapper_mBase; + public static HackedClass DexClassLoader; + public static HackedMethod DexClassLoader_findClass; + public static ArrayList GeneratePackageInfoList; + public static ArrayList GetPackageInfoList; + public static HackedClass IPackageManager; + public static HackedClass LexFile; + public static HackedMethod LexFile_close; + public static HackedMethod LexFile_loadClass; + public static HackedMethod LexFile_loadLex; + public static HackedClass LoadedApk; + public static HackedField LoadedApk_mAppDir; + public static HackedField LoadedApk_mApplication; + public static HackedField LoadedApk_mBaseClassLoader; + public static HackedField LoadedApk_mClassLoader; + public static HackedField LoadedApk_mResDir; + public static HackedField LoadedApk_mResources; + public static HackedClass Resources; + public static HackedField Resources_mAssets; + public static HackedClass Service; + public static HackedClass Instrumentation; + + public static boolean sIsIgnoreFailure; + public static boolean sIsReflectAvailable; + public static boolean sIsReflectChecked; + private AssertionArrayException mExceptionArray; + static final Logger log; + + public SysHacks() { + this.mExceptionArray = null; + } + + static { + log = LoggerFactory.getLogcatLogger("SysHacks"); + sIsReflectAvailable = false; + sIsReflectChecked = false; + sIsIgnoreFailure = false; + GeneratePackageInfoList = new ArrayList(); + GetPackageInfoList = new ArrayList(); + } + + public static boolean defineAndVerify() throws AssertionArrayException { + if (sIsReflectChecked) { + return sIsReflectAvailable; + } + SysHacks atlasHacks = new SysHacks(); + try { + Hack.setAssertionFailureHandler(atlasHacks); + if (Build.VERSION.SDK_INT == 11) { + atlasHacks.onAssertionFailure(new HackAssertionException("Hack Assertion Failed: Android OS Version 11")); + } + allClasses(); + allConstructors(); + allFields(); + allMethods(); + if (atlasHacks.mExceptionArray != null) { + sIsReflectAvailable = false; + throw atlasHacks.mExceptionArray; + } + sIsReflectAvailable = true; + return sIsReflectAvailable; + } catch (Throwable e) { + sIsReflectAvailable = false; + log.log("HackAssertionException", Logger.LogLevel.ERROR, e); + throw new AssertionArrayException("defineAndVerify HackAssertionException"); + } finally { + Hack.setAssertionFailureHandler(null); + sIsReflectChecked = true; + } + } + + public static void allClasses() throws HackAssertionException { + if (Build.VERSION.SDK_INT <= 8) { + LoadedApk = Hack.into("android.app.ActivityThread$PackageInfo"); + } else { + LoadedApk = Hack.into("android.app.LoadedApk"); + } + ActivityThread = Hack.into("android.app.ActivityThread"); + Resources = Hack.into(Resources.class); + Application = Hack.into(Application.class); + AssetManager = Hack.into(AssetManager.class); + IPackageManager = Hack.into("android.content.pm.IPackageManager"); + Service = Hack.into(Service.class); + ContextImpl = Hack.into("android.app.ContextImpl"); + ContextThemeWrapper = Hack.into(ContextThemeWrapper.class); + ContextWrapper = Hack.into("android.content.ContextWrapper"); + sIsIgnoreFailure = true; + ClassLoader = Hack.into(ClassLoader.class); + DexClassLoader = Hack.into(DexClassLoader.class); + LexFile = Hack.into("dalvik.system.LexFile"); + Instrumentation = Hack.into("android.app.Instrumentation"); + sIsIgnoreFailure = false; + } + + public static void allFields() throws HackAssertionException { + + ActivityThread_mInstrumentation = ActivityThread.field("mInstrumentation"); + ActivityThread_mInstrumentation.ofType(Instrumentation.class); + ActivityThread_mAllApplications = ActivityThread.field("mAllApplications"); + ActivityThread_mAllApplications.ofGenericType(ArrayList.class); + ActivityThread_mPackages = ActivityThread.field("mPackages"); + ActivityThread_mPackages.ofGenericType(Map.class); + ActivityThread_sPackageManager = ActivityThread.staticField("sPackageManager").ofType(IPackageManager.getmClass()); + LoadedApk_mApplication = LoadedApk.field("mApplication"); + LoadedApk_mApplication.ofType(Application.class); + LoadedApk_mResources = LoadedApk.field("mResources"); + LoadedApk_mResources.ofType(Resources.class); + LoadedApk_mResDir = LoadedApk.field("mResDir"); + LoadedApk_mResDir.ofType(String.class); + LoadedApk_mClassLoader = LoadedApk.field("mClassLoader"); + LoadedApk_mClassLoader.ofType(ClassLoader.class); + LoadedApk_mBaseClassLoader = LoadedApk.field("mBaseClassLoader"); + LoadedApk_mBaseClassLoader.ofType(ClassLoader.class); + LoadedApk_mAppDir = LoadedApk.field("mAppDir"); + LoadedApk_mAppDir.ofType(String.class); + ContextImpl_mResources = ContextImpl.field("mResources"); + ContextImpl_mResources.ofType(Resources.class); + ContextImpl_mTheme = ContextImpl.field("mTheme"); + ContextImpl_mTheme.ofType(android.content.res.Resources.Theme.class); + sIsIgnoreFailure = true; + ContextThemeWrapper_mBase = ContextThemeWrapper.field("mBase"); + ContextThemeWrapper_mBase.ofType(Context.class); + sIsIgnoreFailure = false; + ContextThemeWrapper_mTheme = ContextThemeWrapper.field("mTheme"); + ContextThemeWrapper_mTheme.ofType(android.content.res.Resources.Theme.class); + try { + if (Build.VERSION.SDK_INT >= 17 && ContextThemeWrapper.getmClass().getDeclaredField("mResources") != null) { + ContextThemeWrapper_mResources = ContextThemeWrapper.field("mResources"); + ContextThemeWrapper_mResources.ofType(Resources.class); + } + } catch (NoSuchFieldException e) { + log.log("Not found ContextThemeWrapper.mResources on VERSION " + Build.VERSION.SDK_INT, Logger.LogLevel.WARN); + } + ContextWrapper_mBase = ContextWrapper.field("mBase"); + ContextWrapper_mBase.ofType(Context.class); + Resources_mAssets = Resources.field("mAssets"); + } + + public static void allMethods() throws HackAssertionException { + ActivityThread_currentActivityThread = ActivityThread.method("currentActivityThread", new Class[0]); + AssetManager_addAssetPath = AssetManager.method("addAssetPath", String.class); + Application_attach = Application.method("attach", Context.class); + ClassLoader_findLibrary = ClassLoader.method("findLibrary", String.class); + if (LexFile != null && LexFile.getmClass() != null) { + LexFile_loadLex = LexFile.method("loadLex", String.class, Integer.TYPE); + LexFile_loadClass = LexFile.method("loadClass", String.class, ClassLoader.class); + LexFile_close = LexFile.method("close", new Class[0]); + DexClassLoader_findClass = DexClassLoader.method("findClass", String.class); + } + } + + public static void allConstructors() throws HackAssertionException { + } + + public boolean onAssertionFailure(HackAssertionException hackAssertionException) { + if (!sIsIgnoreFailure) { + if (this.mExceptionArray == null) { + this.mExceptionArray = new AssertionArrayException("atlas hack assert failed"); + } + this.mExceptionArray.addException(hackAssertionException); + } + return true; + } +} diff --git a/bundle/src/ctrip/android/bundle/loader/BundlePathLoader.java b/bundle/src/ctrip/android/bundle/loader/BundlePathLoader.java new file mode 100644 index 0000000..d28401e --- /dev/null +++ b/bundle/src/ctrip/android/bundle/loader/BundlePathLoader.java @@ -0,0 +1,304 @@ +package ctrip.android.bundle.loader; + +import android.os.Build; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ListIterator; +import java.util.zip.ZipFile; + +import dalvik.system.DexFile; + +/** + * Created by yb.wang on 15/4/22. + */ +public class BundlePathLoader { + + + + static final String TAG = "BundlePathLoader"; + + // private static final String OLD_SECONDARY_FOLDER_NAME = "secondary-dexes"; + +// private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + +// "secondary-dexes"; +// +// private static final int MAX_SUPPORTED_SDK_VERSION = 20; +// +// +// private static final Set installedApk = new HashSet(); + + private BundlePathLoader() { + } + + + public static void installBundleDexs(ClassLoader loader, File dexDir, List files,boolean isHotFix) + throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,InstantiationException, + InvocationTargetException, NoSuchMethodException, IOException { + if (!files.isEmpty()) { + if (Build.VERSION.SDK_INT >= 23) { + V23.install(loader, files, dexDir,isHotFix); + }else if (Build.VERSION.SDK_INT >= 19) { + V19.install(loader, files, dexDir,isHotFix); + } else if (Build.VERSION.SDK_INT >= 14) { + V14.install(loader, files, dexDir,isHotFix); + } else { + V4.install(loader, files,isHotFix); + } + } + } + +// /** +// * Returns whether all files in the list are valid zip files. If {@code files} is empty, then +// * returns true. +// */ +// private static boolean checkValidZipFiles(List files) { +// for (File file : files) { +// if (!MultiDexExtractor.verifyZipFile(file)) { +// return false; +// } +// } +// return true; +// } + + /** + * Locates a given field anywhere in the class inheritance hierarchy. + * + * @param instance an object to search the field into. + * @param name field name + * @return a field object + * @throws NoSuchFieldException if the field cannot be located + */ + private static Field findField(Object instance, String name) throws NoSuchFieldException { + for (Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + Field field = clazz.getDeclaredField(name); + + if (!field.isAccessible()) { + field.setAccessible(true); + } + + return field; + } catch (NoSuchFieldException e) { + // ignore and search next + } + } + + throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass()); + } + + /** + * Locates a given method anywhere in the class inheritance hierarchy. + * + * @param instance an object to search the method into. + * @param name method name + * @param parameterTypes method parameter types + * @return a method object + * @throws NoSuchMethodException if the method cannot be located + */ + private static Method findMethod(Object instance, String name, Class... parameterTypes) + throws NoSuchMethodException { + for (Class clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + + Method method = clazz.getDeclaredMethod(name, parameterTypes); + + + if (!method.isAccessible()) { + method.setAccessible(true); + } + + return method; + } catch (NoSuchMethodException e) { + // ignore and search next + } + } + + throw new NoSuchMethodException("Method " + name + " with parameters " + + Arrays.asList(parameterTypes) + " not found in " + instance.getClass()); + } + + /** + * Replace the value of a field containing a non null array, by a new array containing the + * elements of the original array plus the elements of extraElements. + * + * @param instance the instance whose field is to be modified. + * @param fieldName the field to modify. + * @param extraElements elements to append at the end of the array. + */ + private static void expandFieldArray(Object instance, String fieldName, + Object[] extraElements,boolean isHotFix) throws NoSuchFieldException, IllegalArgumentException, + IllegalAccessException { + synchronized (BundlePathLoader.class) { + Field jlrField = findField(instance, fieldName); + Object[] original = (Object[]) jlrField.get(instance); + Object[] combined = (Object[]) Array.newInstance( + original.getClass().getComponentType(), original.length + extraElements.length); + if(isHotFix) { + System.arraycopy(extraElements, 0, combined, 0, extraElements.length); + System.arraycopy(original, 0, combined, extraElements.length, original.length); + }else { + System.arraycopy(original, 0, combined, 0, original.length); + System.arraycopy(extraElements, 0, combined, original.length, extraElements.length); + } + jlrField.set(instance, combined); + } + } + + + private static final class V23 { + + private static void install(ClassLoader loader, List additionalClassPathEntries, + File optimizedDirectory,boolean isHotFix) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, InvocationTargetException, NoSuchMethodException, InstantiationException { + + Field pathListField = findField(loader, "pathList"); + Object dexPathList = pathListField.get(loader); + Field dexElement = findField(dexPathList, "dexElements"); + Class elementType = dexElement.getType().getComponentType(); + Method loadDex = findMethod(dexPathList, "loadDexFile", File.class, File.class); + Object dex = loadDex.invoke(dexPathList, additionalClassPathEntries.get(0), optimizedDirectory); + Constructor constructor = elementType.getConstructor(File.class, boolean.class, File.class, DexFile.class); + Object element = constructor.newInstance(new File(""), false, additionalClassPathEntries.get(0), dex); + Object[] newEles=new Object[1]; + newEles[0]=element; + expandFieldArray(dexPathList, "dexElements",newEles,isHotFix); + } + + } + + /** + * Installer for platform versions 19. + */ + private static final class V19 { + + private static void install(ClassLoader loader, List additionalClassPathEntries, + File optimizedDirectory,boolean isHotFix) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.BaseDexClassLoader. We modify its + * dalvik.system.DexPathList pathList field to append additional DEX + * file entries. + */ + Field pathListField = findField(loader, "pathList"); + Object dexPathList = pathListField.get(loader); + ArrayList suppressedExceptions = new ArrayList(); + expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, + new ArrayList(additionalClassPathEntries), optimizedDirectory, + suppressedExceptions),isHotFix); + if (suppressedExceptions.size() > 0) { + for (IOException e : suppressedExceptions) { + Log.w(TAG, "Exception in makeDexElement", e); + + } + throw suppressedExceptions.get(0); + } + } + + /** + * A wrapper around + * {@code private static final dalvik.system.DexPathList#makeDexElements}. + */ + private static Object[] makeDexElements( + Object dexPathList, ArrayList files, File optimizedDirectory, + ArrayList suppressedExceptions) + throws IllegalAccessException, InvocationTargetException, + NoSuchMethodException { + Method makeDexElements = + findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, + ArrayList.class); + + return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, + suppressedExceptions); + } + } + + /** + * Installer for platform versions 14, 15, 16, 17 and 18. + */ + private static final class V14 { + + private static void install(ClassLoader loader, List additionalClassPathEntries, + File optimizedDirectory,boolean isHotFix) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, InvocationTargetException, NoSuchMethodException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.BaseDexClassLoader. We modify its + * dalvik.system.DexPathList pathList field to append additional DEX + * file entries. + */ + Field pathListField = findField(loader, "pathList"); + Object dexPathList = pathListField.get(loader); + expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, + new ArrayList(additionalClassPathEntries), optimizedDirectory),isHotFix); + } + + /** + * A wrapper around + * {@code private static final dalvik.system.DexPathList#makeDexElements}. + */ + private static Object[] makeDexElements( + Object dexPathList, ArrayList files, File optimizedDirectory) + throws IllegalAccessException, InvocationTargetException, + NoSuchMethodException { + Method makeDexElements = + findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class); + + return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory); + } + } + + /** + * Installer for platform versions 4 to 13. + */ + private static final class V4 { + private static void install(ClassLoader loader, List additionalClassPathEntries,boolean isHotFix) + throws IllegalArgumentException, IllegalAccessException, + NoSuchFieldException, IOException { + /* The patched class loader is expected to be a descendant of + * dalvik.system.DexClassLoader. We modify its + * fields mPaths, mFiles, mZips and mDexs to append additional DEX + * file entries. + */ + int extraSize = additionalClassPathEntries.size(); + + Field pathField = findField(loader, "path"); + + StringBuilder path = new StringBuilder((String) pathField.get(loader)); + String[] extraPaths = new String[extraSize]; + File[] extraFiles = new File[extraSize]; + ZipFile[] extraZips = new ZipFile[extraSize]; + DexFile[] extraDexs = new DexFile[extraSize]; + for (ListIterator iterator = additionalClassPathEntries.listIterator(); + iterator.hasNext(); ) { + File additionalEntry = iterator.next(); + String entryPath = additionalEntry.getAbsolutePath(); + path.append(':').append(entryPath); + int index = iterator.previousIndex(); + extraPaths[index] = entryPath; + extraFiles[index] = additionalEntry; + extraZips[index] = new ZipFile(additionalEntry); + extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); + } + + pathField.set(loader, path.toString()); + expandFieldArray(loader, "mPaths", extraPaths,isHotFix); + expandFieldArray(loader, "mFiles", extraFiles,isHotFix); + expandFieldArray(loader, "mZips", extraZips,isHotFix); + expandFieldArray(loader, "mDexs", extraDexs,isHotFix); + } + } + + +} diff --git a/bundle/src/ctrip/android/bundle/log/LogcatLogger.java b/bundle/src/ctrip/android/bundle/log/LogcatLogger.java new file mode 100644 index 0000000..fcfa02e --- /dev/null +++ b/bundle/src/ctrip/android/bundle/log/LogcatLogger.java @@ -0,0 +1,65 @@ +package ctrip.android.bundle.log; + +import android.util.Log; + +/** + * Created by yb.wang on 15/1/23. + */ +public class LogcatLogger implements Logger { + + private final String tag; + + public LogcatLogger(String _tag) { + this.tag = _tag; + } + + public LogcatLogger(Class classType) { + this(classType.getSimpleName()); + } + + @Override + public void log(String msg, LogLevel level) { + if (!LoggerFactory.isNeedLog) return; + if (level.getLevel() < LoggerFactory.minLevel.getLevel()) return; + switch (level) { + case DBUG: + Log.d(tag, msg); + break; + case INFO: + Log.i(tag, msg); + break; + case WARN: + Log.w(tag, msg); + break; + case ERROR: + Log.e(tag, msg); + break; + default: + break; + } + } + + @Override + public void log(String msg, LogLevel level, Throwable th) { + if (!LoggerFactory.isNeedLog) return; + if (level.getLevel() < LoggerFactory.minLevel.getLevel()) return; + switch (level) { + case DBUG: + Log.d(tag, msg, th); + break; + case INFO: + Log.i(tag, msg, th); + break; + case WARN: + Log.w(tag, msg, th); + break; + case ERROR: + Log.e(tag, msg, th); + break; + default: + break; + } + } + + +} diff --git a/bundle/src/ctrip/android/bundle/log/Logger.java b/bundle/src/ctrip/android/bundle/log/Logger.java new file mode 100644 index 0000000..3e941f3 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/log/Logger.java @@ -0,0 +1,33 @@ +package ctrip.android.bundle.log; + +/** + * Created by yb.wang on 15/1/23. + */ +public interface Logger { + + enum LogLevel { + DBUG(1), INFO(2), WARN(3), ERROR(4); + private int _level; + + private LogLevel(int level) { + _level = level; + } + + public int getLevel() { + return this._level; + } + + public static LogLevel getValue(int level) { + for (LogLevel l : LogLevel.values()) { + if (l.getLevel() == level) { + return l; + } + } + return LogLevel.DBUG; + } + } + + void log(String msg, LogLevel level); + + void log(String msg, LogLevel level, Throwable th); +} diff --git a/bundle/src/ctrip/android/bundle/log/LoggerFactory.java b/bundle/src/ctrip/android/bundle/log/LoggerFactory.java new file mode 100644 index 0000000..3a172a6 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/log/LoggerFactory.java @@ -0,0 +1,29 @@ +package ctrip.android.bundle.log; + +/** + * Created by yb.wang on 15/1/23. + */ +public class LoggerFactory { + + public static boolean isNeedLog; + + public static Logger.LogLevel minLevel; + + static { + isNeedLog = false; + minLevel = Logger.LogLevel.DBUG; + } + + public static Logger getLogcatLogger(String tag) { + return getLogcatLogger(tag, null); + } + + public static Logger getLogcatLogger(Class cls) { + return getLogcatLogger(null, cls); + } + + private static Logger getLogcatLogger(String tag, Class cls) { + return cls != null ? new LogcatLogger((Class) cls) : new LogcatLogger(tag); + } + +} diff --git a/bundle/src/ctrip/android/bundle/runtime/BundleInstalledListener.java b/bundle/src/ctrip/android/bundle/runtime/BundleInstalledListener.java new file mode 100644 index 0000000..9efe5be --- /dev/null +++ b/bundle/src/ctrip/android/bundle/runtime/BundleInstalledListener.java @@ -0,0 +1,8 @@ +package ctrip.android.bundle.runtime; + +/** + * Created by yb.wang on 15/8/7. + */ +public interface BundleInstalledListener { + void onBundleInstalled(); +} diff --git a/bundle/src/ctrip/android/bundle/runtime/ContextImplHook.java b/bundle/src/ctrip/android/bundle/runtime/ContextImplHook.java new file mode 100644 index 0000000..204737e --- /dev/null +++ b/bundle/src/ctrip/android/bundle/runtime/ContextImplHook.java @@ -0,0 +1,39 @@ +package ctrip.android.bundle.runtime; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.AssetManager; +import android.content.res.Resources; + +import ctrip.android.bundle.log.Logger; +import ctrip.android.bundle.log.LoggerFactory; + +/** + * Created by yb.wang on 15/1/6. + * Android Context Hook 挂载载系统的Context中,拦截相应的方法 + */ +public class ContextImplHook extends ContextWrapper { + static final Logger log; + + static { + log = LoggerFactory.getLogcatLogger("ContextImplHook"); + } + + public ContextImplHook(Context context) { + super(context); + + } + + @Override + public Resources getResources() { + log.log("getResources is invoke", Logger.LogLevel.INFO); + return RuntimeArgs.delegateResources; + } + + @Override + public AssetManager getAssets() { + log.log("getAssets is invoke", Logger.LogLevel.INFO); + return RuntimeArgs.delegateResources.getAssets(); + } + +} diff --git a/bundle/src/ctrip/android/bundle/runtime/DelegateResources.java b/bundle/src/ctrip/android/bundle/runtime/DelegateResources.java new file mode 100644 index 0000000..707b219 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/runtime/DelegateResources.java @@ -0,0 +1,71 @@ +package ctrip.android.bundle.runtime; + +import android.app.Application; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.DisplayMetrics; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +import ctrip.android.bundle.framework.Bundle; +import ctrip.android.bundle.framework.BundleImpl; +import ctrip.android.bundle.framework.Framework; +import ctrip.android.bundle.hack.AndroidHack; +import ctrip.android.bundle.hack.SysHacks; +import ctrip.android.bundle.log.Logger; +import ctrip.android.bundle.log.LoggerFactory; + +/** + * Created by yb.wang on 15/1/5. + * 挂载载系统资源中,处理框架资源加载 + */ +public class DelegateResources extends Resources { + static final Logger log; + + static { + log = LoggerFactory.getLogcatLogger("DelegateResources"); + } + + public DelegateResources(AssetManager assets, Resources resources) { + super(assets, resources.getDisplayMetrics(), resources.getConfiguration()); + } + + public static void newDelegateResources(Application application, Resources resources) throws Exception { + List bundles = Framework.getBundles(); + if (bundles != null && !bundles.isEmpty()) { + Resources delegateResources; + List arrayList = new ArrayList(); + arrayList.add(application.getApplicationInfo().sourceDir); + for (Bundle bundle : bundles) { + arrayList.add(((BundleImpl) bundle).getArchive().getArchiveFile().getAbsolutePath()); + } + AssetManager assetManager = AssetManager.class.newInstance(); + for (String str : arrayList) { + SysHacks.AssetManager_addAssetPath.invoke(assetManager, str); + } + //处理小米UI资源 + if (resources == null || !resources.getClass().getName().equals("android.content.res.MiuiResources")) { + delegateResources = new DelegateResources(assetManager, resources); + } else { + Constructor declaredConstructor = Class.forName("android.content.res.MiuiResources").getDeclaredConstructor(new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class}); + declaredConstructor.setAccessible(true); + delegateResources = (Resources) declaredConstructor.newInstance(new Object[]{assetManager, resources.getDisplayMetrics(), resources.getConfiguration()}); + } + RuntimeArgs.delegateResources = delegateResources; + AndroidHack.injectResources(application, delegateResources); + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append("newDelegateResources ["); + for (int i = 0; i < arrayList.size(); i++) { + if (i > 0) { + stringBuffer.append(","); + } + stringBuffer.append(arrayList.get(i)); + } + stringBuffer.append("]"); + log.log(stringBuffer.toString(), Logger.LogLevel.DBUG); + } + } +} diff --git a/bundle/src/ctrip/android/bundle/runtime/InstrumentationHook.java b/bundle/src/ctrip/android/bundle/runtime/InstrumentationHook.java new file mode 100644 index 0000000..b999f0a --- /dev/null +++ b/bundle/src/ctrip/android/bundle/runtime/InstrumentationHook.java @@ -0,0 +1,400 @@ +package ctrip.android.bundle.runtime; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.Application; +import android.app.Fragment; +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.IBinder; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import java.util.List; + +import ctrip.android.bundle.framework.Framework; +import ctrip.android.bundle.hack.SysHacks; +import ctrip.android.bundle.log.Logger; +import ctrip.android.bundle.log.LoggerFactory; +import ctrip.android.bundle.util.StringUtil; + + +/** + * Created by yb.wang on 15/1/5. + * 挂载在系统中的Instrumentation,以拦截相应的方法 + */ +public class InstrumentationHook extends Instrumentation { + static final Logger log; + private Context context; + private Instrumentation mBase; + + private static interface ExecStartActivityCallback { + ActivityResult execStartActivity(); + } + + static { + log = LoggerFactory.getLogcatLogger("InstrumentationHook"); + } + + public InstrumentationHook(Instrumentation instrumentation, Context context) { + this.context = context; + this.mBase = instrumentation; + } + + public ActivityResult execStartActivity(final Context context, final IBinder iBinder, final IBinder iBinder2, final Activity activity, final Intent intent, final int i) { + return execStartActivityInternal(this.context, intent, new ExecStartActivityCallback() { + @Override + public ActivityResult execStartActivity() { + try { + return (ActivityResult) SysHacks.Instrumentation.method("execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class) + .invoke(mBase, context, iBinder, iBinder2, activity, intent, i); + } catch (Throwable ex) { + ex.printStackTrace(); + return null; + } + + + } + }); + } + + @TargetApi(16) + public ActivityResult execStartActivity(final Context context, final IBinder iBinder, final IBinder iBinder2, final Activity activity, final Intent intent, final int i, final Bundle bundle) { + return execStartActivityInternal(this.context, intent, new ExecStartActivityCallback() { + @Override + public ActivityResult execStartActivity() { + try { + Object result = SysHacks.Instrumentation.method("execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class) + .invoke(mBase, context, iBinder, iBinder2, activity, intent, i, bundle); + if (result != null) return (ActivityResult) result; + } catch (Throwable ex) { + ex.printStackTrace(); + + } + return null; + } + }); + } + + @TargetApi(14) + public ActivityResult execStartActivity(final Context context, final IBinder iBinder, final IBinder iBinder2, final Fragment fragment, final Intent intent, final int i) { + return execStartActivityInternal(this.context, intent, new ExecStartActivityCallback() { + @Override + public ActivityResult execStartActivity() { + try { + return (ActivityResult) SysHacks.Instrumentation.method("execStartActivity", Context.class, IBinder.class, IBinder.class, Fragment.class, Intent.class, int.class) + .invoke(mBase, context, iBinder, iBinder2, fragment, intent, i); + } catch (Throwable ex) { + ex.printStackTrace(); + return null; + } + + } + }); + } + + @TargetApi(16) + public ActivityResult execStartActivity(final Context context, final IBinder iBinder, final IBinder iBinder2, final Fragment fragment, final Intent intent, final int i, final Bundle bundle) { + return execStartActivityInternal(this.context, intent, new ExecStartActivityCallback() { + @Override + public ActivityResult execStartActivity() { + try { + return (ActivityResult) SysHacks.Instrumentation.method("execStartActivity", Context.class, IBinder.class, IBinder.class, Fragment.class, Intent.class, int.class, Bundle.class) + .invoke(mBase, context, iBinder, iBinder2, fragment, intent, i, bundle); + } catch (Throwable ex) { + ex.printStackTrace(); + return null; + } + } + }); + } + + private ActivityResult execStartActivityInternal(Context context, Intent intent, ExecStartActivityCallback execStartActivityCallback) { + String packageName; + if (intent.getComponent() != null) { + packageName = intent.getComponent().getPackageName(); + } else { + ResolveInfo resolveActivity = context.getPackageManager().resolveActivity(intent, 0); + if (resolveActivity == null || resolveActivity.activityInfo == null) { + packageName = null; + } else { + packageName = resolveActivity.activityInfo.packageName; + } + } + if (!StringUtil.equals(context.getPackageName(), packageName)) { + return execStartActivityCallback.execStartActivity(); + } + + return execStartActivityCallback.execStartActivity(); + } + + public Activity newActivity(Class cls, Context context, IBinder iBinder, Application application, Intent intent, ActivityInfo activityInfo, CharSequence charSequence, Activity activity, String str, Object obj) throws InstantiationException, IllegalAccessException { + Activity newActivity = this.mBase.newActivity(cls, context, iBinder, application, intent, activityInfo, charSequence, activity, str, obj); + if (RuntimeArgs.androidApplication.getPackageName().equals(activityInfo.packageName) && SysHacks.ContextThemeWrapper_mResources != null) { + SysHacks.ContextThemeWrapper_mResources.set(newActivity, RuntimeArgs.delegateResources); + } + return newActivity; + } + + public Activity newActivity(ClassLoader classLoader, String str, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { + Activity newActivity; + try { + newActivity = this.mBase.newActivity(classLoader, str, intent); + if (SysHacks.ContextThemeWrapper_mResources != null) { + SysHacks.ContextThemeWrapper_mResources.set(newActivity, RuntimeArgs.delegateResources); + } + } catch (ClassNotFoundException e) { + String property = Framework.getProperty("ctrip.android.bundle.welcome", "ctrip.android.view.home.CtripSplashActivity"); + if (StringUtil.isEmpty(property)) { + throw e; + } else { + List runningTasks = ((ActivityManager) this.context.getSystemService(Context.ACTIVITY_SERVICE)).getRunningTasks(1); + if (runningTasks != null && runningTasks.size() > 0 && ((ActivityManager.RunningTaskInfo) runningTasks.get(0)).numActivities > 1) { + if (intent.getComponent() == null) { + intent.setClassName(this.context, str); + } + } + log.log("Could not find activity class: " + str, Logger.LogLevel.WARN); + log.log("Redirect to welcome activity: " + property, Logger.LogLevel.WARN); + newActivity = this.mBase.newActivity(classLoader, property, intent); + } + } + return newActivity; + } + + public void callActivityOnCreate(Activity activity, Bundle bundle) { + if (RuntimeArgs.androidApplication.getPackageName().equals(activity.getPackageName())) { + ContextImplHook contextImplHook = new ContextImplHook(activity.getBaseContext()); + if (!(SysHacks.ContextThemeWrapper_mBase == null || SysHacks.ContextThemeWrapper_mBase.getField() == null)) { + SysHacks.ContextThemeWrapper_mBase.set(activity, contextImplHook); + } + SysHacks.ContextWrapper_mBase.set(activity, contextImplHook); + } + this.mBase.callActivityOnCreate(activity, bundle); + } + + @TargetApi(18) + public UiAutomation getUiAutomation() { + return this.mBase.getUiAutomation(); + } + + public void onCreate(Bundle bundle) { + this.mBase.onCreate(bundle); + } + + public void start() { + this.mBase.start(); + } + + public void onStart() { + this.mBase.onStart(); + } + + public boolean onException(Object obj, Throwable th) { + return this.mBase.onException(obj, th); + } + + public void sendStatus(int i, Bundle bundle) { + this.mBase.sendStatus(i, bundle); + } + + public void finish(int i, Bundle bundle) { + this.mBase.finish(i, bundle); + } + + public void setAutomaticPerformanceSnapshots() { + this.mBase.setAutomaticPerformanceSnapshots(); + } + + public void startPerformanceSnapshot() { + this.mBase.startPerformanceSnapshot(); + } + + public void endPerformanceSnapshot() { + this.mBase.endPerformanceSnapshot(); + } + + public void onDestroy() { + this.mBase.onDestroy(); + } + + public Context getContext() { + return this.mBase.getContext(); + } + + public ComponentName getComponentName() { + return this.mBase.getComponentName(); + } + + public Context getTargetContext() { + return this.mBase.getTargetContext(); + } + + public boolean isProfiling() { + return this.mBase.isProfiling(); + } + + public void startProfiling() { + this.mBase.startProfiling(); + } + + public void stopProfiling() { + this.mBase.stopProfiling(); + } + + public void setInTouchMode(boolean z) { + this.mBase.setInTouchMode(z); + } + + public void waitForIdle(Runnable runnable) { + this.mBase.waitForIdle(runnable); + } + + public void waitForIdleSync() { + this.mBase.waitForIdleSync(); + } + + public void runOnMainSync(Runnable runnable) { + this.mBase.runOnMainSync(runnable); + } + + public Activity startActivitySync(Intent intent) { + return this.mBase.startActivitySync(intent); + } + + public void addMonitor(ActivityMonitor activityMonitor) { + this.mBase.addMonitor(activityMonitor); + } + + public ActivityMonitor addMonitor(IntentFilter intentFilter, ActivityResult activityResult, boolean z) { + return this.mBase.addMonitor(intentFilter, activityResult, z); + } + + public ActivityMonitor addMonitor(String str, ActivityResult activityResult, boolean z) { + return this.mBase.addMonitor(str, activityResult, z); + } + + public boolean checkMonitorHit(ActivityMonitor activityMonitor, int i) { + return this.mBase.checkMonitorHit(activityMonitor, i); + } + + public Activity waitForMonitor(ActivityMonitor activityMonitor) { + return this.mBase.waitForMonitor(activityMonitor); + } + + public Activity waitForMonitorWithTimeout(ActivityMonitor activityMonitor, long j) { + return this.mBase.waitForMonitorWithTimeout(activityMonitor, j); + } + + public void removeMonitor(ActivityMonitor activityMonitor) { + this.mBase.removeMonitor(activityMonitor); + } + + public boolean invokeMenuActionSync(Activity activity, int i, int i2) { + return this.mBase.invokeMenuActionSync(activity, i, i2); + } + + public boolean invokeContextMenuAction(Activity activity, int i, int i2) { + return this.mBase.invokeContextMenuAction(activity, i, i2); + } + + public void sendStringSync(String str) { + this.mBase.sendStringSync(str); + } + + public void sendKeySync(KeyEvent keyEvent) { + this.mBase.sendKeySync(keyEvent); + } + + public void sendKeyDownUpSync(int i) { + this.mBase.sendKeyDownUpSync(i); + } + + public void sendCharacterSync(int i) { + this.mBase.sendCharacterSync(i); + } + + public void sendPointerSync(MotionEvent motionEvent) { + this.mBase.sendPointerSync(motionEvent); + } + + public void sendTrackballEventSync(MotionEvent motionEvent) { + this.mBase.sendTrackballEventSync(motionEvent); + } + + public Application newApplication(ClassLoader classLoader, String str, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { + return this.mBase.newApplication(classLoader, str, context); + } + + public void callApplicationOnCreate(Application application) { + this.mBase.callApplicationOnCreate(application); + } + + public void callActivityOnDestroy(Activity activity) { + this.mBase.callActivityOnDestroy(activity); + } + + public void callActivityOnRestoreInstanceState(Activity activity, Bundle bundle) { + this.mBase.callActivityOnRestoreInstanceState(activity, bundle); + } + + public void callActivityOnPostCreate(Activity activity, Bundle bundle) { + this.mBase.callActivityOnPostCreate(activity, bundle); + } + + public void callActivityOnNewIntent(Activity activity, Intent intent) { + this.mBase.callActivityOnNewIntent(activity, intent); + } + + public void callActivityOnStart(Activity activity) { + this.mBase.callActivityOnStart(activity); + } + + public void callActivityOnRestart(Activity activity) { + this.mBase.callActivityOnRestart(activity); + } + + public void callActivityOnResume(Activity activity) { + this.mBase.callActivityOnResume(activity); + } + + public void callActivityOnStop(Activity activity) { + this.mBase.callActivityOnStop(activity); + } + + public void callActivityOnSaveInstanceState(Activity activity, Bundle bundle) { + this.mBase.callActivityOnSaveInstanceState(activity, bundle); + } + + public void callActivityOnPause(Activity activity) { + this.mBase.callActivityOnPause(activity); + } + + public void callActivityOnUserLeaving(Activity activity) { + this.mBase.callActivityOnUserLeaving(activity); + } + + public void startAllocCounting() { + this.mBase.startAllocCounting(); + } + + public void stopAllocCounting() { + this.mBase.stopAllocCounting(); + } + + public Bundle getAllocCounts() { + return this.mBase.getAllocCounts(); + } + + public Bundle getBinderCounts() { + return this.mBase.getBinderCounts(); + } +} diff --git a/bundle/src/ctrip/android/bundle/runtime/RuntimeArgs.java b/bundle/src/ctrip/android/bundle/runtime/RuntimeArgs.java new file mode 100644 index 0000000..e499c72 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/runtime/RuntimeArgs.java @@ -0,0 +1,14 @@ +package ctrip.android.bundle.runtime; + +import android.app.Application; +import android.content.res.Resources; + +/** + * Created by yb.wang on 15/1/4. + * 运行时重要的参数 + */ + +public class RuntimeArgs { + public static Application androidApplication; + public static Resources delegateResources; +} diff --git a/bundle/src/ctrip/android/bundle/util/APKUtil.java b/bundle/src/ctrip/android/bundle/util/APKUtil.java new file mode 100644 index 0000000..325a646 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/util/APKUtil.java @@ -0,0 +1,45 @@ +package ctrip.android.bundle.util; + + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Created by yb.wang on 14/12/31. + */ +public class APKUtil { + + + public static void copyInputStreamToFile(InputStream inputStream, File file) throws IOException { + FileChannel fileChannel = null; + FileOutputStream fileOutputStream = null; + try { + fileOutputStream = new FileOutputStream(file); + fileChannel = fileOutputStream.getChannel(); + byte[] buffer = new byte[1024]; + while (true) { + int read = inputStream.read(buffer); + if (read <= 0) { + break; + } + fileChannel.write(ByteBuffer.wrap(buffer, 0, read)); + } + + } catch (IOException ex) { + throw ex; + } finally { + if (inputStream != null) + inputStream.close(); + if (fileChannel != null) + fileChannel.close(); + if (fileOutputStream != null) + fileOutputStream.close(); + } + + } + +} diff --git a/bundle/src/ctrip/android/bundle/util/StringUtil.java b/bundle/src/ctrip/android/bundle/util/StringUtil.java new file mode 100644 index 0000000..b6b5076 --- /dev/null +++ b/bundle/src/ctrip/android/bundle/util/StringUtil.java @@ -0,0 +1,79 @@ +package ctrip.android.bundle.util; + +/** + * Created by yb.wang on 14/12/31. + */ +public class StringUtil { + private static final String EMPTY = ""; + + public static boolean isEmpty(String source) { + return source == null || source.length() == 0; + } + + + public static boolean equals(String str, String str2) { + return str == null ? false : str.equals(str2); + } + + public static String subStringBetween(String source, String start, String end) { + if (source == null || start == null || end == null) { + return null; + } + int indexOf = source.indexOf(start); + if (indexOf == -1) return null; + int indexOf2 = source.indexOf(end, start.length() + indexOf); + return indexOf2 != -1 ? source.substring(start.length() + indexOf, indexOf2) : null; + } + + public static String subStringAfter(String source, String prefix) { + if (isEmpty(source)) return source; + if (prefix == null) return EMPTY; + int indexOf = source.indexOf(prefix); + return indexOf != -1 ? source.substring(indexOf + prefix.length()) : EMPTY; + + } + + + public static boolean isBlank(String str) { + if (str != null) { + int length = str.length(); + if (length != 0) { + for (int i = 0; i < length; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return false; + } + } + return true; + } + } + return true; + } + + public static String join(Object[] objArr, String str) { + return objArr == null ? null : join(objArr, str, 0, objArr.length); + } + + public static String join(Object[] objArr, String str, int i, int i2) { + if (objArr == null) { + return null; + } + if (str == null) { + str = EMPTY; + } + int i3 = i2 - i; + if (i3 <= 0) { + return EMPTY; + } + StringBuilder stringBuilder = new StringBuilder(((objArr[i] == null ? 128 : objArr[i].toString().length()) + str.length()) * i3); + for (int i4 = i; i4 < i2; i4++) { + if (i4 > i) { + stringBuilder.append(str); + } + if (objArr[i4] != null) { + stringBuilder.append(objArr[i4]); + } + } + return stringBuilder.toString(); + } + +} diff --git a/demo.jks b/demo.jks new file mode 100644 index 0000000..03b824d Binary files /dev/null and b/demo.jks differ diff --git a/demo1/.gitignore b/demo1/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/demo1/.gitignore @@ -0,0 +1 @@ +/build diff --git a/demo1/AndroidManifest.xml b/demo1/AndroidManifest.xml new file mode 100644 index 0000000..a8666d0 --- /dev/null +++ b/demo1/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/demo1/assets/.gitignore b/demo1/assets/.gitignore new file mode 100644 index 0000000..eb03f4b --- /dev/null +++ b/demo1/assets/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore + diff --git a/demo1/build.gradle b/demo1/build.gradle new file mode 100644 index 0000000..c080501 --- /dev/null +++ b/demo1/build.gradle @@ -0,0 +1,26 @@ + +if(solidMode){ + project.ext { + packageName = 'ctrip.android.demo1' + apkName = packageName.replace('.', '_') + } + apply from: '../sub-project-build.gradle' +} +else { + + apply plugin: 'com.android.library' + apply from: '../global_config.gradle' + android { + + defaultConfig { + versionCode 1 + versionName "1.0" + } + + } + + dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + + } +} \ No newline at end of file diff --git a/demo1/proguard-rules.pro b/demo1/proguard-rules.pro new file mode 100644 index 0000000..f0baf96 --- /dev/null +++ b/demo1/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/yb.wang/Downloads/android_SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/demo1/res/drawable-hdpi/demo1.png b/demo1/res/drawable-hdpi/demo1.png new file mode 100644 index 0000000..05622b7 Binary files /dev/null and b/demo1/res/drawable-hdpi/demo1.png differ diff --git a/demo1/res/layout/demo1_activity_main.xml b/demo1/res/layout/demo1_activity_main.xml new file mode 100644 index 0000000..86c2d80 --- /dev/null +++ b/demo1/res/layout/demo1_activity_main.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/demo1/res/values-w820dp/dimens.xml b/demo1/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..8f310d9 --- /dev/null +++ b/demo1/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/demo1/res/values/dimens.xml b/demo1/res/values/dimens.xml new file mode 100644 index 0000000..412cc8c --- /dev/null +++ b/demo1/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/demo1/res/values/strings.xml b/demo1/res/values/strings.xml new file mode 100644 index 0000000..d2a2654 --- /dev/null +++ b/demo1/res/values/strings.xml @@ -0,0 +1,7 @@ + + demo1 + + Hello,I am Demo1! + Settings + This is the demo1 image resource: + diff --git a/demo1/src/ctrip/android/demo1/MainActivity.java b/demo1/src/ctrip/android/demo1/MainActivity.java new file mode 100644 index 0000000..56bdac4 --- /dev/null +++ b/demo1/src/ctrip/android/demo1/MainActivity.java @@ -0,0 +1,15 @@ +package ctrip.android.demo1; + +import android.app.Activity; +import android.os.Bundle; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.demo1_activity_main); + } + + +} diff --git a/demo2/.gitignore b/demo2/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/demo2/.gitignore @@ -0,0 +1 @@ +/build diff --git a/demo2/AndroidManifest.xml b/demo2/AndroidManifest.xml new file mode 100644 index 0000000..b3458bd --- /dev/null +++ b/demo2/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/demo2/assets/.gitignore b/demo2/assets/.gitignore new file mode 100644 index 0000000..eb03f4b --- /dev/null +++ b/demo2/assets/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore + diff --git a/demo2/build.gradle b/demo2/build.gradle new file mode 100644 index 0000000..9c0cf76 --- /dev/null +++ b/demo2/build.gradle @@ -0,0 +1,25 @@ +if(solidMode){ + project.ext { + packageName = 'ctrip.android.demo2' + apkName = packageName.replace('.', '_') + } + apply from: '../sub-project-build.gradle' +}else { + apply plugin: 'com.android.library' + apply from: '../global_config.gradle' + android { + compileSdkVersion 23 + buildToolsVersion "23.0.0" + + defaultConfig { + + versionCode 1 + versionName "1.0" + } + + } + dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + + } +} \ No newline at end of file diff --git a/demo2/proguard-rules.pro b/demo2/proguard-rules.pro new file mode 100644 index 0000000..f0baf96 --- /dev/null +++ b/demo2/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/yb.wang/Downloads/android_SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/demo2/res/drawable-hdpi/demo2.png b/demo2/res/drawable-hdpi/demo2.png new file mode 100644 index 0000000..2607ec5 Binary files /dev/null and b/demo2/res/drawable-hdpi/demo2.png differ diff --git a/demo2/res/layout/demo2_activity_main.xml b/demo2/res/layout/demo2_activity_main.xml new file mode 100644 index 0000000..21a3486 --- /dev/null +++ b/demo2/res/layout/demo2_activity_main.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/demo2/res/values-w820dp/dimens.xml b/demo2/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..7f8ebab --- /dev/null +++ b/demo2/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/demo2/res/values/dimens.xml b/demo2/res/values/dimens.xml new file mode 100644 index 0000000..c47d99a --- /dev/null +++ b/demo2/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/demo2/res/values/strings.xml b/demo2/res/values/strings.xml new file mode 100644 index 0000000..44dbe34 --- /dev/null +++ b/demo2/res/values/strings.xml @@ -0,0 +1,7 @@ + + demo2 + + Hello,I am Demo2! + Settings + This is the demo2 image resource: + diff --git a/demo2/src/ctrip/android/demo2/MainActivity.java b/demo2/src/ctrip/android/demo2/MainActivity.java new file mode 100644 index 0000000..82d1f1e --- /dev/null +++ b/demo2/src/ctrip/android/demo2/MainActivity.java @@ -0,0 +1,21 @@ +package ctrip.android.demo2; + +import android.app.Activity; +import android.os.Bundle; +import android.widget.ImageView; +import android.widget.TextView; + +public class MainActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.demo2_activity_main); + TextView textView=(TextView)findViewById(R.id.demo2_textView3); + textView.setText(R.string.sample_text); + ImageView imageView=(ImageView)findViewById(R.id.demo2_imageView2); + imageView.setImageResource(R.drawable.sample); + } + + +} diff --git a/global_config.gradle b/global_config.gradle new file mode 100644 index 0000000..6c5d416 --- /dev/null +++ b/global_config.gradle @@ -0,0 +1,68 @@ +//所有工程都要用的公共配置,由各个子模块直接apply from + + +android { + compileSdkVersion 23 + buildToolsVersion "21.1.2" + + useLibrary 'org.apache.http.legacy' + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 19 + } + + buildTypes { + debug { + debuggable true + minifyEnabled false + } + release { + minifyEnabled false + } + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + + packagingOptions{ + exclude 'META-INF/MANIFEST.MF' + exclude 'META-INF/NOTICE.TXT' + exclude 'META-INF/LICENSE.TXT' + exclude 'META-INF/LICENSE.TXT' + exclude 'META-INF/LICENSE.txt' + } + + dexOptions { + javaMaxHeapSize "4g" + preDexLibraries = false + } + + // lint所有选项请单独加入 + lintOptions { + checkReleaseBuilds true + abortOnError false + check 'NewApi' //新API + showAll true + textReport true + textOutput file("${ctripRoot}/build-outputs/lint/${project.name}_lint-report.txt") + xmlReport true + xmlOutput file("${ctripRoot}/build-outputs/lint/${project.name}_lint-report.xml") + htmlReport true + htmlOutput file("${ctripRoot}/build-outputs/lint/${project.name}_lint-report.html") + } +} + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1d3591c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8c0fb64 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..8888d2f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Sep 23 11:19:55 CST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/AndroidManifest.xml b/sample/AndroidManifest.xml new file mode 100644 index 0000000..105030c --- /dev/null +++ b/sample/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/assets/.gitignore b/sample/assets/.gitignore new file mode 100644 index 0000000..eb03f4b --- /dev/null +++ b/sample/assets/.gitignore @@ -0,0 +1,5 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore + diff --git a/sample/build.gradle b/sample/build.gradle new file mode 100644 index 0000000..4e3ce24 --- /dev/null +++ b/sample/build.gradle @@ -0,0 +1,321 @@ +apply plugin: 'com.android.application' + +apply from: '../global_config.gradle' + +android { + signingConfigs { + demo { + keyAlias 'demo' + keyPassword '123456' + storePassword '123456' + storeFile file('../demo.jks') + } + } + + defaultConfig { + applicationId "ctrip.android.sample" + versionCode 1 + versionName "1.0" + } + buildTypes { + debug { + signingConfig signingConfigs.demo + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.demo + } + } +} + +dependencies { + compile fileTree(include: ['*.jar'], dir: 'libs') + compile project(':bundle') + if (!solidMode) { + compile project(':demo1') + compile project(':demo2') + } +} + + + +//打包后产出物复制到build-outputs目录。apk、manifest、mapping +task copyReleaseOutputs(type:Copy){ + from ("$buildDir/outputs/apk/sample-release.apk") { + rename 'sample-release.apk', 'demo-base-release.apk' + } + from "$buildDir/intermediates/manifests/full/release/AndroidManifest.xml" + from ("$buildDir/outputs/mapping/release/mapping.txt") { + rename 'mapping.txt', 'demo-base-mapping.txt' + } + + into new File(rootDir, 'build-outputs') +} + +assembleRelease<<{ + copyReleaseOutputs.execute() +} + +clean { + delete buildDir + delete "${rootDir}/build-outputs/demo-base-release.apk" + delete "${rootDir}/build-outputs/AndroidManifest.xml" + delete "${rootDir}/build-outputs/demo-base-mapping.txt" + delete "${rootDir}/build-outputs/demo-mapping-final.txt" + delete "${rootDir}/build-outputs/demo-release-reloaded.apk" + delete "${rootDir}/build-outputs/demo-release-resigned.apk" + delete "${rootDir}/build-outputs/demo-release-repacked.apk" + delete "${rootDir}/build-outputs/demo-release-final.apk" +} + +import org.apache.tools.ant.taskdefs.condition.Os + +def getZipAlignPath(){ + def zipAlignPath = "${android.sdkDirectory}/build-tools/${android.buildToolsVersion}/zipalign" + if(Os.isFamily(Os.FAMILY_WINDOWS)){ + zipAlignPath += '.exe' + } + assert (new File(zipAlignPath)).exists() : '没有找到zipalign应用程序!' + + return zipAlignPath +} + +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream + +// 打包过程中很多手工zip过程: +// 1,为了压缩resources.arsc文件而对标准产出包重新压缩 +// 2,以及各子apk的纯手打apk包 +// 但对于音频等文件,压缩会导致资源加载报异常 +// 重新打包方法,使用STORED过滤掉不应该压缩的文件们 +// 后缀名列表来自于android源码 +def repackApk(originApk, targetApk){ + def noCompressExt = [".jpg", ".jpeg", ".png", ".gif", + ".wav", ".mp2", ".mp3", ".ogg", ".aac", + ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", + ".rtttl", ".imy", ".xmf", ".mp4", ".m4a", + ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2", + ".amr", ".awb", ".wma", ".wmv"] + + ZipFile zipFile = new ZipFile(originApk) + ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(targetApk))) + zipFile.entries().each{ entryIn -> + if(entryIn.directory){ + println "${entryIn.name} is a directory" + } + else{ + def entryOut = new ZipEntry(entryIn.name) + def dotPos = entryIn.name.lastIndexOf('.') + def ext = (dotPos >= 0) ? entryIn.name.substring(dotPos) : "" + def isRes = entryIn.name.startsWith('res/') + if(isRes && ext in noCompressExt){ + entryOut.method = ZipEntry.STORED + entryOut.size = entryIn.size + entryOut.compressedSize = entryIn.size + entryOut.crc = entryIn.crc + } + else{ + entryOut.method = ZipEntry.DEFLATED + } + zos.putNextEntry(entryOut) + zos << zipFile.getInputStream(entryIn) + zos.closeEntry() + } + } + zos.finish() + zos.close() + zipFile.close() +} + +// multidex默认会把manifest中注册的所有组件以及它们的直接引用类放在主dex里, +// 以保证至少在查找组件的时候涉及到的类加载正确。 +// 但第一级+第二级已经会导致主dex超标。 +// 所以在此hack修改CreateManifestKeepList类,让它不要顾忌activity、service、receiver +// 以保障主dex足够小不至于爆掉 +def patchKeepSpecs() { + def taskClass = "com.android.build.gradle.internal.tasks.multidex.CreateManifestKeepList"; + def clazz = this.class.classLoader.loadClass(taskClass) + def keepSpecsField = clazz.getDeclaredField("KEEP_SPECS") + keepSpecsField.setAccessible(true) + def keepSpecsMap = (Map) keepSpecsField.get(null) + if (keepSpecsMap.remove("activity") != null) { + // println "KEEP_SPECS patched: removed 'activity' root" + } else { + // println "Failed to patch KEEP_SPECS: no 'activity' root found" + } + if (keepSpecsMap.remove("service") != null) { + // println "KEEP_SPECS patched: removed 'service' root" + } else { + // println "Failed to patch KEEP_SPECS: no 'service' root found" + } + if (keepSpecsMap.remove("receiver") != null) { + // println "KEEP_SPECS patched: removed 'receiver' root" + } else { + // println "Failed to patch KEEP_SPECS: no 'receiver' root found" + } +} +patchKeepSpecs() + + +// dex命令默认保障方法数索引不超过65535, +// 但在编译期pass掉第一关的dex,有可能在运行期卡在dexopt上, +// 所以指定最大index数50000,远小于65535,安全第一。 +afterEvaluate { + // println tasks.withType(com.android.build.gradle.tasks.Dex) + + tasks.matching { + it.name.startsWith('dex') + }.each { dx -> + // println "found dex task $dx.name, add parameters" + if (dx.additionalParameters == null) { + dx.additionalParameters = [] + } +// dx.additionalParameters += '--minimal-main-dex' + dx.additionalParameters += '--set-max-idx-number=50000' + } +} + + +//base apk的assets中填充各子apk +//输入:Ctrip-base-release.apk +//输出:Ctrip-release-reloaded.apk +task reload(type:Zip){ + inputs.file "$rootDir/build-outputs/demo-base-release.apk" + inputs.files fileTree(new File(rootDir,'build-outputs')).include('*.so') + outputs.file "$rootDir/build-outputs/demo-release-reloaded.apk" + + into 'assets/baseres/',{ + from fileTree(new File(rootDir,'build-outputs')).include('*.so') + } + + from zipTree("$rootDir/build-outputs/demo-base-release.apk"), { + exclude('**/META-INF/*.SF') + exclude('**/META-INF/*.RSA') + } + + destinationDir file("$rootDir/build-outputs/") + + archiveName 'demo-release-reloaded.apk' +} + +//对apk重新压缩,调整各文件压缩比到正确 +//输入:Ctrip-release-reloaded.apk +//输出:Ctrip-release-repacked.apk +task repack (dependsOn: 'reload') { + inputs.file "$rootDir/build-outputs/demo-release-reloaded.apk" + outputs.file "$rootDir/build-outputs/demo-release-repacked.apk" + + doLast{ + println "release打包之后,重新压缩一遍,以压缩resources.arsc" + + def oldApkFile = file("$rootDir/build-outputs/demo-release-reloaded.apk") + + assert oldApkFile != null : "没有找到release包!" + + def newApkFile = new File(oldApkFile.parentFile, 'demo-release-repacked.apk') + + //重新打包 + repackApk(oldApkFile.absolutePath, newApkFile.absolutePath) + + assert newApkFile.exists() : "没有找到重新压缩的release包!" + } +} + +//对apk重签名 +//输入:Ctrip-release-repacked.apk +//输出:Ctrip-release-resigned.apk +task resign(type:Exec,dependsOn: 'repack'){ + inputs.file "$rootDir/build-outputs/demo-release-repacked.apk" + outputs.file "$rootDir/build-outputs/demo-release-resigned.apk" + + workingDir "$rootDir/build-outputs" + executable "${System.env.'JAVA_HOME'}/bin/jarsigner" + + def argv = [] + argv << '-verbose' + argv << '-sigalg' + argv << 'SHA1withRSA' + argv << '-digestalg' + argv << 'SHA1' + argv << '-keystore' + argv << "$rootDir/demo.jks" + argv << '-storepass' + argv << '123456' + argv << '-keypass' + argv << '123456' + argv << '-signedjar' + argv << 'demo-release-resigned.apk' + argv << 'demo-release-repacked.apk' + argv << 'demo' + + args = argv +} + + +//重新对jar包做对齐操作 +//输入:Ctrip-release-resigned.apk +//输出:Ctrip-release-final.apk +task realign (dependsOn: 'resign') { + inputs.file "$rootDir/build-outputs/demo-release-resigned.apk" + outputs.file "$rootDir/build-outputs/demo-release-final.apk" + + doLast{ + println '重新zipalign,还可以加大压缩率!' + + def oldApkFile = file("$rootDir/build-outputs/demo-release-resigned.apk") + assert oldApkFile != null : "没有找到release包!" + + def newApkFile = new File(oldApkFile.parentFile,'demo-release-final.apk') + + def cmdZipAlign = getZipAlignPath() + def argv = [] + argv << '-f' //overwrite existing outfile.zip + // argv << '-z' //recompress using Zopfli + argv << '-v' //verbose output + argv << '4' //alignment in bytes, e.g. '4' provides 32-bit alignment + argv << oldApkFile.absolutePath + argv << newApkFile.absolutePath + + project.exec { + commandLine cmdZipAlign + args argv + } + + assert newApkFile.exists() : "没有找到重新zipalign的release包!" + } +} + +/** + * 用来连接文件的task + */ +class ConcatFiles extends DefaultTask { + @InputFiles + FileCollection sources + + @OutputFile + File target + + @TaskAction + void concat() { + File tmp = File.createTempFile('concat', null, target.getParentFile()) + tmp.withWriter { writer -> + sources.each { file -> + file.withReader { reader -> + writer << reader + } + } + } + target.delete() + tmp.renameTo(target) + } +} + +//合并base和所有模块的mapping文件 +task concatMappings(type: ConcatFiles){ + sources = fileTree(new File(rootDir,'build-outputs')).include('*mapping.txt') + target = new File(rootDir,'build-outputs/demo-mapping-final.txt') +} + +task repackAll(dependsOn: ['reload','resign','repack','realign','concatMappings']) diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro new file mode 100644 index 0000000..f0baf96 --- /dev/null +++ b/sample/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/yb.wang/Downloads/android_SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/sample/res/drawable-hdpi/sample.png b/sample/res/drawable-hdpi/sample.png new file mode 100644 index 0000000..bd2722b Binary files /dev/null and b/sample/res/drawable-hdpi/sample.png differ diff --git a/sample/res/layout/activity_main.xml b/sample/res/layout/activity_main.xml new file mode 100644 index 0000000..08370a3 --- /dev/null +++ b/sample/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + +