Commit aecde1de authored by Nicholas Hollingum's avatar Nicholas Hollingum Committed by Commit Bot

folder persistence on crostini side

Adds the relevant pieces to crostini that allow it to declare that one
of ash's folders should be persistent (i.e. alive with only 1 item in
it). We do this by adding an observer which updates the folder's
metadata with the persistence property.

This CL also includes some refactoring of the unit tests here so that
they better reflect how chrome and ash communicate, and so the
expectations are a bit nicer to work with.

Bug: 925052
Change-Id: Ie0c211cf9783fa2318d1e35d1b25827c0fdc048f
Reviewed-on: https://chromium-review.googlesource.com/c/1469194
Commit-Queue: Nic Hollingum <hollingum@google.com>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Reviewed-by: default avatarTimothy Loh <timloh@chromium.org>
Cr-Commit-Position: refs/heads/master@{#634498}
parent 50cea97f
......@@ -16,11 +16,45 @@
#include "components/prefs/pref_change_registrar.h"
#include "ui/base/l10n/l10n_util.h"
// Folder items are created by the Ash process and their existence is
// communicated to chrome via the AppListClient. Therefore, crostini has an
// observer that listens for the creation of its folder, and updates the
// properties accordingly.
class CrostiniAppModelBuilder::CrostiniFolderObserver
: public AppListModelUpdaterObserver {
public:
explicit CrostiniFolderObserver(CrostiniAppModelBuilder* parent)
: parent_(parent) {}
~CrostiniFolderObserver() override = default;
void OnAppListItemAdded(ChromeAppListItem* item) override {
if (item->id() != crostini::kCrostiniFolderId)
return;
// Persistence is not recorded by the sync, so we always set it.
item->SetIsPersistent(true);
// Either the name and position will be in the sync, or we set them
// manually.
if (parent_->GetSyncItem(crostini::kCrostiniFolderId))
return;
item->SetName(
l10n_util::GetStringUTF8(IDS_APP_LIST_CROSTINI_DEFAULT_FOLDER_NAME));
item->SetDefaultPositionIfApplicable(parent_->model_updater());
}
private:
CrostiniAppModelBuilder* parent_;
};
CrostiniAppModelBuilder::CrostiniAppModelBuilder(
AppListControllerDelegate* controller)
: AppListModelBuilder(controller, CrostiniAppItem::kItemType) {}
CrostiniAppModelBuilder::~CrostiniAppModelBuilder() {
if (crostini_folder_observer_) {
model_updater()->RemoveObserver(crostini_folder_observer_.get());
}
// We don't need to remove ourself from the registry's observer list as these
// are both KeyedServices (this class lives on AppListSyncableService).
}
......@@ -40,6 +74,13 @@ void CrostiniAppModelBuilder::BuildModel() {
crostini::prefs::kCrostiniEnabled,
base::BindRepeating(&CrostiniAppModelBuilder::OnCrostiniEnabledChanged,
base::Unretained(this)));
// We register an observer against the model_updater in order to track
// creation and deletion of the crostini folder.
if (model_updater()) {
crostini_folder_observer_ = std::make_unique<CrostiniFolderObserver>(this);
model_updater()->AddObserver(crostini_folder_observer_.get());
}
}
void CrostiniAppModelBuilder::InsertCrostiniAppItem(
......@@ -56,7 +97,6 @@ void CrostiniAppModelBuilder::InsertCrostiniAppItem(
if (registration.NoDisplay())
return;
MaybeCreateRootFolder();
InsertApp(std::make_unique<CrostiniAppItem>(profile(), model_updater(),
GetSyncItem(app_id), app_id,
registration.Name()));
......@@ -120,21 +160,3 @@ void CrostiniAppModelBuilder::OnCrostiniEnabledChanged() {
RemoveApp(crostini::kCrostiniTerminalId, unsynced_change);
}
}
void CrostiniAppModelBuilder::MaybeCreateRootFolder() {
// If a sync item exists for the root folder, then it has been created
// already.
const app_list::AppListSyncableService::SyncItem* sync_item =
GetSyncItem(crostini::kCrostiniFolderId);
if (sync_item)
return;
std::unique_ptr<ChromeAppListItem> crositini_folder =
std::make_unique<ChromeAppListItem>(
profile(), crostini::kCrostiniFolderId, model_updater());
crositini_folder->SetChromeIsFolder(true);
crositini_folder->SetName(
l10n_util::GetStringUTF8(IDS_APP_LIST_CROSTINI_DEFAULT_FOLDER_NAME));
crositini_folder->SetDefaultPositionIfApplicable(model_updater());
InsertApp(std::move(crositini_folder));
}
......@@ -4,6 +4,8 @@
#ifndef CHROME_BROWSER_UI_APP_LIST_CROSTINI_CROSTINI_APP_MODEL_BUILDER_H_
#define CHROME_BROWSER_UI_APP_LIST_CROSTINI_CROSTINI_APP_MODEL_BUILDER_H_
#include <memory>
#include "base/macros.h"
#include "chrome/browser/chromeos/crostini/crostini_registry_service.h"
#include "chrome/browser/ui/app_list/app_list_model_builder.h"
......@@ -20,6 +22,10 @@ class CrostiniAppModelBuilder
~CrostiniAppModelBuilder() override;
private:
// This observer will be used to update the properties of the crostini folder
// when ash creates it.
class CrostiniFolderObserver;
// AppListModelBuilder:
void BuildModel() override;
......@@ -38,15 +44,13 @@ class CrostiniAppModelBuilder
void OnCrostiniEnabledChanged();
// Creates root folder for Crostini apps in case it was not created (in which
// case the sync item will not exist). Once it is created, a sync item is
// allocated, and it will be reused to restore the root folder on demand
// automatically.
void MaybeCreateRootFolder();
// Observer Crostini installation so we can start showing The Terminal app.
std::unique_ptr<PrefChangeRegistrar> pref_change_registrar_;
// Observer that listens for crostini folder creation and sets its properties
// accordingly.
std::unique_ptr<AppListModelUpdaterObserver> crostini_folder_observer_;
DISALLOW_COPY_AND_ASSIGN(CrostiniAppModelBuilder);
};
......
......@@ -4,6 +4,7 @@
#include "chrome/browser/ui/app_list/crostini/crostini_app_model_builder.h"
#include <utility>
#include <vector>
#include "chrome/browser/chromeos/crostini/crostini_registry_service.h"
......@@ -23,6 +24,8 @@
#include "ui/base/l10n/l10n_util.h"
using crostini::CrostiniTestHelper;
using ::testing::_;
using ::testing::Matcher;
namespace {
......@@ -34,51 +37,18 @@ constexpr char kAppNewName[] = "new name";
constexpr char kBananaAppId[] = "banana";
constexpr char kBananaAppName[] = "banana app name";
// Returns map of items, key-ed by id.
std::map<std::string, ChromeAppListItem*> GetAppListItems(
AppListModelUpdater* model_updater) {
std::map<std::string, ChromeAppListItem*> result;
for (size_t i = 0; i < model_updater->ItemCount(); ++i) {
ChromeAppListItem* item = model_updater->ItemAtForTest(i);
result[item->id()] = item;
}
return result;
}
std::vector<std::string> GetAppIds(AppListModelUpdater* model_updater) {
std::vector<std::string> result;
for (auto item : GetAppListItems(model_updater))
result.push_back(item.first);
return result;
}
// This also includes parent folder name, if applicable.
std::vector<std::string> GetAppNames(AppListModelUpdater* model_updater) {
std::vector<std::string> result;
std::map<std::string, ChromeAppListItem*> items =
GetAppListItems(model_updater);
for (auto item : items) {
const std::string folder_id = item.second->folder_id();
if (folder_id.empty()) {
result.push_back(item.second->name());
continue;
}
ChromeAppListItem* parent = items[folder_id];
DCHECK(parent && parent->is_folder());
result.push_back(parent->name() + "/" + item.second->name());
}
return result;
}
std::string GetFullName(const std::string& app_name) {
return std::string(kRootFolderName) + "/" + app_name;
// Convenience matcher for some important fields of the chrome app.
MATCHER_P3(IsChromeApp, id, name, folder_id, "") {
Matcher<std::string> id_m(id);
Matcher<std::string> name_m(name);
Matcher<std::string> folder_id_m(folder_id);
return id_m.Matches(arg->id()) && name_m.Matches(arg->name()) &&
folder_id_m.Matches(arg->folder_id());
}
std::vector<std::string> AppendRootFolderId(
const std::vector<std::string> ids) {
std::vector<std::string> result = ids;
result.emplace_back(crostini::kCrostiniFolderId);
return result;
// Matches a chrome app item if its persistence field is set to true.
MATCHER(IsPersistentApp, "") {
return arg->is_persistent();
}
// For testing purposes, we want to pretend there are only crostini apps on the
......@@ -107,11 +77,6 @@ class CrostiniAppModelBuilderTest : public AppListTestBase {
void SetUp() override {
AppListTestBase::SetUp();
test_helper_ = std::make_unique<CrostiniTestHelper>(profile());
model_updater_factory_scope_ = std::make_unique<
app_list::AppListSyncableService::ScopedModelUpdaterFactoryForTest>(
base::BindRepeating([]() -> std::unique_ptr<AppListModelUpdater> {
return std::make_unique<FakeAppListModelUpdater>();
}));
CreateBuilder();
}
......@@ -130,18 +95,34 @@ class CrostiniAppModelBuilderTest : public AppListTestBase {
return sync_service_->GetModelUpdater()->ItemCount();
}
std::vector<ChromeAppListItem*> GetAllApps() const {
std::vector<ChromeAppListItem*> result;
for (size_t i = 0; i < GetModelItemCount(); ++i) {
result.emplace_back(GetModelUpdater()->ItemAtForTest(i));
}
return result;
}
void CreateBuilder() {
model_updater_factory_scope_ = std::make_unique<
app_list::AppListSyncableService::ScopedModelUpdaterFactoryForTest>(
base::BindRepeating(
[](Profile* profile) -> std::unique_ptr<AppListModelUpdater> {
return std::make_unique<FakeAppListModelUpdater>(profile);
},
profile()));
controller_ = std::make_unique<test::TestAppListControllerDelegate>();
builder_ = std::make_unique<CrostiniAppModelBuilder>(controller_.get());
sync_service_ = std::make_unique<app_list::AppListSyncableService>(
profile_.get(), extensions::ExtensionSystem::Get(profile_.get()));
builder_ = std::make_unique<CrostiniAppModelBuilder>(controller_.get());
RemoveNonCrostiniApps(sync_service_.get());
}
void ResetBuilder() {
builder_.reset();
controller_.reset();
sync_service_.reset();
controller_.reset();
model_updater_factory_scope_.reset();
}
crostini::CrostiniRegistryService* RegistryService() {
......@@ -177,129 +158,129 @@ TEST_F(CrostiniAppModelBuilderTest, EnableAndDisableCrostini) {
EXPECT_EQ(0u, GetModelItemCount());
CrostiniTestHelper::EnableCrostini(profile());
// Root folder + terminal app.
EXPECT_THAT(GetAppIds(GetModelUpdater()),
testing::UnorderedElementsAre(crostini::kCrostiniFolderId,
crostini::kCrostiniTerminalId));
EXPECT_THAT(GetAppNames(GetModelUpdater()),
testing::UnorderedElementsAre(kRootFolderName,
GetFullName(TerminalAppName())));
EXPECT_THAT(GetAllApps(),
testing::UnorderedElementsAre(
IsChromeApp(crostini::kCrostiniTerminalId, TerminalAppName(),
crostini::kCrostiniFolderId)));
CrostiniTestHelper::DisableCrostini(profile());
EXPECT_THAT(GetAppIds(GetModelUpdater()),
testing::ElementsAre(crostini::kCrostiniFolderId));
EXPECT_THAT(GetAllApps(), testing::IsEmpty());
}
TEST_F(CrostiniAppModelBuilderTest, AppInstallation) {
// Root folder + terminal app.
EXPECT_EQ(2u, GetModelItemCount());
// Terminal app.
EXPECT_EQ(1u, GetModelItemCount());
test_helper_->SetupDummyApps();
EXPECT_THAT(GetAppIds(GetModelUpdater()),
testing::UnorderedElementsAreArray(AppendRootFolderId(
RegistryService()->GetRegisteredAppIds())));
EXPECT_THAT(GetAppNames(GetModelUpdater()),
EXPECT_THAT(
GetAllApps(),
testing::UnorderedElementsAre(
kRootFolderName, GetFullName(TerminalAppName()),
GetFullName(kDummpyApp1Name), GetFullName(kDummpyApp2Name)));
IsChromeApp(crostini::kCrostiniTerminalId, TerminalAppName(),
crostini::kCrostiniFolderId),
IsChromeApp(_, kDummpyApp1Name, crostini::kCrostiniFolderId),
IsChromeApp(_, kDummpyApp2Name, crostini::kCrostiniFolderId)));
test_helper_->AddApp(
CrostiniTestHelper::BasicApp(kBananaAppId, kBananaAppName));
EXPECT_THAT(GetAppIds(GetModelUpdater()),
testing::UnorderedElementsAreArray(AppendRootFolderId(
RegistryService()->GetRegisteredAppIds())));
EXPECT_THAT(GetAppNames(GetModelUpdater()),
EXPECT_THAT(GetAllApps(),
testing::UnorderedElementsAre(
kRootFolderName, GetFullName(TerminalAppName()),
GetFullName(kDummpyApp1Name), GetFullName(kDummpyApp2Name),
GetFullName(kBananaAppName)));
IsChromeApp(crostini::kCrostiniTerminalId, TerminalAppName(),
crostini::kCrostiniFolderId),
IsChromeApp(_, kDummpyApp1Name, crostini::kCrostiniFolderId),
IsChromeApp(_, kDummpyApp2Name, crostini::kCrostiniFolderId),
IsChromeApp(_, kBananaAppName, crostini::kCrostiniFolderId)));
}
// Test that the app model builder correctly picks up changes to existing apps.
TEST_F(CrostiniAppModelBuilderTest, UpdateApps) {
test_helper_->SetupDummyApps();
// 3 apps + root folder.
EXPECT_EQ(4u, GetModelItemCount());
// 3 apps.
EXPECT_EQ(3u, GetModelItemCount());
// Setting NoDisplay to true should hide an app.
vm_tools::apps::App dummy1 = test_helper_->GetApp(0);
dummy1.set_no_display(true);
test_helper_->AddApp(dummy1);
EXPECT_EQ(3u, GetModelItemCount());
EXPECT_THAT(GetAppIds(GetModelUpdater()),
EXPECT_THAT(
GetAllApps(),
testing::UnorderedElementsAre(
crostini::kCrostiniFolderId, crostini::kCrostiniTerminalId,
CrostiniTestHelper::GenerateAppId(kDummpyApp2Id)));
IsChromeApp(crostini::kCrostiniTerminalId, _, _),
IsChromeApp(CrostiniTestHelper::GenerateAppId(kDummpyApp2Name), _,
_)));
// Setting NoDisplay to false should unhide an app.
dummy1.set_no_display(false);
test_helper_->AddApp(dummy1);
EXPECT_EQ(4u, GetModelItemCount());
EXPECT_THAT(GetAppIds(GetModelUpdater()),
testing::UnorderedElementsAreArray(AppendRootFolderId(
RegistryService()->GetRegisteredAppIds())));
EXPECT_THAT(
GetAllApps(),
testing::UnorderedElementsAre(
IsChromeApp(crostini::kCrostiniTerminalId, _, _),
IsChromeApp(CrostiniTestHelper::GenerateAppId(kDummpyApp1Name), _, _),
IsChromeApp(CrostiniTestHelper::GenerateAppId(kDummpyApp2Name), _,
_)));
// Changes to app names should be detected.
vm_tools::apps::App dummy2 =
CrostiniTestHelper::BasicApp(kDummpyApp2Id, kAppNewName);
test_helper_->AddApp(dummy2);
EXPECT_EQ(4u, GetModelItemCount());
EXPECT_THAT(GetAppIds(GetModelUpdater()),
testing::UnorderedElementsAreArray(AppendRootFolderId(
RegistryService()->GetRegisteredAppIds())));
EXPECT_THAT(GetAppNames(GetModelUpdater()),
EXPECT_THAT(
GetAllApps(),
testing::UnorderedElementsAre(
kRootFolderName, GetFullName(TerminalAppName()),
GetFullName(kDummpyApp1Name), GetFullName(kAppNewName)));
IsChromeApp(crostini::kCrostiniTerminalId, _, _),
IsChromeApp(CrostiniTestHelper::GenerateAppId(kDummpyApp1Name),
kDummpyApp1Name, _),
IsChromeApp(CrostiniTestHelper::GenerateAppId(kDummpyApp2Name),
kAppNewName, _)));
}
// Test that the app model builder handles removed apps
TEST_F(CrostiniAppModelBuilderTest, RemoveApps) {
test_helper_->SetupDummyApps();
// 3 apps + root folder.
EXPECT_EQ(4u, GetModelItemCount());
// 3 apps.
EXPECT_EQ(3u, GetModelItemCount());
// Remove dummy1
test_helper_->RemoveApp(0);
EXPECT_EQ(3u, GetModelItemCount());
EXPECT_EQ(2u, GetModelItemCount());
// Remove dummy2
test_helper_->RemoveApp(0);
EXPECT_EQ(2u, GetModelItemCount());
EXPECT_EQ(1u, GetModelItemCount());
}
// Tests that the crostini folder is recreated on demand.
TEST_F(CrostiniAppModelBuilderTest, RecreateFolder) {
CrostiniTestHelper::EnableCrostini(profile());
// Root folder + terminal app.
EXPECT_THAT(GetAppNames(GetModelUpdater()),
testing::UnorderedElementsAre(kRootFolderName,
GetFullName(TerminalAppName())));
// Move the terminal out and delete the old folder.
GetModelUpdater()->MoveItemToFolder(crostini::kCrostiniTerminalId, "");
GetModelUpdater()->RemoveItem(crostini::kCrostiniFolderId);
EXPECT_THAT(GetAppNames(GetModelUpdater()),
testing::UnorderedElementsAre(TerminalAppName()));
// Adding a new app should recreate the folder.
test_helper_->AddApp(
CrostiniTestHelper::BasicApp(kDummpyApp2Id, kDummpyApp2Name));
EXPECT_THAT(GetAppNames(GetModelUpdater()),
testing::UnorderedElementsAre(TerminalAppName(), kRootFolderName,
GetFullName(kDummpyApp2Name)));
// Tests that the crostini folder is (re)created with the correct parameters.
TEST_F(CrostiniAppModelBuilderTest, CreatesFolder) {
EXPECT_THAT(GetAllApps(),
testing::UnorderedElementsAre(
IsChromeApp(crostini::kCrostiniTerminalId, TerminalAppName(),
crostini::kCrostiniFolderId)));
// We simulate ash creating the crostini folder and calling back into chrome
// (rather than use a full browser test).
ash::mojom::AppListItemMetadataPtr metadata =
ash::mojom::AppListItemMetadata::New();
metadata->id = crostini::kCrostiniFolderId;
GetModelUpdater()->OnFolderCreated(std::move(metadata));
EXPECT_THAT(GetAllApps(),
testing::UnorderedElementsAre(
IsChromeApp(crostini::kCrostiniTerminalId, TerminalAppName(),
crostini::kCrostiniFolderId),
testing::AllOf(IsChromeApp(crostini::kCrostiniFolderId,
kRootFolderName, ""),
IsPersistentApp())));
}
// Test that the Terminal app is removed when Crostini is disabled.
TEST_F(CrostiniAppModelBuilderTest, DisableCrostini) {
test_helper_->SetupDummyApps();
// 3 apps + root folder.
EXPECT_EQ(4u, GetModelItemCount());
// 3 apps.
EXPECT_EQ(3u, GetModelItemCount());
// The uninstall flow removes all apps before setting the CrostiniEnabled pref
// to false, so we need to do that explicitly too.
RegistryService()->ClearApplicationList(crostini::kCrostiniDefaultVmName, "");
CrostiniTestHelper::DisableCrostini(profile());
// Root folder is left. We rely on default handling of empty folder.
EXPECT_EQ(1u, GetModelItemCount());
EXPECT_EQ(0u, GetModelItemCount());
}
......@@ -10,7 +10,8 @@
#include "chrome/browser/ui/app_list/chrome_app_list_item.h"
#include "extensions/common/constants.h"
FakeAppListModelUpdater::FakeAppListModelUpdater() = default;
FakeAppListModelUpdater::FakeAppListModelUpdater(Profile* profile)
: profile_(profile) {}
FakeAppListModelUpdater::~FakeAppListModelUpdater() = default;
......@@ -214,6 +215,17 @@ void FakeAppListModelUpdater::UpdateAppItemFromSyncItem(
}
}
void FakeAppListModelUpdater::OnFolderCreated(
ash::mojom::AppListItemMetadataPtr folder) {
std::unique_ptr<ChromeAppListItem> stub_folder =
std::make_unique<ChromeAppListItem>(profile_, folder->id, this);
for (AppListModelUpdaterObserver& observer : observers_)
observer.OnAppListItemAdded(stub_folder.get());
AddItem(std::move(stub_folder));
}
void FakeAppListModelUpdater::AddObserver(
AppListModelUpdaterObserver* observer) {
observers_.AddObserver(observer);
......
......@@ -18,7 +18,7 @@ class ChromeAppListItem;
class FakeAppListModelUpdater : public AppListModelUpdater {
public:
FakeAppListModelUpdater();
explicit FakeAppListModelUpdater(Profile* profile = nullptr);
~FakeAppListModelUpdater() override;
// For AppListModel:
......@@ -62,7 +62,7 @@ class FakeAppListModelUpdater : public AppListModelUpdater {
return search_results_;
}
void OnFolderCreated(ash::mojom::AppListItemMetadataPtr folder) override {}
void OnFolderCreated(ash::mojom::AppListItemMetadataPtr folder) override;
void OnFolderDeleted(ash::mojom::AppListItemMetadataPtr item) override {}
void OnItemUpdated(ash::mojom::AppListItemMetadataPtr item) override {}
void OnPageBreakItemAdded(const std::string& id,
......@@ -77,6 +77,7 @@ class FakeAppListModelUpdater : public AppListModelUpdater {
std::vector<std::unique_ptr<ChromeAppListItem>> items_;
std::vector<ChromeSearchResult*> search_results_;
base::ObserverList<AppListModelUpdaterObserver> observers_;
Profile* profile_;
ash::mojom::AppListItemMetadataPtr FindOrCreateOemFolder(
const std::string& oem_folder_name,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment