diff --git a/PRESUBMIT.py b/PRESUBMIT.py index dec3bd35bb0e4..0b09333e2f40c 100644 --- a/PRESUBMIT.py +++ b/PRESUBMIT.py @@ -1306,6 +1306,7 @@ 'build/android/gyp/apkbuilder.pydeps', 'build/android/gyp/assert_static_initializers.pydeps', 'build/android/gyp/bytecode_processor.pydeps', + 'build/android/gyp/bytecode_rewriter.pydeps', 'build/android/gyp/compile_java.pydeps', 'build/android/gyp/compile_resources.pydeps', 'build/android/gyp/copy_ex.pydeps', diff --git a/build/android/bytecode/BUILD.gn b/build/android/bytecode/BUILD.gn index 4d29aca9dbcdd..b56f341d90ceb 100644 --- a/build/android/bytecode/BUILD.gn +++ b/build/android/bytecode/BUILD.gn @@ -18,3 +18,17 @@ java_binary("bytecode_processor") { wrapper_script_name = "helper/bytecode_processor" enable_bytecode_checks = false } + +java_binary("fragment_activity_replacer") { + sources = [ + "java/org/chromium/bytecode/ByteCodeRewriter.java", + "java/org/chromium/bytecode/FragmentActivityReplacer.java", + ] + main_class = "org.chromium.bytecode.FragmentActivityReplacer" + deps = [ + "//third_party/android_deps:org_ow2_asm_asm_commons_java", + "//third_party/android_deps:org_ow2_asm_asm_java", + "//third_party/android_deps:org_ow2_asm_asm_util_java", + ] + wrapper_script_name = "helper/fragment_activity_replacer" +} diff --git a/build/android/bytecode/java/org/chromium/bytecode/ByteCodeRewriter.java b/build/android/bytecode/java/org/chromium/bytecode/ByteCodeRewriter.java new file mode 100644 index 0000000000000..3d0d9cdd47dc6 --- /dev/null +++ b/build/android/bytecode/java/org/chromium/bytecode/ByteCodeRewriter.java @@ -0,0 +1,91 @@ +// Copyright 2020 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 org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +/** + * Base class for scripts that perform bytecode modifications on a jar file. + */ +public abstract class ByteCodeRewriter { + private static final String CLASS_FILE_SUFFIX = ".class"; + + public void rewrite(File inputJar, File outputJar) throws IOException { + if (!inputJar.exists()) { + throw new FileNotFoundException("Input jar not found: " + inputJar.getPath()); + } + try (InputStream inputStream = new BufferedInputStream(new FileInputStream(inputJar))) { + try (OutputStream outputStream = new FileOutputStream(outputJar)) { + processZip(inputStream, outputStream); + } + } + } + + /** Returns true if the class at the given path in the archive should be rewritten. */ + protected abstract boolean shouldRewriteClass(String classPath); + + /** + * Returns the ClassVisitor that should be used to modify the bytecode of class at the given + * path in the archive. + */ + protected abstract ClassVisitor getClassVisitorForClass( + String classPath, ClassVisitor delegate); + + private void processZip(InputStream inputStream, OutputStream outputStream) { + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + ZipInputStream zipInputStream = new ZipInputStream(inputStream); + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + boolean handled = processClassEntry(entry, zipInputStream, buffer); + if (handled) { + ZipEntry newEntry = new ZipEntry(entry.getName()); + zipOutputStream.putNextEntry(newEntry); + zipOutputStream.write(buffer.toByteArray(), 0, buffer.size()); + } else { + zipOutputStream.putNextEntry(entry); + zipInputStream.transferTo(zipOutputStream); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private boolean processClassEntry( + ZipEntry entry, InputStream inputStream, OutputStream outputStream) { + if (!entry.getName().endsWith(CLASS_FILE_SUFFIX) || !shouldRewriteClass(entry.getName())) { + return false; + } + try { + ClassReader reader = new ClassReader(inputStream); + ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES); + ClassVisitor classVisitor = getClassVisitorForClass(entry.getName(), writer); + reader.accept(classVisitor, ClassReader.EXPAND_FRAMES); + + writer.visitEnd(); + byte[] classData = writer.toByteArray(); + outputStream.write(classData, 0, classData.length); + return true; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/build/android/bytecode/java/org/chromium/bytecode/FragmentActivityReplacer.java b/build/android/bytecode/java/org/chromium/bytecode/FragmentActivityReplacer.java new file mode 100644 index 0000000000000..2199588b173a3 --- /dev/null +++ b/build/android/bytecode/java/org/chromium/bytecode/FragmentActivityReplacer.java @@ -0,0 +1,119 @@ +// Copyright 2020 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 org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.MethodRemapper; +import org.objectweb.asm.commons.Remapper; + +import java.io.File; +import java.io.IOException; + +/** + * Java application that modifies Fragment.getActivity() to return an Activity instead of a + * FragmentActivity, and updates any existing getActivity() calls to reference the updated method. + * + * See crbug.com/1144345 for more context. + */ +public class FragmentActivityReplacer extends ByteCodeRewriter { + private static final String FRAGMENT_CLASS_PATH = "androidx/fragment/app/Fragment.class"; + private static final String FRAGMENT_ACTIVITY_INTERNAL_CLASS_NAME = + "androidx/fragment/app/FragmentActivity"; + private static final String ACTIVITY_INTERNAL_CLASS_NAME = "android/app/Activity"; + private static final String GET_ACTIVITY_METHOD_NAME = "getActivity"; + private static final String REQUIRE_ACTIVITY_METHOD_NAME = "requireActivity"; + private static final String OLD_METHOD_DESCRIPTOR = + "()Landroidx/fragment/app/FragmentActivity;"; + private static final String NEW_METHOD_DESCRIPTOR = "()Landroid/app/Activity;"; + + public static void main(String[] args) throws IOException { + // Invoke this script using //build/android/gyp/bytecode_processor.py + if (args.length != 2) { + System.err.println("Expected 2 arguments: [input.jar] [output.jar]"); + System.exit(1); + } + + FragmentActivityReplacer rewriter = new FragmentActivityReplacer(); + rewriter.rewrite(new File(args[0]), new File(args[1])); + } + + @Override + protected boolean shouldRewriteClass(String classPath) { + return true; + } + + @Override + protected ClassVisitor getClassVisitorForClass(String classPath, ClassVisitor delegate) { + ClassVisitor getActivityReplacer = new GetActivityReplacer(delegate); + if (classPath.equals(FRAGMENT_CLASS_PATH)) { + return new FragmentClassVisitor(getActivityReplacer); + } + return getActivityReplacer; + } + + /** Updates any Fragment.getActivity/requireActivity() calls to call the replaced method. */ + private static class GetActivityReplacer extends ClassVisitor { + private GetActivityReplacer(ClassVisitor baseVisitor) { + super(Opcodes.ASM7, baseVisitor); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor base = super.visitMethod(access, name, descriptor, signature, exceptions); + return new MethodVisitor(Opcodes.ASM7, base) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, + String descriptor, boolean isInterface) { + if ((opcode == Opcodes.INVOKEVIRTUAL || opcode == Opcodes.INVOKESPECIAL) + && descriptor.equals(OLD_METHOD_DESCRIPTOR) + && (name.equals(GET_ACTIVITY_METHOD_NAME) + || name.equals(REQUIRE_ACTIVITY_METHOD_NAME))) { + super.visitMethodInsn( + opcode, owner, name, NEW_METHOD_DESCRIPTOR, isInterface); + } else { + super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + } + } + }; + } + } + + /** + * Makes Fragment.getActivity() and Fragment.requireActivity() non-final, and changes their + * return types to Activity. + */ + private static class FragmentClassVisitor extends ClassVisitor { + private FragmentClassVisitor(ClassVisitor baseVisitor) { + super(Opcodes.ASM7, baseVisitor); + } + + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor base; + // Update the descriptor of getActivity/requireActivity, and make them non-final. + if (name.equals(GET_ACTIVITY_METHOD_NAME) + || name.equals(REQUIRE_ACTIVITY_METHOD_NAME)) { + base = super.visitMethod( + access & ~Opcodes.ACC_FINAL, name, NEW_METHOD_DESCRIPTOR, null, exceptions); + } else { + base = super.visitMethod(access, name, descriptor, signature, exceptions); + } + + return new MethodRemapper(base, new Remapper() { + @Override + public String mapType(String internalName) { + if (internalName.equals(FRAGMENT_ACTIVITY_INTERNAL_CLASS_NAME)) { + return ACTIVITY_INTERNAL_CLASS_NAME; + } + return internalName; + } + }); + } + } +} diff --git a/build/android/gyp/bytecode_rewriter.py b/build/android/gyp/bytecode_rewriter.py new file mode 100755 index 0000000000000..b37ef6f466919 --- /dev/null +++ b/build/android/gyp/bytecode_rewriter.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# Copyright 2020 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. +"""Wrapper script around ByteCodeRewriter subclass scripts.""" + +import argparse +import sys + +from util import build_utils + + +def main(argv): + argv = build_utils.ExpandFileArgs(argv[1:]) + parser = argparse.ArgumentParser() + build_utils.AddDepfileOption(parser) + parser.add_argument('--script', + required=True, + help='Path to the java binary wrapper script.') + parser.add_argument('--classpath', action='append', nargs='+') + parser.add_argument('--input-jar', required=True) + parser.add_argument('--output-jar', required=True) + args = parser.parse_args(argv) + + classpath = build_utils.ParseGnList(args.classpath) + build_utils.WriteDepfile(args.depfile, args.output_jar, inputs=classpath) + + classpath.append(args.input_jar) + cmd = [ + args.script, '--classpath', ':'.join(classpath), args.input_jar, + args.output_jar + ] + build_utils.CheckOutput(cmd, print_stdout=True) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/build/android/gyp/bytecode_rewriter.pydeps b/build/android/gyp/bytecode_rewriter.pydeps new file mode 100644 index 0000000000000..b8f304a783623 --- /dev/null +++ b/build/android/gyp/bytecode_rewriter.pydeps @@ -0,0 +1,6 @@ +# Generated by running: +# build/print_python_deps.py --root build/android/gyp --output build/android/gyp/bytecode_rewriter.pydeps build/android/gyp/bytecode_rewriter.py +../../gn_helpers.py +bytecode_rewriter.py +util/__init__.py +util/build_utils.py diff --git a/build/config/android/internal_rules.gni b/build/config/android/internal_rules.gni index e4468d8b92ca0..f79f26ef501ed 100644 --- a/build/config/android/internal_rules.gni +++ b/build/config/android/internal_rules.gni @@ -3982,8 +3982,56 @@ if (enable_java_templates) { } } # _has_sources - # TODO(crbug.com/1123216): Implement bytecode_rewriter_target. - not_needed(invoker, [ "bytecode_rewriter_target" ]) + if (_is_prebuilt || _build_device_jar || _build_host_jar) { + _unprocessed_jar_deps = _full_classpath_deps + if (_has_sources) { + _unprocessed_jar_deps += [ ":$_compile_java_target" ] + } + } + + if (defined(invoker.bytecode_rewriter_target)) { + assert(_build_host_jar || _build_device_jar, + "A host or device jar must be created to use bytecode rewriting") + + _rewritten_jar = + string_replace(_unprocessed_jar_path, ".jar", "_rewritten.jar") + _rewritten_jar_target_name = "${target_name}__rewritten" + _rewriter_path = root_build_dir + "/bin/helper/" + + get_label_info(invoker.bytecode_rewriter_target, "name") + _rebased_build_config = rebase_path(_build_config, root_build_dir) + action_with_pydeps(_rewritten_jar_target_name) { + script = "//build/android/gyp/bytecode_rewriter.py" + inputs = [ + _rewriter_path, + _build_config, + _unprocessed_jar_path, + ] + outputs = [ _rewritten_jar ] + depfile = "$target_gen_dir/$target_name.d" + args = [ + "--depfile", + rebase_path(depfile, root_build_dir), + "--script", + rebase_path(_rewriter_path, root_build_dir), + "--classpath", + "@FileArg($_rebased_build_config:deps_info:javac_full_classpath)", + "--classpath", + "@FileArg($_rebased_build_config:android:sdk_jars)", + "--input-jar", + rebase_path(_unprocessed_jar_path, root_build_dir), + "--output-jar", + rebase_path(_rewritten_jar, root_build_dir), + ] + deps = _unprocessed_jar_deps + [ + ":$_build_config_target_name", + invoker.bytecode_rewriter_target, + ] + } + + _unprocessed_jar_deps = [] + _unprocessed_jar_deps = [ ":$_rewritten_jar_target_name" ] + _unprocessed_jar_path = _rewritten_jar + } if (_is_prebuilt) { generate_interface_jar(_header_target_name) { @@ -3998,10 +4046,7 @@ if (enable_java_templates) { # target. If we can change compile & desugar steps to use direct # interface classpath rather than full interface classpath, then this # could just be _non_java_deps. - deps = _classpath_deps - if (_has_sources) { - deps += [ ":$_compile_java_target" ] - } + deps = _unprocessed_jar_deps } _public_deps += [ ":$_header_target_name" ] } @@ -4017,10 +4062,7 @@ if (enable_java_templates) { build_config = _build_config build_config_dep = ":$_build_config_target_name" input_jar_path = _unprocessed_jar_path - jar_deps = _non_java_deps - if (_has_sources) { - jar_deps += [ ":$_compile_java_target" ] - } + jar_deps = _unprocessed_jar_deps if (_build_host_jar) { host_jar_path = _host_processed_jar_path } @@ -4063,10 +4105,7 @@ if (enable_java_templates) { _bytecode_checks_target = "${target_name}__validate_classpath" bytecode_processor(_bytecode_checks_target) { forward_variables_from(invoker, [ "missing_classes_allowlist" ]) - deps = _full_classpath_deps - if (_has_sources) { - deps += [ ":$_compile_java_target" ] - } + deps = _unprocessed_jar_deps + [ ":$_build_config_target_name" ] requires_android = _requires_android target_label = get_label_info(":${invoker.target_name}", "label_no_toolchain") diff --git a/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunFragment.java b/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunFragment.java index baeda3365cc35..3d57b546ae98f 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunFragment.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/firstrun/FirstRunFragment.java @@ -4,7 +4,7 @@ package org.chromium.chrome.browser.firstrun; -import androidx.fragment.app.FragmentActivity; +import android.app.Activity; /** * This interface is implemented by FRE fragments. @@ -25,7 +25,7 @@ default boolean interceptBackPressed() { /** * @see Fragment#getActivity(). */ - FragmentActivity getActivity(); + Activity getActivity(); /** * Set the a11y focus when the fragment is shown on the screen. diff --git a/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordEntryEditor.java b/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordEntryEditor.java index bbb980970bd97..d2782bf2fe687 100644 --- a/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordEntryEditor.java +++ b/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordEntryEditor.java @@ -141,8 +141,7 @@ private void performActionAfterReauth( } mPendingAction = action; ReauthenticationManager.displayReauthenticationFragment(reasonString, View.NO_ID, - getActivity().getSupportFragmentManager(), - ReauthenticationManager.ReauthScope.ONE_AT_A_TIME); + getParentFragmentManager(), ReauthenticationManager.ReauthScope.ONE_AT_A_TIME); } @Override @@ -150,4 +149,4 @@ public void onDestroy() { super.onDestroy(); PasswordEditingDelegateProvider.getInstance().getPasswordEditingDelegate().destroy(); } -} \ No newline at end of file +} diff --git a/chrome/browser/webauthn/android/java/src/org/chromium/chrome/browser/webauthn/CableAuthenticatorModuleProvider.java b/chrome/browser/webauthn/android/java/src/org/chromium/chrome/browser/webauthn/CableAuthenticatorModuleProvider.java index 3e3796e0c2255..9c8f6850b63b5 100644 --- a/chrome/browser/webauthn/android/java/src/org/chromium/chrome/browser/webauthn/CableAuthenticatorModuleProvider.java +++ b/chrome/browser/webauthn/android/java/src/org/chromium/chrome/browser/webauthn/CableAuthenticatorModuleProvider.java @@ -84,8 +84,7 @@ public View onCreateView( private void showModule() { mStatus.setText("Installed."); - FragmentTransaction transaction = - getActivity().getSupportFragmentManager().beginTransaction(); + FragmentTransaction transaction = getParentFragmentManager().beginTransaction(); Fragment fragment = Cablev2AuthenticatorModule.getImpl().getFragment(); Bundle arguments = getArguments(); if (arguments == null) { diff --git a/third_party/android_deps/BUILD.gn b/third_party/android_deps/BUILD.gn index 018502864131c..eca9299bf0bc7 100644 --- a/third_party/android_deps/BUILD.gn +++ b/third_party/android_deps/BUILD.gn @@ -366,6 +366,9 @@ android_aar_prebuilt("androidx_fragment_fragment_java") { jar_excluded_patterns = [ "androidx/fragment/app/DialogFragment*" ] ignore_proguard_configs = true + + bytecode_rewriter_target = + "//build/android/bytecode:fragment_activity_replacer" } # This is generated, do not edit. Update BuildConfigGenerator.groovy instead. @@ -577,6 +580,8 @@ android_aar_prebuilt("androidx_preference_preference_java") { "androidx/preference/PreferenceFragmentCompat*", ] + bytecode_rewriter_target = + "//build/android/bytecode:fragment_activity_replacer" ignore_proguard_configs = true } diff --git a/third_party/android_deps/buildSrc/src/main/groovy/BuildConfigGenerator.groovy b/third_party/android_deps/buildSrc/src/main/groovy/BuildConfigGenerator.groovy index f83c1f955406c..e88767d77f0a6 100644 --- a/third_party/android_deps/buildSrc/src/main/groovy/BuildConfigGenerator.groovy +++ b/third_party/android_deps/buildSrc/src/main/groovy/BuildConfigGenerator.groovy @@ -354,6 +354,8 @@ class BuildConfigGenerator extends DefaultTask { | ] | | ignore_proguard_configs = true + | + | bytecode_rewriter_target = "//build/android/bytecode:fragment_activity_replacer" |""".stripMargin()) break case 'androidx_media_media': @@ -496,6 +498,7 @@ class BuildConfigGenerator extends DefaultTask { | "androidx/preference/PreferenceFragmentCompat*", | ] | + | bytecode_rewriter_target = "//build/android/bytecode:fragment_activity_replacer" |""".stripMargin()) // Replace broad library -keep rules with a more limited set in // chrome/android/java/proguard.flags instead. diff --git a/third_party/android_deps/local_modifications/androidx_preference_preference/README b/third_party/android_deps/local_modifications/androidx_preference_preference/README index f4fb51cf978ed..697cd14415edf 100644 --- a/third_party/android_deps/local_modifications/androidx_preference_preference/README +++ b/third_party/android_deps/local_modifications/androidx_preference_preference/README @@ -1,8 +1,8 @@ This directory contains PreferenceFragmentCompat.java and PreferenceDialogFragmentCompat.java, copied without changes from the AndroidX -preference library at commit beeb6fb. These files contain two changes (commits -72c0381 and beeb6fb) that we want in Chromium, but are not yet in an official -preference library release. +preference library at commit e865a9b. These files contain three changes +(commits 72c0381, beeb6fb, and e865a9b) that we want in Chromium, but are not +yet in an official preference library release. To pull in these changes, we exclude PreferenceFragmentCompat and PreferenceDialogFragmentCompat from the androidx_preference_preference library diff --git a/third_party/android_deps/local_modifications/androidx_preference_preference/androidx_preference_preference_java.jar b/third_party/android_deps/local_modifications/androidx_preference_preference/androidx_preference_preference_java.jar index 201cd50404ba6..de88d158ec9e7 100644 Binary files a/third_party/android_deps/local_modifications/androidx_preference_preference/androidx_preference_preference_java.jar and b/third_party/android_deps/local_modifications/androidx_preference_preference/androidx_preference_preference_java.jar differ diff --git a/third_party/android_deps/local_modifications/androidx_preference_preference/java/androidx/preference/PreferenceFragmentCompat.java b/third_party/android_deps/local_modifications/androidx_preference_preference/java/androidx/preference/PreferenceFragmentCompat.java index 1fa2669828511..5922dfab41737 100644 --- a/third_party/android_deps/local_modifications/androidx_preference_preference/java/androidx/preference/PreferenceFragmentCompat.java +++ b/third_party/android_deps/local_modifications/androidx_preference_preference/java/androidx/preference/PreferenceFragmentCompat.java @@ -61,7 +61,7 @@ * *

