package com.oasisfeng.google.maps.rectify;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.Formatter;
import java.util.LinkedHashMap;
import java.util.Set;
import android.os.Debug;
import android.util.Log;
import android.util.Pair;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.XposedHelpers.ClassNotFoundError;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
/** @author Oasis */
public class GoogleMapsXposed implements IXposedHookLoadPackage {
private static final boolean DEBUG = false;
@Override public void handleLoadPackage(final LoadPackageParam loadpkg) throws Throwable {
final Class<?> LatLng;
Constructor<?> LatLng_ctor;
final long start = DEBUG ? Debug.threadCpuTimeNanos() : 0;
try {
LatLng = Class.forName("com.google.android.gms.maps.model.LatLng", false, loadpkg.classLoader);
// public LatLng(double latitude, double longitude)
LatLng_ctor = LatLng.getConstructor(double.class, double.class);
} catch (final Throwable e) {
if (DEBUG) {
final long end = Debug.threadCpuTimeNanos();
Log.d(TAG, "Load time for skipped process: " + (end - start) + "ns");
}
return;
}
Log.i(TAG, "Patching Google Maps SDK v2 in " + loadpkg.packageName);
XposedBridge.hookMethod(LatLng_ctor, new XC_MethodHook() {
@Override protected void beforeHookedMethod(final MethodHookParam param) throws Throwable {
final String skip_reason = mSkip.get();
if (skip_reason != null) {
Log.d(TAG, "Skip rectifying for " + skip_reason);
return;
}
final Double latitude = (Double) param.args[0];
final Double longitude = (Double) param.args[1];
if (mRecent.contains(new Pair<>(latitude, longitude).hashCode())) return; // Duplicate
final Pair<Double, Double> trans = Transform.transform(latitude, longitude);
param.args[0] = trans.first;
param.args[1] = trans.second;
mRecent.add(trans.hashCode());
if (DEBUG) {
final Formatter formatter = new Formatter();
Log.d(TAG, formatter.format("%.4f,%.4f => %.4f,%.4f", latitude, longitude, param.args[0], param.args[1]).toString()/*, new Throwable()*/);
formatter.close();
}
}
});
// Skip duplicate rectify in following methods:
// LatLngBounds$Builder.build()
// LatLngBounds.including(LatLng)
// LatLngBounds.getCenter()
try {
XposedHelpers.findAndHookMethod("com.google.android.gms.maps.model.LatLngBounds$Builder", loadpkg.classLoader, "build", new XC_MethodHook() {
@Override protected void beforeHookedMethod(final MethodHookParam param) throws Throwable {
mSkip.set("LatLngBounds.Builder.build()");
}
@Override protected void afterHookedMethod(final MethodHookParam param) throws Throwable {
mSkip.set(null);
}
});
} catch (ClassNotFoundError | NoSuchMethodError e) {}
try {
XposedHelpers.findAndHookMethod("com.google.android.gms.maps.model.LatLngBounds", loadpkg.classLoader, "including", LatLng, new XC_MethodHook() {
@Override protected void beforeHookedMethod(final MethodHookParam param) throws Throwable {
mSkip.set("LatLngBounds.including()");
}
@Override protected void afterHookedMethod(final MethodHookParam param) throws Throwable {
mSkip.set(null);
}
});
} catch (ClassNotFoundError | NoSuchMethodError e) {}
try {
XposedHelpers.findAndHookMethod("com.google.android.gms.maps.model.LatLngBounds", loadpkg.classLoader, "getCenter", new XC_MethodHook() {
@Override protected void beforeHookedMethod(final MethodHookParam param) throws Throwable {
mSkip.set("LatLngBounds.getCenter()");
}
@Override protected void afterHookedMethod(final MethodHookParam param) throws Throwable {
mSkip.set(null);
}
});
} catch (ClassNotFoundError | NoSuchMethodError e) {}
}
private final Set<Integer> mRecent = Collections.synchronizedSet(Collections.newSetFromMap(new Cache<Integer, Boolean>()));
static ThreadLocal<String> mSkip = new ThreadLocal<String>();
static final String TAG = "GoogleMapsXposed";
}
class Cache<K, V> extends LinkedHashMap<K, V> {
private static final int KMaxEntries = 128;
@Override protected boolean removeEldestEntry(final java.util.Map.Entry<K, V> eldest) {
return (size() > KMaxEntries);
}
private static final long serialVersionUID = 1L;
}