序言
最近因为政策收紧,现在要求APP必须在用户同意的情况下才能获取隐私信息。但是很多隐私信息的获取是第三方SDK获取的。而SDK的初始化一般都在application中。由于维护的项目多,如果贸然改动很有可能造成潜在的问题。所以想研究一个低侵入性的方案。在不影响原有APP流程的基础上完成隐私改造。
方案
研究了几个方案,简单的说一下
方案1
通过给APP在设置一个入口,将原有入口的activity的enable设置为false。让客户端先进入到隐私确认界面 。确认完成,再用代码使这个activity的enable设置为false。将原来的入口设置为true。 需要的技术来自这篇文章 (技术)Android修改桌面图标
效果
这种方案基本能满足要求。但是存在两个问题。
- 将activity设置为false的时候会让应用崩溃。上一篇文章提到使用别名的方案也不行。
- 修改了activity以后,Android Studio启动的时候无法找到在清单文件中声明的activity。
方案2
直接Hook Activity的创建过程,如果用户没有通过协议,就将activity 变为我们的询问界面。 参考文献: Android Hook Activity 的几种姿势 Android应用进程的创建 — Activity的启动流程
需要注意的是,我们只需要Hook ActivityThread 的mInstrumentation 即可。需要hook的方法是newActivity方法。
public class ApplicationInstrumentation extends Instrumentation {
private static final String TAG = "ApplicationInstrumentation";
Instrumentation mBase;
public ApplicationInstrumentation(Instrumentation base) {
mBase = base;
}
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
className = CheckApp.getApp().getActivityName(className);
return mBase.newActivity(cl, className, intent);
}
}
使用
最终使用了方案2。通过一个CheckApp类来实现管理。 使用很简单,将你的Application类继承自CheckApp 将sdk的初始化放置到 initSDK方法中 为了避免出错,在CheckApp中我已经将onCreate设置为final了
public class MyApp extends CheckApp {
public DatabaseHelper dbHelper;
protected void initSDK() {
RxJava1Util.setErrorNotImplementedHandler();
mInstance = this;
initUtils();
}
private void initUtils() {
}
}
在清单文件中只需要注册你需要让用户确认隐私协议的activity。
<application>
...
<meta-data
android:name="com.trs.library.check.activity"
android:value=".activity.splash.GuideActivity" />
</application>
如果要在应用每次升级以后都判断用户协议,只需要覆盖CheckApp中的这个方法。(默认开启该功能)
protected boolean checkForEachVersion() {
return true;
}
判断用户是否同意用这个方法
CheckApp.getApp().isUserAgree();
用户同意以后的回调,第二个false表示不自动跳转到被拦截的Activity
CheckApp.getApp().agree(this,false,getIntent().getExtras());
源码
一共只有3个类 
ApplicationInstrumentation
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import java.lang.reflect.Method;
public class ApplicationInstrumentation extends Instrumentation {
private static final String TAG = "ApplicationInstrumentation";
Instrumentation mBase;
public ApplicationInstrumentation(Instrumentation base) {
mBase = base;
}
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
className = CheckApp.getApp().getActivityName(className);
return mBase.newActivity(cl, className, intent);
}
}
CheckApp
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.multidex.MultiDexApplication;
import com.trs.library.util.SpUtil;
import java.util.List;
public abstract class CheckApp extends MultiDexApplication {
private static final String KEY_USER_AGREE = CheckApp.class.getName() + "_key_user_agree";
private static final String KEY_CHECK_ACTIVITY = "com.trs.library.check.activity";
private boolean userAgree;
private static CheckApp app;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
userAgree = SpUtil.getBoolean(this, getUserAgreeKey(base), false);
getCheckActivityName(base);
if (!userAgree) {
try {
HookUtil.attachContext();
} catch (Exception e) {
e.printStackTrace();
}
}
}
protected String getUserAgreeKey(Context base) {
if (checkForEachVersion()) {
try {
long longVersionCode = base.getPackageManager().getPackageInfo(base.getPackageName(), 0).versionCode;
return KEY_USER_AGREE + "_version_" + longVersionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
return KEY_USER_AGREE;
}
protected boolean checkForEachVersion() {
return true;
}
private static boolean initSDK = false;
String checkActivityName = null;
private void getCheckActivityName(Context base) {
mPackageManager = base.getPackageManager();
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
checkActivityName = appInfo.metaData.getString(KEY_CHECK_ACTIVITY);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
checkActivityName = checkName(checkActivityName);
}
public String getActivityName(String name) {
if (isUserAgree()) {
return name;
} else {
setRealFirstActivityName(name);
return checkActivityName;
}
}
private String checkName(String name) {
String newName = name;
if (!newName.startsWith(".")) {
newName = "." + newName;
}
if (!name.startsWith(getPackageName())) {
newName = getPackageName() + newName;
}
return newName;
}
@Override
public final void onCreate() {
super.onCreate();
if (!isRunOnMainProcess()) {
return;
}
app = this;
initSafeSDK();
if (userAgree && !initSDK) {
initSDK = true;
initSDK();
}
}
public static CheckApp getApp() {
return app;
}
protected void initSafeSDK() {
}
public boolean isUserAgree() {
return userAgree;
}
static PackageManager mPackageManager;
private static String realFirstActivityName = null;
public static void setRealFirstActivityName(String realFirstActivityName) {
CheckApp.realFirstActivityName = realFirstActivityName;
}
public void agree(Activity activity, boolean gotoFirstActivity, Bundle extras) {
SpUtil.putBoolean(this, getUserAgreeKey(this), true);
userAgree = true;
if (!initSDK) {
initSDK = true;
initSDK();
}
if (!gotoFirstActivity) {
return;
}
try {
Intent intent = new Intent(activity, Class.forName(realFirstActivityName));
if (extras != null) {
intent.putExtras(extras);
}
activity.startActivity(intent);
activity.finish();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
abstract protected void initSDK();
public boolean isRunOnMainProcess() {
ActivityManager am = ((ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE));
List<ActivityManager.RunningAppProcessInfo> processInfos = am.getRunningAppProcesses();
String mainProcessName = this.getPackageName();
int myPid = android.os.Process.myPid();
for (ActivityManager.RunningAppProcessInfo info : processInfos) {
if (info.pid == myPid && mainProcessName.equals(info.processName)) {
return true;
}
}
return false;
}
}
HookUtil
import android.app.Instrumentation;
import android.util.Log;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class HookUtil {
public static void attachContext() throws Exception {
Log.i("zzz", "attachContext: ");
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
Instrumentation evilInstrumentation = new ApplicationInstrumentation(mInstrumentation);
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
}
|