To build a hierarchy from code, use * {@link PreferenceManager#createPreferenceScreen(Context)} to create the root - * {@link PreferenceScreen}. Once you have added other {@link Preference}s to this root scree + * {@link PreferenceScreen}. Once you have added other {@link Preference}s to this root screen * with {@link PreferenceScreen#addPreference(Preference)}, you then need to set the screen as * the root screen in your hierarchy with {@link #setPreferenceScreen(PreferenceScreen)}. * @@ -420,8 +420,7 @@ public boolean onPreferenceTreeClick(Preference preference) { + "implement this method so that you can configure the new " + "fragment that will be displayed, and set a transition between " + "the fragments."); - final FragmentManager fragmentManager = requireActivity() - .getSupportFragmentManager(); + final FragmentManager fragmentManager = getParentFragmentManager(); final Bundle args = preference.getExtras(); final Fragment fragment = fragmentManager.getFragmentFactory().instantiate( requireActivity().getClassLoader(), preference.getFragment()); @@ -457,7 +456,7 @@ public void onNavigateToScreen(PreferenceScreen preferenceScreen) { .onPreferenceStartScreen(this, preferenceScreen); } if (!handled && getContext() instanceof OnPreferenceStartScreenCallback) { - ((OnPreferenceStartScreenCallback) getContext()) + handled = ((OnPreferenceStartScreenCallback) getContext()) .onPreferenceStartScreen(this, preferenceScreen); } // Check the Activity as well in case getContext was overridden to return something other