summaryrefslogtreecommitdiffstats
path: root/chromium/build/android/bytecode/java/org/chromium/bytecode/SplitCompatClassAdapter.java
blob: 17c271642c0a153c87bb704fd06d1aa00f442dc0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.bytecode;

import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.RETURN;

import static org.chromium.bytecode.TypeUtils.CONTEXT;
import static org.chromium.bytecode.TypeUtils.VOID;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.util.Set;

/**
 * A ClassVisitor for injecting ModuleInstaller.initActivity(activity) method call
 * into Activity's attachBaseContext() method. The goal is to eventually invoke
 * SplitCompat.install() method if running with the binary that has bundle support
 * enabled. This needs to happen for activities that were not built with SplitCompat
 * support.
 */
class SplitCompatClassAdapter extends ClassVisitor {
    private static final String ANDROID_APP_ACTIVITY_CLASS_NAME = "android/app/Activity";
    private static final String ATTACH_BASE_CONTEXT_METHOD_NAME = "attachBaseContext";
    private static final String ATTACH_BASE_CONTEXT_DESCRIPTOR =
            TypeUtils.getMethodDescriptor(VOID, CONTEXT);

    private static final String MODULE_INSTALLER_CLASS_NAME =
            "org/chromium/components/module_installer/ModuleInstaller";
    private static final String GET_INSTANCE_METHOD_NAME = "getInstance";
    private static final String GET_INSTANCE_DESCRIPTOR =
            TypeUtils.getMethodDescriptor(MODULE_INSTALLER_CLASS_NAME);
    private static final String INIT_ACTIVITY_METHOD_NAME = "initActivity";
    private static final String INIT_ACTIVITY_DESCRIPTOR =
            TypeUtils.getMethodDescriptor(VOID, CONTEXT);

    private boolean mShouldTransform;

    private Set<String> mClassNames;

    private ClassLoader mClassLoader;

    /**
     * Creates instance of SplitCompatClassAdapter.
     *
     * @param visitor
     * @param classNames Names of classes into which the attachBaseContext method will be
     *         injected. Currently, we'll only consider classes for bytecode rewriting only if
     *         they inherit directly from android.app.Activity & not already contain
     *         attachBaseContext method.
     * @param classLoader
     */
    SplitCompatClassAdapter(ClassVisitor visitor, Set<String> classNames, ClassLoader classLoader) {
        super(Opcodes.ASM5, visitor);

        mShouldTransform = false;
        mClassNames = classNames;
        mClassLoader = classLoader;
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName,
            String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);

        if (mClassNames.contains(name)) {
            if (!isSubclassOfActivity(name)) {
                throw new RuntimeException(name
                        + " should be transformed but does not inherit from android.app.Activity");
            }

            mShouldTransform = true;
        }
    }

    @Override
    public MethodVisitor visitMethod(
            int access, String name, String descriptor, String signature, String[] exceptions) {
        // Check if current method matches attachBaseContext & we're supposed to emit code - if so,
        // fail.
        if (mShouldTransform && name.equals(ATTACH_BASE_CONTEXT_METHOD_NAME)) {
            throw new RuntimeException(ATTACH_BASE_CONTEXT_METHOD_NAME + " method already exists");
        }

        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }

    @Override
    public void visitEnd() {
        if (mShouldTransform) {
            // If we reached this place, it means we're rewriting a class that inherits from
            // Activity and there was no exception thrown due to existence of attachBaseContext
            // method - emit code.
            emitAttachBaseContext();
        }

        super.visitEnd();
    }

    /**
     * Generates:
     *
     * <pre>
     * protected void attachBaseContext(Context base) {
     *     super.attachBaseContext(base);
     *     ModuleInstaller.getInstance().initActivity(this);
     * }
     * </pre>
     */
    private void emitAttachBaseContext() {
        MethodVisitor mv = super.visitMethod(ACC_PROTECTED, ATTACH_BASE_CONTEXT_METHOD_NAME,
                ATTACH_BASE_CONTEXT_DESCRIPTOR, null, null);
        mv.visitCode();
        // Push "this" on stack.
        mv.visitVarInsn(ALOAD, 0);
        // Push first method parameter on stack (Context).
        mv.visitVarInsn(ALOAD, 1);
        // Pop argument from stack (Context).
        // Pop target object from stack ("this").
        // Calls attachBaseContext.
        mv.visitMethodInsn(INVOKESPECIAL, ANDROID_APP_ACTIVITY_CLASS_NAME,
                ATTACH_BASE_CONTEXT_METHOD_NAME, ATTACH_BASE_CONTEXT_DESCRIPTOR, false);
        // Push return value on stack (ModuleInstaller).
        // Calls getInstance.
        mv.visitMethodInsn(INVOKESTATIC, MODULE_INSTALLER_CLASS_NAME, GET_INSTANCE_METHOD_NAME,
                GET_INSTANCE_DESCRIPTOR, true);
        // Push "this" on stack.
        mv.visitVarInsn(ALOAD, 0);
        // Pop argument from stack ("this").
        // Pop target object from stack (ModuleInstaller).
        // Calls initActivity.
        mv.visitMethodInsn(INVOKEINTERFACE, MODULE_INSTALLER_CLASS_NAME, INIT_ACTIVITY_METHOD_NAME,
                INIT_ACTIVITY_DESCRIPTOR, true);
        mv.visitInsn(RETURN);
        // Max stack size = 2 (Only push at most 2 before popping).
        // Max locals = 2 ("this" and 1 parameter).
        mv.visitMaxs(2, 2);
        mv.visitEnd();
    }

    /**
     * Checks whether passed in class inherits from android.app.Activity.
     * @param name Name of the class to be checked.
     * @return true if class inherits from android.app.Activity, false otherwise.
     */
    private boolean isSubclassOfActivity(String name) {
        Class<?> activityClass = loadClass(ANDROID_APP_ACTIVITY_CLASS_NAME);
        Class<?> candidateClass = loadClass(name);
        return activityClass.isAssignableFrom(candidateClass);
    }

    private Class<?> loadClass(String className) {
        try {
            return mClassLoader.loadClass(className.replace('/', '.'));
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}