Our project was originally like this. At the beginning of the project, we decided to register as com.B.C. Then, in order to enable users to successfully upgrade from 1.0 to 2.0, we changed the package name com.A.B after the development of the project was completed. Since it was not easy to directly change the whole project directory, So we changed the applicationId under app/build.gradle to the latest com.a.b. When writing an in-app upgrade, in androidmanifest.xml looks like this:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.b.c.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
Copy the code

In use it looks like this (part of the code) :

Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apk);
Copy the code

The react-native code is included in our project, and a number of plug-ins are installed, among which is also defined in the Androidmanifest.xml of the react-native webView:

<provider
    android:name=".RNCWebViewFileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_provider_paths" />
</provider>
Copy the code

Our previous upgrades have been perfect, every time successful; One day, our leader decided to abandon React-Native and use H5 instead, so I was responsible for deleting the react-Native related codes from the project. The deletion process was very pleasant and natural. After the successful deletion, I verified the related functions of the deleted part and found that everything was normal.

Project in the update soon, everything is so taken for granted, users normal upgrade, delete the react – native did not bring problems to the project, with time, the second batch of function development soon, will update once again, I think this update content less, plus test also tests pass, should do not have what problem, However, the bad news happened the next morning. A large number of upgrades failed, and the flash backoff rate rose sharply. Therefore, we tried to reproduce the phenomenon and found that it was an inevitable bug.

At this time I am very happy, but also very sad, happy is that the bug is 100 percent recurrence, sad is, because of my reasons to let the user experience sharply declined, I know, at present to do is to use the fastest speed to fix the bug, so that fewer people “injured”.

Through my investigation, I found that it was caused by the package name. Because the error message directly pointed to the line where the error was reported, the message prompted:

Caused by: java.lang.IllegalArgumentException: Couldn't find meta-data for provider with authority com.a.b.fileprovider
Copy the code

In the androidmanifest.xml file, I found that our has already written com.b.c.

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
Copy the code

I think the problem has been solved, but we haven’t found the cause. First, we need to locate what code caused the problem and why it was possible before. Then we started to check submission records and merge records, and finally it was found that the problem was caused by the deletion of React-Native. However, there is another problem, why did I delete React-Native cause this problem? When I was still struggling, I suddenly received feedback that the app photo function could not be used, which is the core function of our app. Suddenly, it changed from harmless to covered with cuts and bruises. Therefore, I immediately put down the doubts in my head and began to look for the answer in the vast sea of the project. I knew the answer was there, that is, related to the package. Therefore, based on the question, I checked the code related to the package and found that the code (part) was as follows because it was to be saved in the place where the photo was taken:

private const val authorities = "com.b.a.fileprovider"
FileProvider.getUriForFile(requireContext(), authorities, file)
Copy the code

I know it’s because I changed androidmanifest.xml earlier. So I checked all the relevant codes and made sure they were all in line with the package name. Then I packaged them for the test. After the test was completed, I went online again.

All the problems were solved by me, but I still had a lot of doubts in my mind. Since it was difficult for me to read the native code during the development of React-Native, NOW I think I can find the final answer to this problem, so I started my journey to find the problem.

First of all, I went back to the question why deleting react-Native would affect the package name. Then I began to check the deletion by decreasing the deletion to see which row was caused by the deletion.

In fact, if you read this carefully, you must know that it is not the problem of React-Native, but the problem of our code. Therefore, it is precisely the code of React-Native that blocks the problem. The React-native embedded native is introduced based on integration into existing native applications. Gradle app/build.gradle

