910e62b5创建于 1月15日历史提交
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/resource/resource_bundle_android.h"

#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/android/apk_assets.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "ui/base/buildflags.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/data_pack.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/resource/resource_scale_factor.h"
#include "ui/base/ui_base_paths.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "ui/base/ui_base_jni_headers/ResourceBundle_jni.h"

namespace ui {

namespace {

using FileDescriptor = int;

bool g_locale_paks_in_apk = false;
bool g_load_non_webview_locale_paks = false;
// It is okay to cache and share these file descriptors since the
// ResourceBundle singleton never closes the handles.
FileDescriptor g_chrome_100_percent_fd = -1;
FileDescriptor g_chrome_200_percent_fd = -1;
FileDescriptor g_resources_pack_fd = -1;

std::vector<ResourceBundle::FdAndRegion>& GetLocalePaksGlobal() {
  // Required since `static std::vector<ResourceBundle::FdAndRegion>` requires a
  // global destructor.
  static base::NoDestructor<std::vector<ResourceBundle::FdAndRegion>>
      locale_paks;
  return *locale_paks;
}

base::MemoryMappedFile::Region g_chrome_100_percent_region;
base::MemoryMappedFile::Region g_chrome_200_percent_region;
base::MemoryMappedFile::Region g_resources_pack_region;

bool LoadFromApkOrFile(const char* apk_path,
                       const base::FilePath* disk_path,
                       FileDescriptor* out_fd,
                       base::MemoryMappedFile::Region* out_region) {
  DCHECK_EQ(*out_fd, -1) << "Attempt to load " << apk_path << " twice.";
  if (apk_path != nullptr) {
    *out_fd = base::android::OpenApkAsset(apk_path, out_region);
  }
  // For unit tests, the file exists on disk.
  if (*out_fd < 0 && disk_path != nullptr) {
    auto flags =
        static_cast<uint32_t>(base::File::FLAG_OPEN | base::File::FLAG_READ);
    *out_fd = base::File(*disk_path, flags).TakePlatformFile();
    *out_region = base::MemoryMappedFile::Region::kWholeFile;
  }
  bool success = *out_fd >= 0;
  if (!success) {
    LOG(ERROR) << "Failed to open pak file: " << apk_path;
    base::android::DumpLastOpenApkAssetFailure();
  }
  return success;
}

// Returns the path within the apk for the given locale's .pak file, or an
// empty string if it doesn't exist.
// Only locale paks for the active Android language can be retrieved.
// If `in_split` is true, look into bundle split-specific location (e.g.
// 'assets/locales#lang_<lang>/<locale>.pak', otherwise use the default
// WebView-related location, i.e. 'assets/stored-locales/<locale>.pak'.
// If `log_error`, logs the path to logcat, but does not abort.
std::string GetPathForAndroidLocalePakWithinApk(std::string_view locale,
                                                ResourceBundle::Gender gender,
                                                bool in_bundle,
                                                bool log_error) {
  JNIEnv* env = base::android::AttachCurrentThread();
  base::android::ScopedJavaLocalRef<jstring> ret =
      Java_ResourceBundle_getLocalePakResourcePath(
          env, base::android::ConvertUTF8ToJavaString(env, locale),
          static_cast<jint>(gender), in_bundle, log_error);
  if (ret.obj() == nullptr) {
    return std::string();
  }
  return base::android::ConvertJavaStringToUTF8(env, ret.obj());
}

FileDescriptor LoadLocalePakFromApk(
    const std::string& app_locale,
    ResourceBundle::Gender gender,
    bool in_split,
    base::MemoryMappedFile::Region* out_region) {
  bool log_error = true;
  std::string locale_path_within_apk = GetPathForAndroidLocalePakWithinApk(
      app_locale, gender, in_split, log_error);
  if (locale_path_within_apk.empty()) {
    return -1;
  }
  return base::android::OpenApkAsset(locale_path_within_apk, out_region);
}

std::unique_ptr<DataPack> LoadDataPackFromLocalePak(
    FileDescriptor locale_pack_fd,
    const base::MemoryMappedFile::Region& region) {
  auto data_pack = std::make_unique<DataPack>(k100Percent);
  CHECK(data_pack->LoadFromFileRegion(base::File(locale_pack_fd), region))
      << "failed to load locale.pak";
  return data_pack;
}

bool LocaleDataPakExists(std::string_view locale,
                         ResourceBundle::Gender gender,
                         bool in_split,
                         bool log_error) {
  return !GetPathForAndroidLocalePakWithinApk(locale, gender, in_split,
                                              log_error)
              .empty();
}

bool LoadLocaleResourcesForLocaleAndGender(
    const std::string& app_locale,
    const ResourceBundle::Gender gender,
    ResourceBundle::FdAndRegion* webview_locale_pack,
    ResourceBundle::FdAndRegion* non_webview_locale_pack,
    std::vector<std::unique_ptr<ResourceHandle>>* locale_resources_data) {
  // Some Chromium apps have two sets of .pak files for their UI strings, i.e.:
  //
  // a) WebView strings, which are always stored uncompressed under
  //    assets/stored-locales/ inside the APK or App Bundle.
  //
  // b) For APKs, the Chrome UI strings are stored under assets/locales/.
  //
  // c) For App Bundles, Chrome UI strings are stored uncompressed under
  //    assets/locales#lang_<lang>/ (where <lang> is an Android language code)
  //    and assets/fallback-locales/ (for en-US.pak only).
  //
  // Which .pak files to load are determined here by two global variables with
  // the following meaning:
  //
  //  g_locale_paks_in_apk:
  //    If true, load the WebView strings from stored-locales/<locale>.pak file
  //    as the webview locale pak file.
  //
  //    If false, try to load it from the app bundle specific location
  //    (e.g. locales#lang_<language>/<locale>.pak). If the latter does not
  //    exist, try to lookup the APK-specific locale .pak file.
  //
  //    g_locale_paks_in_apk is set by SetLocalePaksStoredInApk() which
  //    is called from the WebView startup code.
  //
  //  g_load_non_webview_locale_paks:
  //    If true, load the Webview strings from stored-locales/<locale>.pak file
  //    as the non-webview locale pak file. Otherwise don't load a non-webview
  //    locale at all.
  //
  //    This is set by DetectAndSetLoadNonWebViewLocalePaks() which is called
  //    during ChromeMainDelegate::PostEarlyInitialization(). It will set the
  //    value to true iff there are stored-locale/ .pak files.
  //
  // In other words, if both |g_locale_paks_in_apk| and
  // |g_load_non_webview_locale_paks| are true, the stored-locales file will be
  // loaded twice as both the webview and non-webview. However, this should
  // never happen in practice.

  // Load webview locale .pak file.
  if (g_locale_paks_in_apk) {
    webview_locale_pack->fd = LoadLocalePakFromApk(
        app_locale, gender, false /* in_split */, &webview_locale_pack->region);
  } else {
    webview_locale_pack->fd = -1;

    CHECK(ResourceBundle::HasSharedInstance());

    // Support overridden pak path for testing.
    base::FilePath locale_file_path =
        ResourceBundle::GetSharedInstance().GetOverriddenPakPath();
    if (locale_file_path.empty()) {
      // Try to find the uncompressed split-specific asset file.
      webview_locale_pack->fd =
          LoadLocalePakFromApk(app_locale, gender, true /* in_split */,
                               &webview_locale_pack->region);
    }
    if (webview_locale_pack->fd < 0) {
      // Otherwise, try to locate the side-loaded locale .pak file (for tests).
      if (locale_file_path.empty()) {
        auto path =
            ResourceBundle::GetSharedInstance().GetLocaleFilePath(app_locale);
        if (base::PathExists(path)) {
          locale_file_path = std::move(path);
        }
      }

      if (locale_file_path.empty()) {
        // It's possible that there is no locale.pak.
        LOG(WARNING) << "locale_file_path.empty() for locale " << app_locale;
        return false;
      }
      auto flags =
          static_cast<uint32_t>(base::File::FLAG_OPEN | base::File::FLAG_READ);
      webview_locale_pack->fd =
          base::File(locale_file_path, flags).TakePlatformFile();
      webview_locale_pack->region = base::MemoryMappedFile::Region::kWholeFile;
    }
  }

  auto locale_data = LoadDataPackFromLocalePak(webview_locale_pack->fd,
                                               webview_locale_pack->region);

  if (!locale_data.get()) {
    return false;
  }

  locale_resources_data->push_back(std::move(locale_data));

  // Load non-webview locale .pak file if it exists. For debug build monochrome,
  // a non-webview locale pak will always be loaded; however, it should be
  // unnecessary for loading locale resources because the webview locale pak
  // would have a copy of all the resources in the non-webview locale pak.
  if (g_load_non_webview_locale_paks) {
    non_webview_locale_pack->fd =
        LoadLocalePakFromApk(app_locale, gender, false /* in_split */,
                             &non_webview_locale_pack->region);

    locale_data = LoadDataPackFromLocalePak(non_webview_locale_pack->fd,
                                            non_webview_locale_pack->region);

    if (!locale_data.get()) {
      return false;
    }

    locale_resources_data->push_back(std::move(locale_data));
  }

  return true;
}

}  // namespace

void ResourceBundle::LoadCommonResources() {
  base::FilePath disk_path;
  base::PathService::Get(ui::DIR_RESOURCE_PAKS_ANDROID, &disk_path);
  disk_path = disk_path.AppendASCII("chrome_100_percent.pak");
  bool success =
      LoadFromApkOrFile("assets/chrome_100_percent.pak", &disk_path,
                        &g_chrome_100_percent_fd, &g_chrome_100_percent_region);
  DCHECK(success);

  AddDataPackFromFileRegion(base::File(g_chrome_100_percent_fd),
                            g_chrome_100_percent_region, k100Percent);

  if constexpr (BUILDFLAG(ENABLE_HIDPI)) {
    disk_path = disk_path.DirName().AppendASCII("chrome_200_percent.pak");
    success = LoadFromApkOrFile("assets/chrome_200_percent.pak", &disk_path,
                                &g_chrome_200_percent_fd,
                                &g_chrome_200_percent_region);
    if (success) {
      AddDataPackFromFileRegion(base::File(g_chrome_200_percent_fd),
                                g_chrome_200_percent_region, k200Percent);
    }
  }
}

// static
bool ResourceBundle::LocaleDataPakExists(std::string_view locale,
                                         Gender gender) {
  const bool in_split = !g_locale_paks_in_apk;
  const bool exists = ::ui::LocaleDataPakExists(locale, gender, in_split,
                                                /*log_error=*/false);
  if (exists || !in_split) {
    return exists;
  }

  // Fall back to checking on disk, which is necessary only for tests.
  const auto path = GetLocaleFilePath(locale);
  return !path.empty() && base::PathExists(path);
}

std::string ResourceBundle::LoadLocaleResources(const std::string& pref_locale,
                                                bool crash_on_failure) {
  DCHECK_EQ(locale_resources_data_.size(), 0u) << "locale.pak already loaded";
  std::string app_locale = l10n_util::GetApplicationLocale(pref_locale);

  // TODO(crbug.com/420947195): Fetch user's gender (behind a flag).
  const Gender gender = Gender::kDefault;

  FdAndRegion webview_locale_pack;
  FdAndRegion non_webview_locale_pack;
  non_webview_locale_pack.fd = -1;

  if (!LoadLocaleResourcesForLocaleAndGender(
          app_locale, gender, &webview_locale_pack, &non_webview_locale_pack,
          &locale_resources_data_)) {
    return std::string();
  }

  std::vector<ResourceBundle::FdAndRegion>& locale_packs =
      GetLocalePaksGlobal();
  CHECK_EQ(locale_packs.size(), 0u);

  webview_locale_pack.purpose = LocalePakPurpose::kWebViewMain;
  locale_packs.push_back(webview_locale_pack);

  if (non_webview_locale_pack.fd >= 0) {
    non_webview_locale_pack.purpose = LocalePakPurpose::kNonWebViewMain;
    locale_packs.push_back(non_webview_locale_pack);
  }

  if (gender != Gender::kDefault) {
    non_webview_locale_pack.fd = -1;

    if (!LoadLocaleResourcesForLocaleAndGender(
            app_locale, Gender::kDefault, &webview_locale_pack,
            &non_webview_locale_pack, &locale_resources_data_)) {
      return std::string();
    }

    webview_locale_pack.purpose = LocalePakPurpose::kWebViewFallback;
    locale_packs.push_back(webview_locale_pack);

    if (non_webview_locale_pack.fd >= 0) {
      non_webview_locale_pack.purpose = LocalePakPurpose::kNonWebViewFallback;
      locale_packs.push_back(non_webview_locale_pack);
    }
  }

  return app_locale;
}

gfx::Image& ResourceBundle::GetNativeImageNamed(int resource_id) {
  return GetImageNamed(resource_id);
}

void SetLocalePaksStoredInApk(bool value) {
  g_locale_paks_in_apk = value;
}

void DetectAndSetLoadNonWebViewLocalePaks() {
  // Auto-detect based on en-US whether non-webview locale .pak files exist.
  g_load_non_webview_locale_paks =
      LocaleDataPakExists("en-US", ResourceBundle::Gender::kDefault,
                          false /* in_split */, false /* log_error */);
}

void LoadMainAndroidPackFile(const char* path_within_apk,
                             const base::FilePath& disk_file_path) {
  if (LoadFromApkOrFile(path_within_apk,
                        &disk_file_path,
                        &g_resources_pack_fd,
                        &g_resources_pack_region)) {
    ResourceBundle::GetSharedInstance().AddDataPackFromFileRegion(
        base::File(g_resources_pack_fd), g_resources_pack_region,
        kScaleFactorNone);
  }
}

void LoadPackFileFromApk(const std::string& path,
                         const std::string& split_name) {
  base::MemoryMappedFile::Region region;
  FileDescriptor fd = base::android::OpenApkAsset(path, split_name, &region);
  CHECK_GE(fd, 0) << "Could not find " << path << " in APK.";
  ui::ResourceBundle::GetSharedInstance().AddDataPackFromFileRegion(
      base::File(fd), region, ui::kScaleFactorNone);
}

FileDescriptor GetMainAndroidPackFd(
    base::MemoryMappedFile::Region* out_region) {
  DCHECK_GE(g_resources_pack_fd, 0);
  *out_region = g_resources_pack_region;
  return g_resources_pack_fd;
}

FileDescriptor GetCommonResourcesPackFd(
    base::MemoryMappedFile::Region* out_region) {
  DCHECK_GE(g_chrome_100_percent_fd, 0);
  *out_region = g_chrome_100_percent_region;
  return g_chrome_100_percent_fd;
}

FileDescriptor Get200PercentResourcesPackFd(
    base::MemoryMappedFile::Region* out_region) {
  DCHECK_GE(g_chrome_200_percent_fd, 0);
  *out_region = g_chrome_200_percent_region;
  return g_chrome_200_percent_fd;
}

const std::vector<ResourceBundle::FdAndRegion>& GetLocalePaks() {
  const std::vector<ResourceBundle::FdAndRegion>& locale_packs =
      GetLocalePaksGlobal();
  CHECK_GT(locale_packs.size(), 0u);
  return locale_packs;
}

void SetNoAvailableLocalePaksForTest() {
  Java_ResourceBundle_setNoAvailableLocalePaks(
      base::android::AttachCurrentThread());
}

void UnloadAndroidLocaleResources() {
  GetLocalePaksGlobal().clear();
}

std::vector<ResourceBundle::FdAndRegion> SwapAndroidGlobalsForTesting(
    const std::vector<ResourceBundle::FdAndRegion>& new_locale_packs) {
  return std::exchange(GetLocalePaksGlobal(), new_locale_packs);
}

}  // namespace ui

DEFINE_JNI(ResourceBundle)