blob: f036efeb51d10736c175eabd4165d7aaa04f75ab [file] [log] [blame]
// Copyright 2016 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.
#include <string.h>
#include <memory>
#include <string>
#include <utility>
#include "base/files/file_path.h"
#include "base/run_loop.h"
#include "chrome/browser/chromeos/arc/fileapi/arc_documents_provider_root.h"
#include "chrome/browser/chromeos/arc/fileapi/arc_documents_provider_util.h"
#include "chrome/browser/chromeos/arc/fileapi/arc_file_system_operation_runner.h"
#include "chrome/test/base/testing_profile.h"
#include "components/arc/arc_bridge_service.h"
#include "components/arc/arc_service_manager.h"
#include "components/arc/common/file_system.mojom.h"
#include "components/arc/test/connection_holder_util.h"
#include "components/arc/test/fake_file_system_instance.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "storage/browser/fileapi/watcher_manager.h"
#include "storage/common/fileapi/directory_entry.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using ChangeType = arc::ArcDocumentsProviderRoot::ChangeType;
using storage::DirectoryEntry;
using Document = arc::FakeFileSystemInstance::Document;
using EntryList = storage::AsyncFileUtil::EntryList;
namespace arc {
namespace {
// Simliar as FakeFileSystemInstance::Document, but all fields are primitives
// so that values can be constexpr.
struct DocumentSpec {
const char* document_id;
const char* parent_document_id;
const char* display_name;
const char* mime_type;
int64_t size;
uint64_t last_modified;
};
// Fake file system hierarchy:
//
// <path> <type> <ID>
// (root)/ dir root-id
// dir/ dir dir-id
// photo.jpg image/jpeg photo-id
// music.bin audio/mp3 music-id
// dups/ dir dups-id
// dup.mp4 video/mp4 dup1-id
// dup.mp4 video/mp4 dup2-id
// dup.mp4 video/mp4 dup3-id
// dup.mp4 video/mp4 dup4-id
constexpr char kAuthority[] = "org.chromium.test";
constexpr DocumentSpec kRootSpec{"root-id", "", "", kAndroidDirectoryMimeType,
-1, 0};
constexpr DocumentSpec kDirSpec{
"dir-id", kRootSpec.document_id, "dir", kAndroidDirectoryMimeType, -1, 22};
constexpr DocumentSpec kPhotoSpec{
"photo-id", kDirSpec.document_id, "photo.jpg", "image/jpeg", 3, 33};
constexpr DocumentSpec kMusicSpec{
"music-id", kDirSpec.document_id, "music.bin", "audio/mp3", 4, 44};
constexpr DocumentSpec kDupsSpec{"dups-id", kRootSpec.document_id,
"dups", kAndroidDirectoryMimeType,
-1, 55};
constexpr DocumentSpec kDup1Spec{
"dup1-id", kDupsSpec.document_id, "dup.mp4", "video/mp4", 6, 66};
constexpr DocumentSpec kDup2Spec{
"dup2-id", kDupsSpec.document_id, "dup.mp4", "video/mp4", 7, 77};
constexpr DocumentSpec kDup3Spec{
"dup3-id", kDupsSpec.document_id, "dup.mp4", "video/mp4", 8, 88};
constexpr DocumentSpec kDup4Spec{
"dup4-id", kDupsSpec.document_id, "dup.mp4", "video/mp4", 9, 99};
// The order is intentionally shuffled here so that
// FileSystemInstance::GetChildDocuments() returns documents in shuffled order.
// See ResolveToContentUrlDups test below.
constexpr DocumentSpec kAllSpecs[] = {kRootSpec, kDirSpec, kPhotoSpec,
kMusicSpec, kDupsSpec, kDup2Spec,
kDup1Spec, kDup4Spec, kDup3Spec};
Document ToDocument(const DocumentSpec& spec) {
return Document(kAuthority, spec.document_id, spec.parent_document_id,
spec.display_name, spec.mime_type, spec.size,
spec.last_modified);
}
void ExpectMatchesSpec(const base::File::Info& info, const DocumentSpec& spec) {
EXPECT_EQ(spec.size, info.size);
if (spec.mime_type == kAndroidDirectoryMimeType) {
EXPECT_TRUE(info.is_directory);
} else {
EXPECT_FALSE(info.is_directory);
}
EXPECT_FALSE(info.is_symbolic_link);
EXPECT_EQ(spec.last_modified,
static_cast<uint64_t>(info.last_modified.ToJavaTime()));
EXPECT_EQ(spec.last_modified,
static_cast<uint64_t>(info.last_accessed.ToJavaTime()));
EXPECT_EQ(spec.last_modified,
static_cast<uint64_t>(info.creation_time.ToJavaTime()));
}
std::unique_ptr<KeyedService> CreateFileSystemOperationRunnerForTesting(
content::BrowserContext* context) {
return ArcFileSystemOperationRunner::CreateForTesting(
context, ArcServiceManager::Get()->arc_bridge_service());
}
class ArcDocumentsProviderRootTest : public testing::Test {
public:
ArcDocumentsProviderRootTest() = default;
~ArcDocumentsProviderRootTest() override = default;
void SetUp() override {
for (auto spec : kAllSpecs) {
fake_file_system_.AddDocument(ToDocument(spec));
}
arc_service_manager_ = std::make_unique<ArcServiceManager>();
profile_ = std::make_unique<TestingProfile>();
arc_service_manager_->set_browser_context(profile_.get());
ArcFileSystemOperationRunner::GetFactory()->SetTestingFactoryAndUse(
profile_.get(), &CreateFileSystemOperationRunnerForTesting);
arc_service_manager_->arc_bridge_service()->file_system()->SetInstance(
&fake_file_system_);
WaitForInstanceReady(
arc_service_manager_->arc_bridge_service()->file_system());
// Run the message loop until FileSystemInstance::Init() is called.
ASSERT_TRUE(fake_file_system_.InitCalled());
root_ = std::make_unique<ArcDocumentsProviderRoot>(
ArcFileSystemOperationRunner::GetForBrowserContext(profile_.get()),
kAuthority, kRootSpec.document_id);
}
void TearDown() override {
root_.reset();
arc_service_manager_->arc_bridge_service()->file_system()->CloseInstance(
&fake_file_system_);
// Run all pending tasks before destroying testing profile.
base::RunLoop().RunUntilIdle();
}
protected:
content::TestBrowserThreadBundle thread_bundle_;
FakeFileSystemInstance fake_file_system_;
// Use the same initialization/destruction order as
// ChromeBrowserMainPartsChromeos.
std::unique_ptr<ArcServiceManager> arc_service_manager_;
std::unique_ptr<TestingProfile> profile_;
std::unique_ptr<ArcDocumentsProviderRoot> root_;
private:
DISALLOW_COPY_AND_ASSIGN(ArcDocumentsProviderRootTest);
};
} // namespace
TEST_F(ArcDocumentsProviderRootTest, GetFileInfo) {
base::RunLoop run_loop;
root_->GetFileInfo(base::FilePath(FILE_PATH_LITERAL("dir/photo.jpg")),
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
ExpectMatchesSpec(info, kPhotoSpec);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, GetFileInfoDirectory) {
base::RunLoop run_loop;
root_->GetFileInfo(base::FilePath(FILE_PATH_LITERAL("dir")),
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
ExpectMatchesSpec(info, kDirSpec);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, GetFileInfoRoot) {
base::RunLoop run_loop;
root_->GetFileInfo(base::FilePath(FILE_PATH_LITERAL("")),
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
ExpectMatchesSpec(info, kRootSpec);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, GetFileInfoNoSuchFile) {
base::RunLoop run_loop;
root_->GetFileInfo(base::FilePath(FILE_PATH_LITERAL("dir/missing.jpg")),
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, error);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, GetFileInfoDups) {
base::RunLoop run_loop;
// "dup (2).mp4" should map to the 3rd instance of "dup.mp4" regardless of the
// order returned from FileSystemInstance.
root_->GetFileInfo(base::FilePath(FILE_PATH_LITERAL("dups/dup (2).mp4")),
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
ExpectMatchesSpec(info, kDup3Spec);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, GetFileInfoWithCache) {
{
base::RunLoop run_loop;
root_->GetFileInfo(
base::FilePath(FILE_PATH_LITERAL("dir/photo.jpg")),
base::Bind([](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) { run_loop->Quit(); },
&run_loop));
run_loop.Run();
}
int last_count = fake_file_system_.get_child_documents_count();
{
base::RunLoop run_loop;
root_->GetFileInfo(
base::FilePath(FILE_PATH_LITERAL("dir/photo.jpg")),
base::Bind([](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) { run_loop->Quit(); },
&run_loop));
run_loop.Run();
}
// GetFileInfo() against the same file shall not issue a new
// GetChildDocuments() call.
EXPECT_EQ(last_count, fake_file_system_.get_child_documents_count());
}
TEST_F(ArcDocumentsProviderRootTest, GetFileInfoWithCacheExpired) {
root_->SetDirectoryCacheExpireSoonForTesting();
{
base::RunLoop run_loop;
root_->GetFileInfo(
base::FilePath(FILE_PATH_LITERAL("dir/photo.jpg")),
base::Bind([](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) { run_loop->Quit(); },
&run_loop));
run_loop.Run();
}
int last_count = fake_file_system_.get_child_documents_count();
// Make sure directory caches expire.
base::RunLoop().RunUntilIdle();
{
base::RunLoop run_loop;
root_->GetFileInfo(
base::FilePath(FILE_PATH_LITERAL("dir/photo.jpg")),
base::Bind([](base::RunLoop* run_loop, base::File::Error error,
const base::File::Info& info) { run_loop->Quit(); },
&run_loop));
run_loop.Run();
}
// If cache expires, two GetChildDocuments() calls will be issued for
// "/" and "/dir".
EXPECT_EQ(last_count + 2, fake_file_system_.get_child_documents_count());
}
TEST_F(ArcDocumentsProviderRootTest, ReadDirectory) {
base::RunLoop run_loop;
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("dir")),
base::BindOnce(
[](base::RunLoop* run_loop, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
ASSERT_EQ(2u, file_list.size());
EXPECT_EQ(FILE_PATH_LITERAL("music.bin.mp3"), file_list[0].name);
EXPECT_EQ("music-id", file_list[0].document_id);
EXPECT_FALSE(file_list[0].is_directory);
EXPECT_EQ(base::Time::FromJavaTime(44), file_list[0].last_modified);
EXPECT_EQ(FILE_PATH_LITERAL("photo.jpg"), file_list[1].name);
EXPECT_EQ("photo-id", file_list[1].document_id);
EXPECT_FALSE(file_list[1].is_directory);
EXPECT_EQ(base::Time::FromJavaTime(33), file_list[1].last_modified);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, ReadDirectoryRoot) {
base::RunLoop run_loop;
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("")),
base::BindOnce(
[](base::RunLoop* run_loop, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
ASSERT_EQ(2u, file_list.size());
EXPECT_EQ(FILE_PATH_LITERAL("dir"), file_list[0].name);
EXPECT_EQ("dir-id", file_list[0].document_id);
EXPECT_TRUE(file_list[0].is_directory);
EXPECT_EQ(base::Time::FromJavaTime(22), file_list[0].last_modified);
EXPECT_EQ(FILE_PATH_LITERAL("dups"), file_list[1].name);
EXPECT_EQ("dups-id", file_list[1].document_id);
EXPECT_TRUE(file_list[1].is_directory);
EXPECT_EQ(base::Time::FromJavaTime(55), file_list[1].last_modified);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, ReadDirectoryNoSuchDirectory) {
base::RunLoop run_loop;
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("missing")),
base::BindOnce(
[](base::RunLoop* run_loop, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_ERROR_NOT_FOUND, error);
EXPECT_EQ(0u, file_list.size());
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, ReadDirectoryDups) {
base::RunLoop run_loop;
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("dups")),
base::BindOnce(
[](base::RunLoop* run_loop, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
ASSERT_EQ(4u, file_list.size());
// Files are sorted lexicographically.
EXPECT_EQ(FILE_PATH_LITERAL("dup (1).mp4"), file_list[0].name);
EXPECT_EQ("dup2-id", file_list[0].document_id);
EXPECT_FALSE(file_list[0].is_directory);
EXPECT_EQ(base::Time::FromJavaTime(77), file_list[0].last_modified);
EXPECT_EQ(FILE_PATH_LITERAL("dup (2).mp4"), file_list[1].name);
EXPECT_EQ("dup3-id", file_list[1].document_id);
EXPECT_FALSE(file_list[1].is_directory);
EXPECT_EQ(base::Time::FromJavaTime(88), file_list[1].last_modified);
EXPECT_EQ(FILE_PATH_LITERAL("dup (3).mp4"), file_list[2].name);
EXPECT_EQ("dup4-id", file_list[2].document_id);
EXPECT_FALSE(file_list[2].is_directory);
EXPECT_EQ(base::Time::FromJavaTime(99), file_list[2].last_modified);
EXPECT_EQ(FILE_PATH_LITERAL("dup.mp4"), file_list[3].name);
EXPECT_EQ("dup1-id", file_list[3].document_id);
EXPECT_FALSE(file_list[3].is_directory);
EXPECT_EQ(base::Time::FromJavaTime(66), file_list[3].last_modified);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, ReadDirectoryWithCache) {
{
base::RunLoop run_loop;
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("dir")),
base::BindOnce(
[](base::RunLoop* run_loop, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
run_loop->Quit();
},
&run_loop));
run_loop.Run();
}
int last_count = fake_file_system_.get_child_documents_count();
{
base::RunLoop run_loop;
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("dir")),
base::BindOnce(
[](base::RunLoop* run_loop, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
run_loop->Quit();
},
&run_loop));
run_loop.Run();
}
// ReadDirectory() against the same directory shall issue one new
// GetChildDocuments() call.
EXPECT_EQ(last_count + 1, fake_file_system_.get_child_documents_count());
}
TEST_F(ArcDocumentsProviderRootTest, ReadDirectoryWithCacheExpired) {
root_->SetDirectoryCacheExpireSoonForTesting();
{
base::RunLoop run_loop;
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("dir")),
base::BindOnce(
[](base::RunLoop* run_loop, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
run_loop->Quit();
},
&run_loop));
run_loop.Run();
}
int last_count = fake_file_system_.get_child_documents_count();
// Make sure directory caches expire.
base::RunLoop().RunUntilIdle();
{
base::RunLoop run_loop;
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("dir")),
base::BindOnce(
[](base::RunLoop* run_loop, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
run_loop->Quit();
},
&run_loop));
run_loop.Run();
}
// If cache expires, two GetChildDocuments() calls will be issued for
// "/" and "/dir".
EXPECT_EQ(last_count + 2, fake_file_system_.get_child_documents_count());
}
TEST_F(ArcDocumentsProviderRootTest, ReadDirectoryPendingCallbacks) {
int num_callbacks = 0;
int last_count = fake_file_system_.get_child_documents_count();
for (int i = 0; i < 3; ++i) {
root_->ReadDirectory(
base::FilePath(FILE_PATH_LITERAL("dir")),
base::BindOnce(
[](int* num_callbacks, base::File::Error error,
std::vector<ArcDocumentsProviderRoot::ThinFileInfo> file_list) {
++*num_callbacks;
},
&num_callbacks));
}
// FakeFileSystemInstance guarantees callbacks are not invoked immediately,
// so callbacks to ReadDirectory() have not been called at this point.
EXPECT_EQ(0, num_callbacks);
// GetChildDocuments() should have been called only once even though we called
// ReadDirectory() three times due to batching.
EXPECT_EQ(last_count + 1, fake_file_system_.get_child_documents_count());
// Process FakeFileSystemInstance callbacks.
base::RunLoop().RunUntilIdle();
// All callbacks should have been invoked.
EXPECT_EQ(3, num_callbacks);
}
TEST_F(ArcDocumentsProviderRootTest, WatchChanged) {
int num_called = 0;
auto watcher_callback = base::Bind(
[](int* num_called, ChangeType type) {
EXPECT_EQ(ChangeType::CHANGED, type);
++(*num_called);
},
&num_called);
{
base::RunLoop run_loop;
root_->AddWatcher(base::FilePath(FILE_PATH_LITERAL("dir")),
watcher_callback,
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
},
&run_loop));
run_loop.Run();
}
// Even if AddWatcher() returns, the watch may not have started. In order to
// make installation finish we run the message loop until idle. This depends
// on the behavior of FakeFileSystemInstance.
//
// TODO(crbug.com/698624): Remove the hack to make AddWatcher() return
// immediately.
base::RunLoop().RunUntilIdle();
EXPECT_EQ(0, num_called);
fake_file_system_.TriggerWatchers(kAuthority, kDirSpec.document_id,
storage::WatcherManager::CHANGED);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(1, num_called);
{
base::RunLoop run_loop;
root_->RemoveWatcher(
base::FilePath(FILE_PATH_LITERAL("dir")),
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
},
&run_loop));
run_loop.Run();
}
}
TEST_F(ArcDocumentsProviderRootTest, WatchDeleted) {
int num_called = 0;
auto watcher_callback = base::Bind(
[](int* num_called, ChangeType type) {
EXPECT_EQ(ChangeType::DELETED, type);
++(*num_called);
},
&num_called);
{
base::RunLoop run_loop;
root_->AddWatcher(base::FilePath(FILE_PATH_LITERAL("dir")),
watcher_callback,
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
},
&run_loop));
run_loop.Run();
}
// Even if AddWatcher() returns, the watch may not have started. In order to
// make installation finish we run the message loop until idle. This depends
// on the behavior of FakeFileSystemInstance.
//
// TODO(crbug.com/698624): Remove the hack to make AddWatcher() return
// immediately.
base::RunLoop().RunUntilIdle();
EXPECT_EQ(0, num_called);
fake_file_system_.TriggerWatchers(kAuthority, kDirSpec.document_id,
storage::WatcherManager::DELETED);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(1, num_called);
// Even if the watched file was deleted, the watcher is still alive and we
// should clean it up.
{
base::RunLoop run_loop;
root_->RemoveWatcher(
base::FilePath(FILE_PATH_LITERAL("dir")),
base::Bind(
[](base::RunLoop* run_loop, base::File::Error error) {
run_loop->Quit();
EXPECT_EQ(base::File::FILE_OK, error);
},
&run_loop));
run_loop.Run();
}
}
TEST_F(ArcDocumentsProviderRootTest, ResolveToContentUrl) {
base::RunLoop run_loop;
root_->ResolveToContentUrl(
base::FilePath(FILE_PATH_LITERAL("dir/photo.jpg")),
base::Bind(
[](base::RunLoop* run_loop, const GURL& url) {
run_loop->Quit();
EXPECT_EQ(GURL("content://org.chromium.test/document/photo-id"),
url);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, ResolveToContentUrlRoot) {
base::RunLoop run_loop;
root_->ResolveToContentUrl(
base::FilePath(FILE_PATH_LITERAL("")),
base::Bind(
[](base::RunLoop* run_loop, const GURL& url) {
run_loop->Quit();
EXPECT_EQ(GURL("content://org.chromium.test/document/root-id"),
url);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, ResolveToContentUrlNoSuchFile) {
base::RunLoop run_loop;
root_->ResolveToContentUrl(base::FilePath(FILE_PATH_LITERAL("missing")),
base::Bind(
[](base::RunLoop* run_loop, const GURL& url) {
run_loop->Quit();
EXPECT_EQ(GURL(), url);
},
&run_loop));
run_loop.Run();
}
TEST_F(ArcDocumentsProviderRootTest, ResolveToContentUrlDups) {
base::RunLoop run_loop;
// "dup 2.mp4" should map to the 3rd instance of "dup.mp4" regardless of the
// order returned from FileSystemInstance.
root_->ResolveToContentUrl(
base::FilePath(FILE_PATH_LITERAL("dups/dup (2).mp4")),
base::Bind(
[](base::RunLoop* run_loop, const GURL& url) {
run_loop->Quit();
EXPECT_EQ(GURL("content://org.chromium.test/document/dup3-id"),
url);
},
&run_loop));
run_loop.Run();
}
} // namespace arc