APP privacy policy transformation

Posted by burgessm on Mon, 03 Jan 2022 15:27:44 +0100

preface

Recently, due to the tightening of policies, APP is now required to obtain private information with the consent of users. However, a lot of private information is obtained by a third-party SDK. The SDK initialization is generally in the application. Due to the large number of maintenance items, it is likely to cause potential problems if they are changed rashly. So I want to study a low invasive scheme. Complete the privacy transformation without affecting the original APP process.

programme

Several schemes have been studied. Briefly

Option 1

By setting an entry for the APP, set the enable of the activity of the original entry to false. Let the client enter the privacy confirmation interface first
. Confirm the completion, and then use the code to set the enable of this activity to false. Set the original entry to true.
The required technology comes from this article
(Technology) Android modify Desktop Icon

effect

This scheme can basically meet the requirements. But there are two problems.

  1. Setting activity to false will crash the application. The scheme of using aliases mentioned in the previous article is also not good.
  2. After modifying the activity, the activity declared in the manifest file cannot be found when Android Studio starts.

Option 2

Direct the creation process of Hook Activity. If the user does not pass the protocol, the activity will be turned into our query interface.
reference:
Several postures of Android Hook Activity

It should be noted that we only need the mminstrumentation of Hook ActivityThread. The method that needs a hook is the newActivity method.

public class ApplicationInstrumentation extends Instrumentation {

    private static final String TAG = "ApplicationInstrumentation";

    // The original object in the ActivityThread is saved
    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);
    }


}

use

Finally, scheme 2 is used. Management is realized through a CheckApp class.
It's easy to use. Inherit your Application class from CheckApp and put the initialization of sdk into the initSDK method
To avoid errors, I have set onCreate to final in CheckApp

public class MyApp extends CheckApp {
  

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

    private void initUtils() {
    }
}

In the manifest file, you only need to register. You need to let the user confirm the activity of the privacy agreement.

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

If you want to judge the user agreement after each application upgrade, you only need to override this method in CheckApp. (this function is enabled by default)

/**
     * Is each version checked for user privacy
     * @return
     */
    protected boolean checkForEachVersion() {
        return true;
    }

Determine whether the user agrees to use this method

CheckApp.getApp().isUserAgree();

The user agrees to the callback in the future. The second false indicates that it will not automatically jump to the intercepted Activity

    /**
             * The second false indicates that it does not automatically jump to the intercepted Activity
             * CheckApp The class name of the intercepted Activity is recorded.
             */
            CheckApp.getApp().agree(this,false,getIntent().getExtras());

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

    // The original object in the ActivityThread is saved
    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:Check whether the user gives permission to the application
 */
public abstract class CheckApp extends MultiDexApplication {

    /**
     * Does the user agree 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;

    }

    /**
     * Is each version checked for user privacy
     * @return
     */
    protected boolean checkForEachVersion() {
        return true;
    }

    private static boolean initSDK = false;//Has the SDK 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);
            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();

        //Initialize SDK s that have nothing to do with privacy
        if (userAgree && !initSDK) {
            initSDK = true;
            initSDK();
        }

    }


    public static CheckApp getApp() {
        return app;
    }


    /**
     * Initialize SDK s that have nothing to do with user privacy
     * If it cannot be distinguished, it is recommended to use only one method, initSDK
     */
    protected void initSafeSDK() {

    }


    /**
     * Judge whether the user agrees
     *
     * @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();
        }

        //Start the real startup page
        if (!gotoFirstActivity) {
            //It is already the same interface and does not need to be opened automatically
            return;
        }
        try {
            Intent intent = new Intent(activity, Class.forName(realFirstActivityName));
            if (extras != null) {
                intent.putExtras(extras);//Perhaps it is to tune app from the web page, when extras contains parameters that open specific news. It needs to be passed to the real startup page
            }
            activity.startActivity(intent);
            activity.finish();//Close current page
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }


    /**
     * Subclass rewriting is used to initialize SDK and other related work
     */
    abstract protected void initSDK();

    /**
     * Judge whether it is in the main process. Pushservers in some SDK s may run in other processes.
     * It will also cause the Application to be initialized twice, which is only required in the main process.
     * * @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: ");
        // Get the current ActivityThread object first
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        //currentActivityThread is a static function, so you can invoke directly without taking instance parameters
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);

        // Get the original Mi instrumentation field
        Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
        mInstrumentationField.setAccessible(true);
        Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
        // Create proxy object
        Instrumentation evilInstrumentation = new ApplicationInstrumentation(mInstrumentation);
        // steal the beams and pillars and replace them with rotten timbers
        mInstrumentationField.set(currentActivityThread, evilInstrumentation);
    }


}

Topics: Android