通过拦截 Activity的创建 实现APP的隐私政策改造
序言
最近因为政策收紧,现在要求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";
// ActivityThread中原始的对象, 保存起来
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中的这个方法。(默认开启该功能)
/**
* 是否每个版本都检查是否拥有用户隐私权限
* @return
*/
protected boolean checkForEachVersion() {
return true;
}
判断用户是否同意用这个方法
CheckApp.getApp().isUserAgree();
用户同意以后的回调,第二个false表示不自动跳转到被拦截的Activity
/**
* 第二个false表示不自动跳转到被拦截的Activity
* CheckApp 记录了被拦截的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;
/**
* Created by zhuguohui
* Date: 2021/7/30
* Time: 13:46
* Desc:
*/
public class ApplicationInstrumentation extends Instrumentation {
private static final String TAG = "ApplicationInstrumentation";
// ActivityThread中原始的对象, 保存起来
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;
/**
* Created by zhuguohui
* Date: 2021/7/30
* Time: 10:01
* Desc:检查用户是否给与权限的application
*/
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) {
//只有在用户不同意的情况下才hook ,避免性能损失
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;
}
/**
* 是否每个版本都检查是否拥有用户隐私权限
* @return
*/
protected boolean checkForEachVersion() {
return true;
}
private static boolean initSDK = false;//是否已经初始化了SDK
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();
//初始化那些和隐私无关的SDK
if (userAgree && !initSDK) {
initSDK = true;
initSDK();
}
}
public static CheckApp getApp() {
return app;
}
/**
* 初始化那些和用户隐私无关的SDK
* 如果无法区分,建议只使用initSDK一个方法
*/
protected void initSafeSDK() {
}
/**
* 判断用户是否同意
*
* @return
*/
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);//也许是从网页中调起app,这时候extras中含有打开特定新闻的参数。需要传递给真正的启动页
}
activity.startActivity(intent);
activity.finish();//关闭当前页面
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 子类重写用于初始化SDK等相关工作
*/
abstract protected void initSDK();
/**
* 判断是否在主进程中,一些SDK中的PushServer可能运行在其他进程中。
* 也就会造成Application初始化两次,而只有在主进程中才需要初始化。
* * @return
*/
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;
/**
* Created by zhuguohui
* Date: 2021/7/30
* Time: 13:20
* Desc:
*/
public class HookUtil {
public static void attachContext() throws Exception {
Log.i("zzz", "attachContext: ");
// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
//currentActivityThread是一个static函数所以可以直接invoke,不需要带实例参数
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
// 创建代理对象
Instrumentation evilInstrumentation = new ApplicationInstrumentation(mInstrumentation);
// 偷梁换柱
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
}
}
作者:solo_99
链接:https://juejin.cn/post/6990643611130363917
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。