chromium/chrome/browser/ash/app_list/arc/arc_default_app_list.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/app_list/arc/arc_default_app_list.h"

#include <string.h>

#include <utility>
#include <vector>

#include "ash/components/arc/arc_util.h"
#include "base/barrier_closure.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_file_value_serializer.h"
#include "base/path_service.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/task/task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/app_list/arc/arc_app_scoped_pref_update.h"
#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_paths.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_registry.h"

namespace {

constexpr char kActivity[] = "activity";
constexpr char kAppPath[] = "app_path";
constexpr char kName[] = "name";
constexpr char kOem[] = "oem";
constexpr char kPackageName[] = "package_name";

constexpr char kDefaultApps[] = "arc.apps.default";
constexpr char kHidden[] = "hidden";

// Sub-directory wher ARC apps forward declarations are stored.
const base::FilePath::CharType kArcDirectory[] = FILE_PATH_LITERAL("arc");
const base::FilePath::CharType kArcTestDirectory[] =
    FILE_PATH_LITERAL("arc_default_apps");
const base::FilePath::CharType kArcTestBoardDirectory[] =
    FILE_PATH_LITERAL("arc_board_default_apps");
const base::FilePath::CharType kArcTestNonAdaptiveDirectory[] =
    FILE_PATH_LITERAL("arc_non_adaptive_default_apps");

bool use_test_apps_directory = false;

std::unique_ptr<ArcDefaultAppList::AppInfoMap> ReadAppsFromFileThread(
    const base::FilePath& base_path) {
  // FileEnumerator does not work with a symbolic link dir. So map link
  // to real folder in case |base_path| specifies a symbolic link.
  std::optional<base::FilePath> link_target =
      base::ReadSymbolicLinkAbsolute(base_path);
  base::FilePath root_dir = link_target.value_or(base_path);

  std::unique_ptr<ArcDefaultAppList::AppInfoMap> apps =
      std::make_unique<ArcDefaultAppList::AppInfoMap>();

  base::FilePath::StringType extension(".json");
  base::FileEnumerator json_files(root_dir,
                                  false,  // Recursive.
                                  base::FileEnumerator::FILES);

  for (base::FilePath file = json_files.Next(); !file.empty();
      file = json_files.Next()) {
    if (!file.MatchesExtension(extension)) {
      DVLOG(1) << "Not considering: " << file.LossyDisplayName()
               << " (does not have a .json extension)";
      continue;
    }

    JSONFileValueDeserializer deserializer(file);
    std::string error_msg;
    auto app_info_ptr = deserializer.Deserialize(nullptr, &error_msg);
    if (!app_info_ptr) {
      VLOG(2) << "Unable to deserialize json data: " << error_msg << " in file "
              << file.value() << ".";
      continue;
    }
    base::Value::Dict app_info = std::move(app_info_ptr->GetDict());

    auto* name = app_info.FindString(kName);
    auto* package_name = app_info.FindString(kPackageName);
    auto* activity = app_info.FindString(kActivity);
    auto* app_path = app_info.FindString(kAppPath);
    bool oem = app_info.FindBool(kOem).value_or(false);

    if (!name || !package_name || !activity || !app_path || name->empty() ||
        package_name->empty() || activity->empty() || app_path->empty()) {
      VLOG(2) << "ARC app declaration is incomplete in file " << file.value()
              << ".";
      continue;
    }

    const std::string app_id =
        ArcAppListPrefs::GetAppId(*package_name, *activity);
    std::unique_ptr<ArcDefaultAppList::AppInfo> app =
        std::make_unique<ArcDefaultAppList::AppInfo>(
            *name, *package_name, *activity, oem, root_dir.Append(*app_path));
    apps.get()->insert(
        std::pair<std::string, std::unique_ptr<ArcDefaultAppList::AppInfo>>(
            app_id, std::move(app)));
  }

  return apps;
}

// Returns true if default app |app_id| is marked as hidden in the prefs.
bool IsAppHidden(const PrefService* prefs, const std::string& app_id) {
  const base::Value::Dict& apps_dict = prefs->GetDict(kDefaultApps);

  const base::Value::Dict* app_dict = apps_dict.FindDict(app_id);
  if (!app_dict)
    return false;
  return app_dict->FindBool(kHidden).value_or(false);
}

std::string GetBoardName(const base::FilePath& build_prop_path) {
  constexpr char kKeyToFind[] = "ro.product.board=";

  std::string content;
  if (!base::ReadFileToString(build_prop_path, &content)) {
    PLOG(ERROR) << "Failed to read " << build_prop_path;
    return std::string();
  }

  const std::vector<std::string> lines = base::SplitString(
      content, "\n", base::WhitespaceHandling::KEEP_WHITESPACE,
      base::SplitResult::SPLIT_WANT_ALL);
  for (const auto& line : lines) {
    if (!base::StartsWith(line, kKeyToFind, base::CompareCase::SENSITIVE))
      continue;
    const std::string board = line.substr(strlen(kKeyToFind));
    VLOG(2) << "Current board is " << board;
    return board;
  }

  LOG(ERROR) << "Failed to find " << kKeyToFind << " in " << build_prop_path;
  return std::string();
}

}  // namespace

