chromium/chrome/browser/ash/file_manager/documents_provider_root_manager.cc

// Copyright 2019 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/file_manager/documents_provider_root_manager.h"

#include <string.h>

#include <algorithm>
#include <iterator>
#include <string_view>
#include <tuple>
#include <utility>

#include "ash/components/arc/arc_features.h"
#include "ash/components/arc/mojom/bitmap.mojom.h"
#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ash/arc/fileapi/arc_file_system_operation_runner.h"
#include "chrome/browser/profiles/profile.h"
#include "content/public/browser/browser_thread.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/image/image_skia_operations.h"

namespace file_manager {

namespace {

// Some authorities should be excluded from the list of DocumentsProvider roots
// since equivalent contents are already exposed in Files app.
constexpr const char* kAuthoritiesToExclude[] = {
    // Android internal "external storage" is already exposed as "Play Files".
    "com.android.externalstorage.documents",
    // Android's Downloads directory is already shared with Chrome OS's
    // Downloads.
    "com.android.providers.downloads.documents",
    // Documents from media providers are already exposed as media views.
    "com.android.providers.media.documents",
    // Files app already has its own Google Drive integration.
    "com.google.android.apps.docs.storage",
};

bool IsAuthorityToExclude(const std::string& authority) {
  for (const char* authority_to_exclude : kAuthoritiesToExclude) {
    if (base::EqualsCaseInsensitiveASCII(authority, authority_to_exclude)) {
      return true;
    }
  }
  return false;
}

GURL EncodeIconAsUrl(const SkBitmap& bitmap) {
  // Root icons are resized to 32px*32px in the ARC container. We use the given
  // bitmaps without resizing in Chrome side.
  std::vector<unsigned char> output;
  gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, false, &output);
  std::string encoded = base::Base64Encode(std::string_view(
      reinterpret_cast<const char*>(output.data()), output.size()));
  return GURL("data:image/png;base64," + encoded);
}

// A wrapper class for SkBitmap to compare its pixels.
class BitmapWrapper {
 public:
  explicit BitmapWrapper(const SkBitmap* bitmap) : bitmap_(bitmap) {}

  BitmapWrapper(const BitmapWrapper&) = delete;
  BitmapWrapper& operator=(const BitmapWrapper&) = delete;

  bool operator<(const BitmapWrapper& other) const {
    const size_t size1 = bitmap_->computeByteSize();
    const size_t size2 = other.bitmap_->computeByteSize();
    if (size1 == 0 && size2 == 0) {
      return false;
    }
    if (size1 != size2) {
      return size1 < size2;
    }
    return memcmp(bitmap_->getAddr32(0, 0), other.bitmap_->getAddr32(0, 0),
                  size1) < 0;
  }

 private:
  const raw_ptr<const SkBitmap> bitmap_;
};

}  // namespace

DocumentsProviderRootManager::DocumentsProviderRootManager(
    Profile* profile,
    arc::ArcFileSystemOperationRunner* runner)
    : profile_(profile), runner_(runner) {}

DocumentsProviderRootManager::~DocumentsProviderRootManager() {
  arc::ArcFileSystemBridge* bridge =
      arc::ArcFileSystemBridge::GetForBrowserContext(profile_);
  if (bridge) {
    bridge->RemoveObserver(this);
  }
}

void DocumentsProviderRootManager::AddObserver(Observer* observer) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(observer);
  observer_list_.AddObserver(observer);
}

void DocumentsProviderRootManager::RemoveObserver(Observer* observer) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(observer);
  observer_list_.RemoveObserver(observer);
}

void DocumentsProviderRootManager::SetEnabled(bool enabled) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (enabled == is_enabled_) {
    return;
  }

  is_enabled_ = enabled;
  arc::ArcFileSystemBridge* bridge =
      arc::ArcFileSystemBridge::GetForBrowserContext(profile_);
  if (enabled) {
    if (bridge) {
      bridge->AddObserver(this);
    }
    RequestGetRoots();
  } else {
    if (bridge) {
      bridge->RemoveObserver(this);
    }
    ClearRoots();
  }
}

void DocumentsProviderRootManager::OnRootsChanged() {
  // Instead of process delta updates, we get full roots list every time roots
  // list is updated in ARC container, and overwrite the current volume list
  // based on the returned roots list.
  // Note that observer's callbacks are called only for roots which are
  // added/removed/modified.
  RequestGetRoots();
}

DocumentsProviderRootManager::RootInfo::RootInfo() = default;

DocumentsProviderRootManager::RootInfo::RootInfo(const RootInfo& that) =
    default;

DocumentsProviderRootManager::RootInfo::RootInfo(RootInfo&& that) noexcept =
    default;

