序言
最近因為政策收緊,現在要求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);
}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/291344.html
標籤:其他
