| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/arc/common/intent_helper/activity_icon_loader.h" |
| |
| #include <string.h> |
| |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/base64.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/task/thread_pool.h" |
| #include "components/arc/common/intent_helper/adaptive_icon_delegate.h" |
| #include "third_party/abseil-cpp/absl/types/variant.h" |
| #include "ui/base/resource/resource_scale_factor.h" |
| #include "ui/gfx/codec/png_codec.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/image/image_skia_rep.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| #include "ash/components/arc/arc_util.h" |
| #include "ash/components/arc/session/arc_bridge_service.h" |
| #include "ash/components/arc/session/arc_service_manager.h" |
| #else // BUILDFLAG(IS_CHROMEOS_LACROS) |
| #include "chromeos/lacros/lacros_service.h" |
| #endif |
| |
| namespace arc { |
| namespace internal { |
| |
| namespace { |
| |
| constexpr size_t kSmallIconSizeInDip = 16; |
| constexpr size_t kLargeIconSizeInDip = 20; |
| constexpr size_t kMaxIconSizeInPx = 200; |
| constexpr char kPngDataUrlPrefix[] = "data:image/png;base64,"; |
| |
| // Returns an instance for calling RequestActivityIcons(). |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| // Ash requests icons to ArcServiceManager. |
| absl::variant<mojom::IntentHelperInstance*, ActivityIconLoader::GetResult> |
| GetInstanceForRequestActivityIcons() { |
| auto* arc_service_manager = ArcServiceManager::Get(); |
| if (!arc_service_manager) { |
| // TODO(hidehiko): IsArcAvailable() looks not the condition to be checked |
| // here, because ArcServiceManager instance is created regardless of ARC |
| // availability. This happens only before MessageLoop starts or after |
| // MessageLoop stops, practically. |
| // Also, returning FAILED_ARC_NOT_READY looks problematic at the moment, |
| // because ArcProcessTask::StartIconLoading accesses to |
| // ArcServiceManager::Get() return value, which can be nullptr. |
| if (!IsArcAvailable()) { |
| VLOG(2) << "ARC bridge is not supported."; |
| return ActivityIconLoader::GetResult::FAILED_ARC_NOT_SUPPORTED; |
| } |
| |
| VLOG(2) << "ARC bridge is not ready."; |
| return ActivityIconLoader::GetResult::FAILED_ARC_NOT_READY; |
| } |
| |
| auto* intent_helper_holder = |
| arc_service_manager->arc_bridge_service()->intent_helper(); |
| if (!intent_helper_holder->IsConnected()) { |
| VLOG(2) << "ARC intent helper instance is not ready."; |
| return ActivityIconLoader::GetResult::FAILED_ARC_NOT_READY; |
| } |
| |
| auto* instance = |
| ARC_GET_INSTANCE_FOR_METHOD(intent_helper_holder, RequestActivityIcons); |
| if (!instance) |
| return ActivityIconLoader::GetResult::FAILED_ARC_NOT_SUPPORTED; |
| return instance; |
| } |
| #else // BUILDFLAG(IS_CHROMEOS_LACROS) |
| // Adapter class for wrapping crosapi::mojom::Arc used in lacros-chrome. |
| // This is returned from GetInstanceForRequestActivityIcons(). |
| class Adapter { |
| public: |
| explicit Adapter(crosapi::mojom::Arc* instance) : instance_(instance) {} |
| ~Adapter() = default; |
| |
| using OnRequestActivityIconsSucceededCallback = |
| base::OnceCallback<void(std::vector<crosapi::mojom::ActivityIconPtr>)>; |
| |
| // If status is not kSuccess, immediately return callback. |
| void RequestActivityIcons( |
| std::vector<crosapi::mojom::ActivityNamePtr> activities, |
| crosapi::mojom::ScaleFactor scale_factor, |
| OnRequestActivityIconsSucceededCallback cb) { |
| instance_->RequestActivityIcons( |
| std::move(activities), scale_factor, |
| base::BindOnce( |
| [](OnRequestActivityIconsSucceededCallback cb, |
| std::vector<crosapi::mojom::ActivityIconPtr> icons, |
| crosapi::mojom::RequestActivityIconsStatus status) { |
| // If status is not kSuccess, immediately return callback. |
| if (status == crosapi::mojom::RequestActivityIconsStatus:: |
| kArcNotAvailable) { |
| LOG(ERROR) << "Failed to connect to ARC in ash-chrome."; |
| std::move(cb).Run( |
| std::vector<crosapi::mojom::ActivityIconPtr>()); |
| return; |
| } |
| |
| std::move(cb).Run(std::move(icons)); |
| }, |
| std::move(cb))); |
| } |
| |
| private: |
| raw_ptr<crosapi::mojom::Arc> instance_; |
| }; |
| |
| // Lacros requests icons to ash-chrome via crosapi. |
| absl::variant<std::unique_ptr<Adapter>, ActivityIconLoader::GetResult> |
| GetInstanceForRequestActivityIcons() { |
| auto* service = chromeos::LacrosService::Get(); |
| |
| if (!service || !service->IsAvailable<crosapi::mojom::Arc>()) { |
| VLOG(2) << "ARC is not supported in Lacros."; |
| return ActivityIconLoader::GetResult::FAILED_ARC_NOT_SUPPORTED; |
| } |
| |
| if (service->GetInterfaceVersion<crosapi::mojom::Arc>() < |
| int{crosapi::mojom::Arc::MethodMinVersions:: |
| kRequestActivityIconsMinVersion}) { |
| VLOG(2) << "Ash Lacros-Arc version " |
| << service->GetInterfaceVersion<crosapi::mojom::Arc>() |
| << " does not support RequestActivityIcons()."; |
| return ActivityIconLoader::GetResult::FAILED_ARC_NOT_SUPPORTED; |
| } |
| |
| return std::make_unique<Adapter>( |
| service->GetRemote<crosapi::mojom::Arc>().get()); |
| } |
| |
| #endif |
| |
| ActivityIconLoader::ActivityName GenerateActivityName( |
| const ActivityIconLoader::ActivityIconPtr& icon) { |
| return ActivityIconLoader::ActivityName( |
| icon->activity->package_name, icon->activity->activity_name.has_value() |
| ? (*icon->activity->activity_name) |
| : std::string()); |
| } |
| |
| // Encodes the |image| as PNG data considering scale factor, and returns it as |
| // data: URL. |
| scoped_refptr<base::RefCountedData<GURL>> GeneratePNGDataUrl( |
| const gfx::ImageSkia& image, |
| ui::ResourceScaleFactor scale_factor) { |
| float scale = ui::GetScaleForResourceScaleFactor(scale_factor); |
| std::vector<unsigned char> output; |
| gfx::PNGCodec::EncodeBGRASkBitmap(image.GetRepresentation(scale).GetBitmap(), |
| false /* discard_transparency */, &output); |
| const std::string encoded = base::Base64Encode(std::string_view( |
| reinterpret_cast<const char*>(output.data()), output.size())); |
| return base::WrapRefCounted( |
| new base::RefCountedData<GURL>(GURL(kPngDataUrlPrefix + encoded))); |
| } |
| |
| ActivityIconLoader::Icons ResizeIconsInternal( |
| const gfx::ImageSkia& image, |
| ui::ResourceScaleFactor scale_factor) { |
| // Resize the original icon to the sizes intent_helper needs. |
| gfx::ImageSkia icon_large(gfx::ImageSkiaOperations::CreateResizedImage( |
| image, skia::ImageOperations::RESIZE_BEST, |
| gfx::Size(kLargeIconSizeInDip, kLargeIconSizeInDip))); |
| icon_large.MakeThreadSafe(); |
| gfx::Image icon20(icon_large); |
| |
| gfx::ImageSkia icon_small(gfx::ImageSkiaOperations::CreateResizedImage( |
| image, skia::ImageOperations::RESIZE_BEST, |
| gfx::Size(kSmallIconSizeInDip, kSmallIconSizeInDip))); |
| icon_small.MakeThreadSafe(); |
| gfx::Image icon16(icon_small); |
| |
| return ActivityIconLoader::Icons( |
| icon16, icon20, GeneratePNGDataUrl(icon_small, scale_factor)); |
| } |
| |
| std::unique_ptr<ActivityIconLoader::ActivityToIconsMap> ResizeAndEncodeIcons( |
| std::vector<ActivityIconLoader::ActivityIconPtr> icons, |
| ui::ResourceScaleFactor scale_factor) { |
| auto result = std::make_unique<ActivityIconLoader::ActivityToIconsMap>(); |
| for (size_t i = 0; i < icons.size(); ++i) { |
| static const size_t kBytesPerPixel = 4; |
| const ActivityIconLoader::ActivityIconPtr& icon = icons.at(i); |
| if (icon->width > kMaxIconSizeInPx || icon->height > kMaxIconSizeInPx || |
| icon->width == 0 || icon->height == 0 || |
| icon->icon.size() != (icon->width * icon->height * kBytesPerPixel)) { |
| continue; |
| } |
| |
| SkBitmap bitmap; |
| bitmap.allocPixels(SkImageInfo::MakeN32Premul(icon->width, icon->height)); |
| if (!bitmap.getPixels()) |
| continue; |
| DCHECK_GE(bitmap.computeByteSize(), icon->icon.size()); |
| memcpy(bitmap.getPixels(), &icon->icon.front(), icon->icon.size()); |
| |
| gfx::ImageSkia original(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); |
| |
| result->insert(std::make_pair(GenerateActivityName(icon), |
| ResizeIconsInternal(original, scale_factor))); |
| } |
| |
| return result; |
| } |
| |
| std::unique_ptr<ActivityIconLoader::ActivityToIconsMap> ResizeIcons( |
| std::vector<ActivityIconLoader::ActivityName> activity_names, |
| const std::vector<gfx::ImageSkia>& images, |
| ui::ResourceScaleFactor scale_factor) { |
| DCHECK_EQ(activity_names.size(), images.size()); |
| auto result = std::make_unique<ActivityIconLoader::ActivityToIconsMap>(); |
| for (size_t i = 0; i < activity_names.size(); ++i) { |
| result->insert(std::make_pair( |
| activity_names[i], ResizeIconsInternal(images[i], scale_factor))); |
| } |
| |
| return result; |
| } |
| |
| } // namespace |
| |
| ActivityIconLoader::Icons::Icons( |
| const gfx::Image& icon16, |
| const gfx::Image& icon20, |
| const scoped_refptr<base::RefCountedData<GURL>>& icon16_dataurl) |
| : icon16(icon16), icon20(icon20), icon16_dataurl(icon16_dataurl) {} |
| |
| ActivityIconLoader::Icons::Icons(const Icons& other) = default; |
| |
| ActivityIconLoader::Icons::~Icons() = default; |
| |
| ActivityIconLoader::ActivityName::ActivityName(const std::string& package_name, |
| const std::string& activity_name) |
| : package_name(package_name), activity_name(activity_name) {} |
| |
| ActivityIconLoader::ActivityName::~ActivityName() = default; |
| |
| bool ActivityIconLoader::ActivityName::operator<( |
| const ActivityName& other) const { |
| return std::tie(package_name, activity_name) < |
| std::tie(other.package_name, other.activity_name); |
| } |
| |
| ActivityIconLoader::ActivityIconLoader() |
| : scale_factor_(ui::GetMaxSupportedResourceScaleFactor()) {} |
| |
| ActivityIconLoader::~ActivityIconLoader() = default; |
| |
| void ActivityIconLoader::SetAdaptiveIconDelegate( |
| AdaptiveIconDelegate* delegate) { |
| delegate_ = delegate; |
| } |
| |
| void ActivityIconLoader::InvalidateIcons(const std::string& package_name) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| for (auto it = cached_icons_.begin(); it != cached_icons_.end();) { |
| if (it->first.package_name == package_name) |
| it = cached_icons_.erase(it); |
| else |
| ++it; |
| } |
| } |
| |
| ActivityIconLoader::GetResult ActivityIconLoader::GetActivityIcons( |
| const std::vector<ActivityName>& activities, |
| OnIconsReadyCallback cb) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| std::unique_ptr<ActivityToIconsMap> result(new ActivityToIconsMap); |
| std::vector<ActivityNamePtr> activities_to_fetch; |
| |
| for (const auto& activity : activities) { |
| const auto& it = cached_icons_.find(activity); |
| if (it == cached_icons_.end()) { |
| ActivityNamePtr name(ActivityNamePtr::Struct::New()); |
| name->package_name = activity.package_name; |
| name->activity_name = activity.activity_name; |
| activities_to_fetch.push_back(std::move(name)); |
| } else { |
| result->insert(std::make_pair(activity, it->second)); |
| } |
| } |
| |
| if (activities_to_fetch.empty()) { |
| // If there's nothing to fetch, run the callback now. |
| std::move(cb).Run(std::move(result)); |
| return GetResult::SUCCEEDED_SYNC; |
| } |
| |
| auto instance = GetInstanceForRequestActivityIcons(); |
| if (absl::holds_alternative<GetResult>(instance)) { |
| // The mojo channel is not yet ready (or not supported at all). Run the |
| // callback with |result| that could be empty. |
| std::move(cb).Run(std::move(result)); |
| return absl::get<GetResult>(instance); |
| } |
| |
| // Fetch icons from ARC. |
| absl::get<0>(instance)->RequestActivityIcons( |
| std::move(activities_to_fetch), ScaleFactor(scale_factor_), |
| base::BindOnce(&ActivityIconLoader::OnIconsReady, |
| weak_ptr_factory_.GetWeakPtr(), std::move(result), |
| std::move(cb))); |
| return GetResult::SUCCEEDED_ASYNC; |
| } |
| |
| void ActivityIconLoader::OnIconsResizedForTesting( |
| OnIconsReadyCallback cb, |
| std::unique_ptr<ActivityToIconsMap> result) { |
| OnIconsResized(std::make_unique<ActivityToIconsMap>(), std::move(cb), |
| std::move(result)); |
| } |
| |
| void ActivityIconLoader::AddCacheEntryForTesting(const ActivityName& activity) { |
| cached_icons_.insert( |
| std::make_pair(activity, Icons(gfx::Image(), gfx::Image(), nullptr))); |
| } |
| |
| void ActivityIconLoader::OnIconsReadyForTesting( |
| std::unique_ptr<ActivityToIconsMap> cached_result, |
| OnIconsReadyCallback cb, |
| std::vector<ActivityIconPtr> icons) { |
| OnIconsReady(std::move(cached_result), std::move(cb), std::move(icons)); |
| } |
| |
| // static |
| bool ActivityIconLoader::HasIconsReadyCallbackRun(GetResult result) { |
| switch (result) { |
| case GetResult::SUCCEEDED_ASYNC: |
| return false; |
| case GetResult::SUCCEEDED_SYNC: |
| case GetResult::FAILED_ARC_NOT_READY: |
| case GetResult::FAILED_ARC_NOT_SUPPORTED: |
| break; |
| } |
| return true; |
| } |
| |
| void ActivityIconLoader::OnIconsReady( |
| std::unique_ptr<ActivityToIconsMap> cached_result, |
| OnIconsReadyCallback cb, |
| std::vector<ActivityIconPtr> icons) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (delegate_) { |
| std::vector<ActivityName> actvity_names; |
| for (const auto& icon : icons) |
| actvity_names.emplace_back(GenerateActivityName(icon)); |
| |
| delegate_->GenerateAdaptiveIcons( |
| icons, |
| base::BindOnce(&ActivityIconLoader::OnAdaptiveIconGenerated, |
| weak_ptr_factory_.GetWeakPtr(), std::move(actvity_names), |
| std::move(cached_result), std::move(cb))); |
| return; |
| } |
| |
| // TODO(crbug.com/40131344): Remove when the adaptive icon feature is enabled |
| // by default. |
| // TODO(crbug.com/40806186): Adaptive Icon is not supported in Lacros now. Do |
| // not remove this until it's supported. |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&ResizeAndEncodeIcons, std::move(icons), scale_factor_), |
| base::BindOnce(&ActivityIconLoader::OnIconsResized, |
| weak_ptr_factory_.GetWeakPtr(), std::move(cached_result), |
| std::move(cb))); |
| } |
| |
| void ActivityIconLoader::OnAdaptiveIconGenerated( |
| std::vector<ActivityName> actvity_names, |
| std::unique_ptr<ActivityToIconsMap> cached_result, |
| OnIconsReadyCallback cb, |
| const std::vector<gfx::ImageSkia>& adaptive_icons) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, |
| base::BindOnce(&ResizeIcons, std::move(actvity_names), adaptive_icons, |
| scale_factor_), |
| base::BindOnce(&ActivityIconLoader::OnIconsResized, |
| weak_ptr_factory_.GetWeakPtr(), std::move(cached_result), |
| std::move(cb))); |
| } |
| |
| void ActivityIconLoader::OnIconsResized( |
| std::unique_ptr<ActivityToIconsMap> cached_result, |
| OnIconsReadyCallback cb, |
| std::unique_ptr<ActivityToIconsMap> result) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // Update |cached_icons_|. |
| for (const auto& kv : *result) { |
| cached_icons_.erase(kv.first); |
| cached_icons_.insert(std::make_pair(kv.first, kv.second)); |
| } |
| |
| // Merge the results that were obtained from cache before doing IPC. |
| result->insert(cached_result->begin(), cached_result->end()); |
| std::move(cb).Run(std::move(result)); |
| } |
| |
| } // namespace internal |
| } // namespace arc |