apply from: file(".. /.. /node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
Copy the code

Native_modules.gradle file. I started with the constructor. I don’t know groovy, but I looked around and found that it was similar to Java.

ReactNativeModules(Logger logger, File root) {
    this.logger = logger
    this.root = root
    def (nativeModules, packageName) = this.getReactNativeConfig()
    this.reactNativeModules = nativeModules
    this.packageName = packageName
}
Copy the code

PackageName here, so I just want to, is it because the execution of this enclosing getReactNativeConfig () changes the packageName, actually I didn’t think that I can change the package name, but I’m not sure, after all, I just contact with android soon. So I continue to look at the implementation of this function:

ArrayList<HashMap<String, String>> getReactNativeConfig() {
    if (this.reactNativeModules ! =null) return this.reactNativeModules
    ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
    def cliResolveScript = "console.log(require('react-native/cli').bin);"
    String[] nodeCommand = ["node"."-e", cliResolveScript]
    def cliPath = this.getCommandOutput(nodeCommand, this.root)
    String[] reactNativeConfigCommand = ["node", cliPath, "config"]
    def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)
    def json
    try {
      json = new JsonSlurper().parseText(reactNativeConfigOutput)
    } catch (Exception exception) {
      throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
    }
    def dependencies = json["dependencies"]
    def project = json["project"] ["android"]
    if (project == null) {
      throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}")
    }
    dependencies.each { name, value ->
      def platformsConfig = value["platforms"];
      def androidConfig = platformsConfig["android"]
      if(androidConfig ! =null && androidConfig["sourceDir"] != null) {
        this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
        HashMap reactNativeModuleConfig = new HashMap<String, String>()
        reactNativeModuleConfig.put("name", name)
        reactNativeModuleConfig.put("nameCleansed", name.replaceAll('[~ *! \' ()] + '.'_').replaceAll('^@([\\w-.]+)/'.'$1 _'))
        reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
        reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
        reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
        this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
        reactNativeModules.add(reactNativeModuleConfig)
      } else {
        this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")}}return [reactNativeModules, json["project"] ["android"] ["packageName"]]. }}Copy the code

It turns out that this is actually the information from the nodeJS execution result, Js file location that is performed in the rn program/node_modules/react – native/node_modules / @ the react – native – community/cli/build/index, js, here is the concrete implementation of js file, There is also a js file, but no code, which executes the run method as shown here:

async function run() {
  try {
    await setupAndRun();
  } catch(e) { handleError(e); }}Copy the code

Then look at the setupAndRun() function:

async function setupAndRun() {
  if (process.argv.includes('config')) {
    _cliTools().logger.disable();
  }
  _cliTools().logger.setVerbose(process.argv.includes('--verbose')); // We only have a setup script for UNIX envs currently
  if(process.platform ! = ='win32') {
    const scriptName = 'setup_env.sh';
    const absolutePath = _path().default.join(__dirname, '.. ', scriptName);
    try {
      _child_process().default.execFileSync(absolutePath, {
        stdio: 'pipe'}); }catch (error) {
      _cliTools().logger.warn(
        `Failed to run environment setup script "${scriptName}"\n\n${_chalk().default.red(
          error,
        )}`,); _cliTools().logger.info(`React Native CLI will continue to run if your local environment matches what React Native expects. If it does fail, check out "${absolutePath}" and adjust your environment to match it.`,); }}for (const command of _commands.detachedCommands) {
    attachCommand(command);
  }
  try {
    const config = (0, _config.default)();
    _cliTools().logger.enable();
    for (const command of[..._commands.projectCommands, ...config.commands]) { attachCommand(command, config); }}catch (error) {
    if (error.message.includes("We couldn't find a package.json")) {
      _cliTools().logger.enable();
      _cliTools().logger.debug(error.message);
      _cliTools().logger.debug(
        'Failed to load configuration of your project. Only a subset of commands will be available.',); }else {
      throw new (_cliTools().CLIError)(
        'Failed to load configuration of your project.',
        error,
      );
    }
  }
  _commander().default.parse(process.argv);
  if (_commander().default.rawArgs.length === 2) {
    _commander().default.outputHelp();
  }
  if (
    _commander().default.args.length === 0 &&
    _commander().default.rawArgs.includes('--version')) {console.log(pkgJson.version); }}Copy the code

After I printed the log, I found that the _commander().default.parse(process.argv) line was returned to Groovy. However, I found that this line was also reading configuration, and had nothing to do with modification, so I started to assume that, Could is groovy eventually modify, just get information from js, so I just to get the value of the modified directly, namely native_modules. Gradle inside this. GetReactNativeConfig function return value, So I changed it:

ArrayList<HashMap<String, String>> getReactNativeConfig() {
    if (this.reactNativeModules ! =null) return this.reactNativeModules
    ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
    def dependencies = new JsonSlurper().parseText('{"react-native-webview":{"root":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview","name":"react -native-webview","platforms":{"ios":{"sourceDir":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webvie w/ios","folder":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview","pbxprojPath":"/Users/wujingyu e/Works/yq-bss-tour-rn/node_modules/react-native-webview/ios/RNCWebView.xcodeproj/project.pbxproj","podfile":null,"podsp ecPath":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview/react-native-webview.podspec","projectP ath":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview/ios/RNCWebView.xcodeproj","projectName":"R NCWebView.xcodeproj","libraryFolder":"Libraries","sharedLibraries":[],"plist":[],"scriptPhases":[]},"android":{"sourceDi r":"/Users/wujingyue/Works/yq-bss-tour-rn/node_modules/react-native-webview/android","folder":"/Users/wujingyue/Works/yq -bss-tour-rn/node_modules/react-native-webview","packageImportPath":"import com.reactnativecommunity.webview.RNCWebViewPackage;" ,"packageInstance":"new RNCWebViewPackage()"}},"assets":[],"hooks":{},"params":[]}}')
dependencies.each { name, value ->
      def platformsConfig = value["platforms"];
      def androidConfig = platformsConfig["android"]
      if(androidConfig ! =null && androidConfig["sourceDir"] != null) {
        this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
        HashMap reactNativeModuleConfig = new HashMap<String, String>()
        reactNativeModuleConfig.put("name", name)
        reactNativeModuleConfig.put("nameCleansed", name.replaceAll('[~ *! \' ()] + '.'_').replaceAll('^@([\\w-.]+)/'.'$1 _'))
        reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
        reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
        reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
        this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
        reactNativeModules.add(reactNativeModuleConfig)

      } else {
        this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")}}// return com.a.b directly here
    return [reactNativeModules, "com.a.b"]; }}Copy the code

The dependencies variable has many more values. New JsonSlurper().parseText(‘{}’); Therefore, I proposed a hypothesis based on this phenomenon, which was caused by a plug-in in the string. So, I started to put plug-ins into it for testing according to this hypothesis, and finally found that the React-native WebView, What you don’t know is that the React-Native WebView is the last plugin. I tested all the previous ones, which was both happy and sad. Finally, I narrowed it down further.

My favorite or “annotation method”, is the classic “method”, the first thing I put all the code is commented out, only a shell, can still be found normal installation, that is not in the code, and then I to build plug-ins. Gradle USES “annotation method”, the result can be, that is not here, then I feel powerless, So I started to check every file in the entire plugin, and a file appeared in front of me. When I opened it, I saw that the plugin also defined . And it’s the right way, so I hypothesize that androidmanifest.xml would be able to explain this if it ended up using the plugin for the React-native webView, but that’s just an assumption. I have to prove my hypothesis in practice.

First, I tried to modify the authorities under AndroidManifest.xml in the react-native WebView plugin. First, I changed the authorities to be the same as that of the project. I went through the documentation to further prove my conclusion. First I looked at the documentation related to the configuration of Androidmanifest.xml. I saw the following description:

Android :authorities A list of one or more URI authorities used to identify data provided by content providers. When listing multiple authorizers, separate their names with a semicolon. To avoid conflicts, the name of the authorized party should follow the style of Java naming conventions (such as com. Example. The provider. Cartoonprovider). Typically, it is the name of a ContentProvider subclass that implements the provider. There is no default value. At least one authorizer must be specified.

I didn’t think much of it the first time I saw it, but the document behind it made me think of it, verified it, and finally found the answer. First, a colleague found itMerge multiple manifest filesThis one, confirmedAndroidManifest.xmlIt’s going to merge, and then you see thisReview the merged listings and look for conflicts:And then I went and looked at our project, and I found this, and I looked at the merged content, and I foundreact-native-webviewIn the end, which is the replacement projectauthoritiesBut I looked at the file carefully and saw that there were definitions belowauthoritiesThat is, if it’s an overlay it doesn’t make sense because the backauthoritiesIt would cause an error, but it didn’t, so I tried to change where I used it, and put itFileProvider.getUriForFile(requireContext(), authorities, file)Change the second parameter to these definitions, and the discovery will still succeed, that is to say, all the things we have defined will work. Then I think of the sentence above, which I have marked in red, and find that all the fog has been cleared up.

So that’s kind of the end of it, but I was wondering why it was designed that way. I finally came up with the following answer: how can I ensure that a plugin can be used everywhere without knowing about the authorities definition in another project? The answer is obvious: multiple validation, the plugin does not need to know about the definition in the project, just use the model defined in the plugin.