DocumentsProviderRootManager::RootInfo::~RootInfo() = default;

DocumentsProviderRootManager::RootInfo&
DocumentsProviderRootManager::RootInfo::operator=(const RootInfo& that) =
    default;

DocumentsProviderRootManager::RootInfo&
DocumentsProviderRootManager::RootInfo::operator=(RootInfo&& that) noexcept =
    default;

bool DocumentsProviderRootManager::RootInfo::operator<(
    const RootInfo& rhs) const {
  BitmapWrapper wrapped_icon(&icon);
  BitmapWrapper wrapped_rhs_icon(&rhs.icon);
  return std::tie(authority, root_id, document_id, title, summary,
                  wrapped_icon) < std::tie(rhs.authority, rhs.root_id,
                                           rhs.document_id, rhs.title,
                                           rhs.summary, wrapped_rhs_icon);
}

void DocumentsProviderRootManager::RequestGetRoots() {
  runner_->GetRoots(base::BindOnce(&DocumentsProviderRootManager::OnGetRoots,
                                   weak_ptr_factory_.GetWeakPtr()));
}

void DocumentsProviderRootManager::OnGetRoots(
    std::optional<std::vector<arc::mojom::RootPtr>> maybe_roots) {
  if (!maybe_roots.has_value()) {
    return;
  }

  std::vector<RootInfo> roots_info;
  for (const auto& root : maybe_roots.value()) {
    if (IsAuthorityToExclude(root->authority)) {
      continue;
    }

    RootInfo root_info;
    root_info.authority = root->authority;
    root_info.title = root->title;
    if (root->summary.has_value()) {
      root_info.summary = root->summary.value();
    }
    if (root->icon.has_value()) {
      root_info.icon = root->icon.value();
    }
    root_info.supports_create = root->supports_create;
    if (root->mime_types.has_value()) {
      root_info.mime_types = root->mime_types.value();
    }

    // Strip new lines from the Root and Document IDs.
    //
    // Some Android Documents Provider implementations (e.g. Dropbox) have
    // long, base-64 encoded IDs broken up by '\n' bytes, presumably to help
    // human readability. However, these IDs become part of URLs (represented
    // by GURL or storage::FileSystemURL objects) but passed between processes
    // (e.g. Fusebox) as strings, not C++ objects. Various URL parsing and
    // unparsing along the way can strip out the '\n' bytes, which breaks exact
    // string match on the IDs (e.g. ArcDocumentsProviderRootMap map keys).
    //
    // New lines within a serialized GURL or storage::FileSystemURL also makes
    // it harder for line-based tools (e.g. grep) to process log messages.
    //
    // To avoid those problems, we strip the '\n' bytes eagerly, here.
    base::RemoveChars(root->root_id, "\n", &root_info.root_id);
    base::RemoveChars(root->document_id, "\n", &root_info.document_id);

    roots_info.emplace_back(std::move(root_info));
  }
  std::sort(roots_info.begin(), roots_info.end());
  UpdateRoots(std::move(roots_info));
}

void DocumentsProviderRootManager::UpdateRoots(
    std::vector<RootInfo> new_roots) {
  // |roots_to_remove| should have roots which were in the previous list but
  // do not exist in the new list.
  std::vector<RootInfo> roots_to_remove;
  std::set_difference(current_roots_.begin(), current_roots_.end(),
                      new_roots.begin(), new_roots.end(),
                      std::inserter(roots_to_remove, roots_to_remove.begin()));
  // |roots_to_add| should have roots which were not in the previous list but
  // exist in the new list.
  std::vector<RootInfo> roots_to_add;
  std::set_difference(new_roots.begin(), new_roots.end(),
                      current_roots_.begin(), current_roots_.end(),
                      std::inserter(roots_to_add, roots_to_add.begin()));
  for (const auto& info : roots_to_remove) {
    NotifyRootRemoved(info);
  }
  for (const auto& info : roots_to_add) {
    NotifyRootAdded(info);
  }
  current_roots_.swap(new_roots);
}

void DocumentsProviderRootManager::ClearRoots() {
  UpdateRoots({});
}

void DocumentsProviderRootManager::NotifyRootAdded(const RootInfo& info) {
  for (auto& observer : observer_list_) {
    observer.OnDocumentsProviderRootAdded(
        info.authority, info.root_id, info.document_id, info.title,
        info.summary, !info.icon.empty() ? EncodeIconAsUrl(info.icon) : GURL(),
        !info.supports_create, info.mime_types);
  }
}

void DocumentsProviderRootManager::NotifyRootRemoved(const RootInfo& info) {
  for (auto& observer : observer_list_) {
    observer.OnDocumentsProviderRootRemoved(info.authority, info.root_id);
  }
}

}  // namespace file_manager