/*
* Original work Copyright (c) 2015, Alibaba Mobile Infrastructure (Android) Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.taobao.patch;
import android.content.Context;
import java.io.File;
import java.util.Enumeration;
import java.util.HashMap;
import dalvik.system.DexClassLoader;
import dalvik.system.DexFile;
/**
*
* <p>PatchMain is used to load a patch apk file.</p>
*
* <p>Note:The patch apk must not include the classes which are already in loader apk, so the patch.jar and dexposed.jar
* should be included in hotpatchpatch project, but not output to patch apk.</p>
*
* <p>Before using this, one necessary condition, the libdexposed.so, libdexposed2.3.so and dexposedbridge.jar must
* be included in loader project.</p>
*
* <p>The patch apk file should just normal apk which include some patch classes to hook
* methods you want to modify. These patch classes will be invoked auto if load(...) of this called.</p>
*
* <p>About patch class, which must implement IPatch interface and override handlePatch.</p>
*
* The sample:
* <pre class="prettyprint">
* public class HotPatchTest implements IPatch {
* //@Override
* public void handlePatch(final PatchParam arg0) throws Throwable {
* }
* }
* </pre>
*
* <p>The PatchParam includes context and an object HashMap which include objects may be used by patch class.
* And that object HashMap is equal the arg one sent in load() method. </p>
*
* <p>How to implement handlePatch method?</p>
*
* <p>First, it needs to find class which will be patched.
* PatchParams' member context has the getClassLoader to load class that you want to patch.</p>
*
* <p>Then, the XposedBridge.findAndHookMethod should be used here. The first argument is the patching class,
* and the second is the patching method name, and the following arguments are the patching method' arguments type class.
* The last argument is the instance of XC_MethodHook or XC_MethodReplacement, the following are description for these two.</p>
*
* <p>The XC_MethodHook has two methods beforeHookedMethod and afterHookedMethod, and these easy to understand that they are called
* before/after the invocation of the hooked method. </p>
*
* <p>The XC_MethodReplacement has method replaceHookedMethod to replace the whole original method.</p>
*
* <P>The MethodHookParam is the only argument used in above three methods, which include some useful contents.
* MethodHookParam.thisObject is the instance of this class.
* In MethodHookParam.args, it offers all argument values of this method.
* And MethodHookParam.setResult can modify the result of the method call. In a "before-method-call"
* hook, prevents the call to the original method. But it still need to "return" from the hook handler if required.</p>
*
* <P>Finally, this load() method will return the PatchResult which show the result or error detail. </p>
*
* @version 1.0
*/
public class PatchMain {
private static final ReadWriteSet<PatchCallback> loadedPatchCallbacks = new ReadWriteSet<PatchCallback>();
/**
* Load a runnable patch apk.
*
* @param context the application or activity context.
* @param apkPath the path of patch apk file.
* @param contentMap the object maps that will be used by patch classes.
* @return PatchResult include if success or error detail.
*/
public static PatchResult load(Context context, String apkPath, HashMap<String, Object> contentMap) {
if (!new File(apkPath).exists()) {
return new PatchResult(false, PatchResult.FILE_NOT_FOUND, "FILE not found on " + apkPath);
}
PatchResult result = loadAllCallbacks(context, apkPath,context.getClassLoader());
if (!result.isSuccess()) {
return result;
}
if (loadedPatchCallbacks.getSize() == 0) {
return new PatchResult(false, PatchResult.NO_PATCH_CLASS_HANDLE, "No patch class to be handle");
}
PatchParam lpparam = new PatchParam(loadedPatchCallbacks);
lpparam.context = context;
lpparam.contentMap = contentMap;
return PatchCallback.callAll(lpparam);
}
private static PatchResult loadAllCallbacks(Context context, String apkPath, ClassLoader cl) {
try {
// String dexPath = new File(context.getFilesDir(), apkPath.).getAbsolutePath();
File dexoptFile = new File(apkPath + "odex");
if (dexoptFile.exists()) {
dexoptFile.delete();
}
ClassLoader mcl = null;
try {
mcl = new DexClassLoader (apkPath,context.getFilesDir().getAbsolutePath(),null, cl);
}catch(Throwable e){
return new PatchResult(false, PatchResult.FOUND_PATCH_CLASS_EXCEPTION, "Find patch class exception ", e);
}
DexFile dexFile = DexFile.loadDex(apkPath, context.getFilesDir().getAbsolutePath() + File.separator + "patch.odex", 0);
Enumeration<String> entrys = dexFile.entries();
// clean old callback
synchronized (loadedPatchCallbacks) {
loadedPatchCallbacks.clear();
}
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
Class<?> entryClass = null;
try {
entryClass = mcl.loadClass(entry);
} catch (ClassNotFoundException e) {
e.printStackTrace();
break;
}
if (isImplementInterface(entryClass, IPatch.class)) {
Object moduleInstance = entryClass.newInstance();
hookLoadPatch(new PatchCallback((IPatch) moduleInstance));
}
}
} catch (Exception e) {
return new PatchResult(false, PatchResult.FOUND_PATCH_CLASS_EXCEPTION, "Find patch class exception ", e);
}
return new PatchResult(true, PatchResult.NO_ERROR, "");
}
private static boolean isImplementInterface(Class<?> entry, Class<?> interClass) {
Class<?>[] interfaces = entry.getInterfaces();
if (interfaces == null) {
return false;
}
for (int i = 0; i < interfaces.length; i++) {
if (interfaces[i].equals(interClass)) {
return true;
}
}
return false;
}
/**
* Get notified when a patch is loaded. This is especially useful to hook some patch-specific methods.
*/
private static void hookLoadPatch(PatchCallback callback) {
synchronized (loadedPatchCallbacks) {
loadedPatchCallbacks.add(callback);
}
}
}