The preface

Recently tightened rules now require apps to obtain users’ consent before accessing private information. However, a lot of private information is obtained by third-party SDKS. The SDK initialization is usually done in the application. Due to the large number of maintenance items, any hasty changes may cause potential problems. So I wanted to develop a less invasive solution. Privacy transformation is completed without affecting the original APP process.

plan

We looked at a couple of options, just to be brief

Plan 1

Set the enable of the activity of the original entry to false by setting an entry for the APP. Let the client enter the privacy confirmation screen first. Verify that it is complete, then use the code to make the activity enable set to false. Set the original entry to true. The technique required from this article (technique)Android modifiers desktop ICONS

The effect

This scheme basically meets the requirements. But there are two problems.

  1. Setting the activity to false crashes the application. The aliases mentioned in the previous article don’t work either.
  2. After modifying the activity, Android Studio startup cannot find the activity declared in the manifest file.

Scheme 2

Hook the Activity creation process directly, and turn the Activity into our query interface if the user does not pass the protocol. References: Several postures for Android Hook Activity

Create an Android application process – start the Activity process

Note that we only need the mInstrumentation of the Hook ActivityThread. The method that requires a hook is the newActivity method.

public class ApplicationInstrumentation extends Instrumentation {

    private static final String TAG = "ApplicationInstrumentation";

    // Save the original object in 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);
        returnmBase.newActivity(cl, className, intent); }}Copy the code

use

Scheme 2 was used. Management is implemented through a CheckApp class. It’s very simple to use, inheriting your Application class from CheckApp and putting the INITIALIZATION of the SDK into the initSDK method to avoid errors, I’ve set onCreate to final in CheckApp

public class MyApp extends CheckApp {
  

    public DatabaseHelper dbHelper;
   protected void initSDK(a) {
        RxJava1Util.setErrorNotImplementedHandler();
        mInstance = this;
        initUtils();
    }

    private void initUtils(a) {}}Copy the code

Only register activities in the manifest file where you want users to confirm privacy agreements.

<application>.<meta-data
            android:name="com.trs.library.check.activity"
            android:value=".activity.splash.GuideActivity" />
           
</application>
           
Copy the code

If you want to determine the user protocol after each application upgrade, you just override this method in CheckApp. (This function is enabled by default.)

/** * Whether each version checks whether it has user privacy rights *@return* /
    protected boolean checkForEachVersion(a) {
        return true;
    }

Copy the code

Determine if the user agrees to use this method

CheckApp.getApp().isUserAgree();
Copy the code

The user agrees to future callbacks, with the second false indicating that the intercepted Activity is not automatically jumped

    /** * The second false does not automatically jump to the intercepted Activity * CheckApp records the intercepted Activity's class name. * /
            CheckApp.getApp().agree(this.false,getIntent().getExtras());
Copy the code

The source code

There are only three classes

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";

    // Save the original object in 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);
        returnmBase.newActivity(cl, className, intent); }}Copy the code

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: Check whether the user has granted permission to application */
public abstract class CheckApp extends MultiDexApplication {

    /** * Whether the user agrees to the privacy agreement */
    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 only when the user disagrees to avoid performance loss
            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;

    }

    /** * Whether each version checks whether it has user privacy rights *@return* /
    protected boolean checkForEachVersion(a) {
        return true;
    }

    private static boolean initSDK = false;// Whether the SDK has been initialized

    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);
            returncheckActivityName; }}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(a) {
        super.onCreate();
        if(! isRunOnMainProcess()) {return;
        }
        app = this;
        initSafeSDK();

        // Initialize the SDKS that have nothing to do with privacy
        if(userAgree && ! initSDK) { initSDK =true; initSDK(); }}public static CheckApp getApp(a) {
        return app;
    }


    /** * Initializes SDKS that have nothing to do with user privacy * if you cannot distinguish between them, it is recommended to use only the initSDK method */
    protected void initSafeSDK(a) {}/** * Determine whether the user agrees to **@return* /
    public boolean isUserAgree(a) {
        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();
        }

        // Start the real startup page
        if(! gotoFirstActivity) {// The interface is already the same, there is no need to open automatically
            return;
        }
        try {
            Intent intent = new Intent(activity, Class.forName(realFirstActivityName));
            if(extras ! =null) {
                intent.putExtras(extras);// Maybe the app is pulled up from a web page and extras contains parameters to open a particular news item. Need to be passed to the actual launch page
            }
            activity.startActivity(intent);
            activity.finish();// Close the current page
        } catch(ClassNotFoundException e) { e.printStackTrace(); }}/** * subclass override is used to initialize the SDK and other related work */
    abstract protected void initSDK(a);

    /** * Determine if PushServer is in the main process. Some SDKS may be running in other processes. * causes the Application to be initialized twice, which is required only in the main process. * *@return* /
    public boolean isRunOnMainProcess(a) {
        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; }}Copy the code

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(a) throws Exception {
        Log.i("zzz"."attachContext: ");
        Get the current ActivityThread objectClass<? > activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        //currentActivityThread is a static function so it can be invoked without instance arguments
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);

        // Get the original mInstrumentation field
        Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
        mInstrumentationField.setAccessible(true);
        Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
        // Create a proxy object
        Instrumentation evilInstrumentation = new ApplicationInstrumentation(mInstrumentation);
        // Change the pillarmInstrumentationField.set(currentActivityThread, evilInstrumentation); }}Copy the code