// static
void ArcDefaultAppList::UseTestAppsDirectory() {
  use_test_apps_directory = true;
}

// static
void ArcDefaultAppList::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterDictionaryPref(kDefaultApps);
}

// static
std::string ArcDefaultAppList::GetBoardNameForTesting(
    const base::FilePath& build_prop_path) {
  return GetBoardName(build_prop_path);
}

ArcDefaultAppList::ArcDefaultAppList(Profile* profile,
                                     base::OnceClosure ready_callback)
    : profile_(profile), ready_callback_(std::move(ready_callback)) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  arc::ArcSessionManager::Get()->AddObserver(this);
}

ArcDefaultAppList::~ArcDefaultAppList() {
  auto* manager = arc::ArcSessionManager::Get();
  if (manager)  // for unit testing
    manager->RemoveObserver(this);
}

void ArcDefaultAppList::OnPropertyFilesExpanded(bool result) {
  if (!result) {
    // Failed to generate |kGeneratedPropertyFilesPath[Vm]| for whatever reason.
    // Continue anyway not to stall the launcher initialization. In this case,
    // ARC[VM] itself won't start, so not being able to get the board name won't
    // be a huge problem either.
    VLOG(1) << "Unable to get the board name.";
    LoadDefaultApps(std::string());
    return;
  }

  VLOG(1) << "Getting the board name";
  const char* source_file = arc::IsArcVmEnabled()
                                ? arc::kGeneratedCombinedPropertyFilePathVm
                                : arc::kGeneratedBuildPropertyFilePath;
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&GetBoardName, base::FilePath(source_file)),
      base::BindOnce(&ArcDefaultAppList::LoadDefaultApps,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ArcDefaultAppList::LoadDefaultApps(std::string board_name) {
  VLOG(1) << "Start loading default apps. Board name is "
          << (board_name.empty() ? "<unknown>" : board_name);
  std::vector<base::FilePath> sources;

  base::FilePath base_path;
  if (!use_test_apps_directory) {
    const bool valid_path = base::PathService::Get(
        chrome::DIR_STANDALONE_EXTERNAL_EXTENSIONS, &base_path);
    DCHECK(valid_path);
    const base::FilePath base_arc_path = base_path.Append(kArcDirectory);
    sources.push_back(base_arc_path);
    if (!board_name.empty())
      sources.push_back(base_arc_path.Append(board_name));
  } else {
    const bool valid_path =
        base::PathService::Get(chrome::DIR_TEST_DATA, &base_path);
    DCHECK(valid_path);
    sources.push_back(base_path.Append(kArcTestDirectory));
    sources.push_back(base_path.Append(kArcTestBoardDirectory));
    sources.push_back(base_path.Append(kArcTestNonAdaptiveDirectory));
  }

  // Using base::Unretained(this) here is safe since we own barrier_closure_.
  barrier_closure_ = base::BarrierClosure(
      sources.size(),
      base::BindOnce(&ArcDefaultAppList::OnAppsReady, base::Unretained(this)));

  // Once ready OnAppsReady is called.
  for (const auto& source : sources) {
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
        base::BindOnce(&ReadAppsFromFileThread, source),
        base::BindOnce(&ArcDefaultAppList::OnAppsRead,
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

void ArcDefaultAppList::OnAppsRead(std::unique_ptr<AppInfoMap> apps) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

  const PrefService* const prefs = profile_->GetPrefs();
  for (auto& entry : *apps.get()) {
    AppInfoMap& app_map =
        IsAppHidden(prefs, entry.first) ? hidden_apps_ : visible_apps_;
    app_map[entry.first] = std::move(entry.second);
  }

  barrier_closure_.Run();
}

void ArcDefaultAppList::OnAppsReady() {
  const PrefService* const prefs = profile_->GetPrefs();
  // Register Play Store as default app. Some services and ArcSupportHost may
  // not be available in tests.
  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(profile_);
  const extensions::Extension* arc_host =
      registry->GetInstalledExtension(arc::kPlayStoreAppId);
  if (arc_host && arc::IsPlayStoreAvailable()) {
    std::unique_ptr<ArcDefaultAppList::AppInfo> play_store_app =
        std::make_unique<ArcDefaultAppList::AppInfo>(arc_host->name(),
                                       arc::kPlayStorePackage,
                                       arc::kPlayStoreActivity,
                                       false /* oem */,
                                       base::FilePath() /* app_path */);
    AppInfoMap& app_map =
        IsAppHidden(prefs, arc::kPlayStoreAppId) ? hidden_apps_ : visible_apps_;
    app_map.insert(
        std::pair<std::string, std::unique_ptr<ArcDefaultAppList::AppInfo>>(
            arc::kPlayStoreAppId, std::move(play_store_app)));
  }

  std::move(ready_callback_).Run();
}

const ArcDefaultAppList::AppInfo* ArcDefaultAppList::GetApp(
    const std::string& app_id) const {
  if ((filter_level_ == FilterLevel::ALL) ||
      (filter_level_ == FilterLevel::OPTIONAL_APPS &&
       app_id != arc::kPlayStoreAppId)) {
    return nullptr;
  }
  const auto it = visible_apps_.find(app_id);
  if (it == visible_apps_.end())
    return nullptr;
  return it->second.get();
}

bool ArcDefaultAppList::HasApp(const std::string& app_id) const {
  return GetApp(app_id) != nullptr;
}

bool ArcDefaultAppList::HasPackage(const std::string& package_name) const {
  for (const auto& it : visible_apps_) {
    if (it.second->package_name == package_name)
      return true;
  }
  return false;
}

bool ArcDefaultAppList::HasHiddenPackage(
    const std::string& package_name) const {
  for (const auto& it : hidden_apps_) {
    if (it.second->package_name == package_name)
      return true;
  }
  return false;
}

void ArcDefaultAppList::SetAppHidden(const std::string& app_id, bool hidden) {
  AppInfoMap& active_map = hidden ? visible_apps_ : hidden_apps_;
  AppInfoMap& inactive_map = hidden ? hidden_apps_ : visible_apps_;

  auto it = active_map.find(app_id);
  if (it == active_map.end())
    return;
  inactive_map[app_id] = std::move(it->second);
  active_map.erase(it);

  // Store hidden flag.
  arc::ArcAppScopedPrefUpdate(profile_->GetPrefs(), app_id, kDefaultApps)
      ->Set(kHidden, hidden);
}

std::map<std::string, const ArcDefaultAppList::AppInfo*>
ArcDefaultAppList::GetActiveApps() const {
  std::map<std::string, const AppInfo*> result;
  for (const auto& it : visible_apps_) {
    if (HasApp(it.first))
      result[it.first] = it.second.get();
  }
  return result;
}

std::unordered_set<std::string> ArcDefaultAppList::GetActivePackages() const {
  std::unordered_set<std::string> result;
  for (const auto& it : GetActiveApps())
    result.insert(it.second->package_name);
  return result;
}

ArcDefaultAppList::AppInfo::AppInfo(const std::string& name,
                                    const std::string& package_name,
                                    const std::string& activity,
                                    bool oem,
                                    const base::FilePath app_path)
    : name(name),
      package_name(package_name),
      activity(activity),
      oem(oem),
      app_path(app_path) {}

ArcDefaultAppList::AppInfo::~AppInfo() {}