chromium/components/paint_preview/player/android/player_compositor_delegate_android.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 "components/paint_preview/player/android/player_compositor_delegate_android.h"

#include <vector>

#include "base/android/callback_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/android/unguessable_token_android.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/common/trace_event_common.h"
#include "base/trace_event/trace_event.h"
#include "base/unguessable_token.h"
#include "components/paint_preview/browser/paint_preview_base_service.h"
#include "components/paint_preview/player/android/convert_to_java_bitmap.h"
#include "components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom.h"
#include "ui/gfx/geometry/rect.h"
#include "url/gurl.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "components/paint_preview/player/android/jni_headers/PlayerCompositorDelegateImpl_jni.h"

using base::android::JavaParamRef;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;

namespace paint_preview {

namespace {

// To minimize peak memory usage limit the number of concurrent bitmap requests.
// These correspond to memory pressure levels None, Moderate, Critical
// respectively. If a value of 0 is used for any level the process will abort
// once that memory level is reached.
constexpr std::
    array<size_t, PlayerCompositorDelegateAndroid::PressureLevelCount::kLevels>
        kMaxParallelBitmapRequests = {3, 2, 0};
constexpr std::
    array<size_t, PlayerCompositorDelegateAndroid::PressureLevelCount::kLevels>
        kMaxParallelBitmapRequestsLowMemory = {2, 1, 0};

}  // namespace

jlong JNI_PlayerCompositorDelegateImpl_Initialize(
    JNIEnv* env,
    const JavaParamRef<jobject>& j_object,
    jlong paint_preview_service,
    jlong j_capture_result_ptr,
    const JavaParamRef<jstring>& j_url_spec,
    const JavaParamRef<jstring>& j_directory_key,
    jboolean j_main_frame_mode,
    const JavaParamRef<jobject>& j_compositor_error_callback,
    jboolean j_is_low_mem) {
  TRACE_EVENT0("paint_preview", "JNI_PlayerCompositorDelegateImpl_Initialize");
  PlayerCompositorDelegateAndroid* delegate =
      new PlayerCompositorDelegateAndroid(
          env, j_object,
          reinterpret_cast<PaintPreviewBaseService*>(paint_preview_service),
          j_capture_result_ptr, j_url_spec, j_directory_key, j_main_frame_mode,
          j_compositor_error_callback, j_is_low_mem);
  return reinterpret_cast<intptr_t>(delegate);
}

PlayerCompositorDelegateAndroid::PlayerCompositorDelegateAndroid(
    JNIEnv* env,
    const JavaParamRef<jobject>& j_object,
    PaintPreviewBaseService* paint_preview_service,
    jlong j_capture_result_ptr,
    const JavaParamRef<jstring>& j_url_spec,
    const JavaParamRef<jstring>& j_directory_key,
    jboolean j_main_frame_mode,
    const JavaParamRef<jobject>& j_compositor_error_callback,
    jboolean j_is_low_mem)
    : PlayerCompositorDelegate(),
      request_id_(0),
      task_runner_(base::ThreadPool::CreateTaskRunner(
          {base::TaskPriority::USER_VISIBLE})),
      startup_timestamp_(base::TimeTicks::Now()) {
  std::string url_string;
  if (j_capture_result_ptr) {
    // Show@Startup doesn't use this.
    std::unique_ptr<CaptureResult> capture_result(
        reinterpret_cast<CaptureResult*>(j_capture_result_ptr));
    url_string = capture_result->proto.metadata().url();
    PlayerCompositorDelegate::SetCaptureResult(std::move(capture_result));
  } else {
    url_string = base::android::ConvertJavaStringToUTF8(env, j_url_spec);
  }

  PlayerCompositorDelegate::Initialize(
      paint_preview_service, GURL(url_string),
      DirectoryKey{
          base::android::ConvertJavaStringToUTF8(env, j_directory_key)},
      static_cast<bool>(j_main_frame_mode),
      base::BindOnce(&base::android::RunIntCallbackAndroid,
                     ScopedJavaGlobalRef<jobject>(j_compositor_error_callback)),
      base::Seconds(15),
      (static_cast<bool>(j_is_low_mem) ? kMaxParallelBitmapRequestsLowMemory
                                       : kMaxParallelBitmapRequests));

  java_ref_.Reset(env, j_object);
}

void PlayerCompositorDelegateAndroid::OnCompositorReady(
    CompositorStatus compositor_status,
    mojom::PaintPreviewBeginCompositeResponsePtr composite_response,
    float page_scale_factor,
    std::unique_ptr<ui::AXTreeUpdate> ax_tree) {
  TRACE_EVENT0("paint_preview",
               "PlayerCompositorDelegateAndroid::OnCompositorReady");
  bool compositor_started = CompositorStatus::OK == compositor_status;
  base::UmaHistogramBoolean(
      "Browser.PaintPreview.Player.CompositorProcessStartedCorrectly",
      compositor_started);
  if (!compositor_started) {
    DLOG(ERROR) << "Compositor process failed to begin with code: "
                << static_cast<int>(compositor_status);
    if (compositor_error_)
      std::move(compositor_error_).Run(static_cast<int>(compositor_status));

    return;
  }
  auto delta = base::TimeTicks::Now() - startup_timestamp_;
  if (delta.InMicroseconds() >= 0) {
    base::UmaHistogramTimes(
        "Browser.PaintPreview.Player.CompositorProcessStartupTime", delta);
  }
  JNIEnv* env = base::android::AttachCurrentThread();

  std::vector<base::UnguessableToken> all_guids;
  std::vector<int32_t> scroll_extents;
  std::vector<int32_t> scroll_offsets;
  std::vector<int32_t> subframe_count;
  std::vector<base::UnguessableToken> subframe_ids;
  std::vector<int32_t> subframe_rects;
  base::UnguessableToken root_frame_guid;

  if (composite_response) {
    CompositeResponseFramesToVectors(
        composite_response->frames, &all_guids, &scroll_extents,
        &scroll_offsets, &subframe_count, &subframe_ids, &subframe_rects);
    root_frame_guid = composite_response->root_frame_guid;
  } else {
    // If there is no composite response due to a failure we don't have a root
    // frame GUID to pass. However, the token cannot be null so create a
    // placeholder.
    root_frame_guid = base::UnguessableToken::Create();
  }

  Java_PlayerCompositorDelegateImpl_onCompositorReady(
      env, java_ref_, root_frame_guid, all_guids, scroll_extents,
      scroll_offsets, subframe_count, subframe_ids, subframe_rects,
      page_scale_factor, reinterpret_cast<intptr_t>(ax_tree.release()));
}

ScopedJavaLocalRef<jintArray>
PlayerCompositorDelegateAndroid::GetRootFrameOffsets(JNIEnv* env) {
  auto offsets = PlayerCompositorDelegate::GetRootFrameOffsets();
  ScopedJavaLocalRef<jintArray> j_offsets = base::android::ToJavaIntArray(
      env, std::vector<int>({offsets.x(), offsets.y()}));
  return j_offsets;
}

void PlayerCompositorDelegateAndroid::OnMemoryPressure(
    base::MemoryPressureListener::MemoryPressureLevel memory_pressure_level) {
  // Don't handle the critical case leave that to the base class implementation
  // which should kill the preview.
  if (memory_pressure_level ==
      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE) {
    Java_PlayerCompositorDelegateImpl_onModerateMemoryPressure(
        base::android::AttachCurrentThread(), java_ref_);
  }
  PlayerCompositorDelegate::OnMemoryPressure(memory_pressure_level);
}

// static
void PlayerCompositorDelegateAndroid::CompositeResponseFramesToVectors(
    const base::flat_map<base::UnguessableToken, mojom::FrameDataPtr>& frames,
    std::vector<base::UnguessableToken>* all_guids,
    std::vector<int>* scroll_extents,
    std::vector<int>* scroll_offsets,
    std::vector<int>* subframe_count,
    std::vector<base::UnguessableToken>* subframe_ids,
    std::vector<int>* subframe_rects) {
  all_guids->reserve(frames.size());
  scroll_extents->reserve(2 * frames.size());
  subframe_count->reserve(frames.size());
  int all_subframes_count = 0;
  for (const auto& pair : frames) {
    all_guids->push_back(pair.first);
    scroll_extents->push_back(pair.second->scroll_extents.width());
    scroll_extents->push_back(pair.second->scroll_extents.height());
    scroll_offsets->push_back(pair.second->scroll_offsets.width());
    scroll_offsets->push_back(pair.second->scroll_offsets.height());
    subframe_count->push_back(pair.second->subframes.size());
    all_subframes_count += pair.second->subframes.size();
  }

  subframe_ids->reserve(all_subframes_count);
  subframe_rects->reserve(4 * all_subframes_count);
  for (const auto& pair : frames) {
    for (const auto& subframe : pair.second->subframes) {
      subframe_ids->push_back(subframe->frame_guid);
      subframe_rects->push_back(subframe->clip_rect.x());
      subframe_rects->push_back(subframe->clip_rect.y());
      subframe_rects->push_back(subframe->clip_rect.width());
      subframe_rects->push_back(subframe->clip_rect.height());
    }
  }
}

jint PlayerCompositorDelegateAndroid::RequestBitmap(
    JNIEnv* env,
    std::optional<base::UnguessableToken>& frame_guid,
    const JavaParamRef<jobject>& j_bitmap_callback,
    const JavaParamRef<jobject>& j_error_callback,
    jfloat j_scale_factor,
    jint j_clip_x,
    jint j_clip_y,
    jint j_clip_width,
    jint j_clip_height) {
  TRACE_EVENT0("paint_preview", "RequestBitmap");
  TRACE_EVENT_NESTABLE_ASYNC_BEGIN0(
      "paint_preview", "PlayerCompositorDelegateAndroid::RequestBitmap",
      TRACE_ID_LOCAL(request_id_));
  gfx::Rect rect(j_clip_x, j_clip_y, j_clip_width, j_clip_height);
  auto callback = base::BindPostTask(
      task_runner_,
      base::BindOnce(
          &ConvertToJavaBitmap,
          base::BindPostTaskToCurrentDefault(base::BindOnce(
              &PlayerCompositorDelegateAndroid::OnJavaBitmapCallback,
              weak_factory_.GetWeakPtr(),
              ScopedJavaGlobalRef<jobject>(j_bitmap_callback),
              ScopedJavaGlobalRef<jobject>(j_error_callback), request_id_))));
  ++request_id_;

  // Callback can skip UI thread.
  return static_cast<jint>(
      PlayerCompositorDelegate::RequestBitmap(frame_guid, rect, j_scale_factor,
                                              std::move(callback)),
      /*run_callback_on_default_task_runner=*/false);
}

jboolean PlayerCompositorDelegateAndroid::CancelBitmapRequest(
    JNIEnv* env,
    jint j_request_id) {
  return static_cast<jboolean>(PlayerCompositorDelegate::CancelBitmapRequest(
      static_cast<int32_t>(j_request_id)));
}

void PlayerCompositorDelegateAndroid::CancelAllBitmapRequests(JNIEnv* env) {
  PlayerCompositorDelegate::CancelAllBitmapRequests();
}

void PlayerCompositorDelegateAndroid::OnJavaBitmapCallback(
    const ScopedJavaGlobalRef<jobject>& j_bitmap_callback,
    const ScopedJavaGlobalRef<jobject>& j_error_callback,
    int request_id,
    JavaBitmapResult result) {
  TRACE_EVENT0("paint_preview", "OnBitmapReceived");
  TRACE_EVENT_NESTABLE_ASYNC_END2(
      "paint_preview", "PlayerCompositorDelegateAndroid::RequestBitmap",
      TRACE_ID_LOCAL(request_id), "status", static_cast<int>(result.status),
      "bytes", result.bytes);

  if (result.status ==
      mojom::PaintPreviewCompositor::BitmapStatus::kAllocFailed) {
    base::android::RunRunnableAndroid(j_error_callback);
    // Treat this as a critical memory pressure failure. We should abort.
    OnMemoryPressure(
        base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_CRITICAL);
    return;
  }

  if (result.status != mojom::PaintPreviewCompositor::BitmapStatus::kSuccess) {
    base::android::RunRunnableAndroid(j_error_callback);
    return;
  }

  base::android::RunObjectCallbackAndroid(j_bitmap_callback,
                                          result.java_bitmap);

  if (request_id == 0) {
    auto delta = base::TimeTicks::Now() - startup_timestamp_;
    if (delta.InMicroseconds() >= 0) {
      base::UmaHistogramTimes("Browser.PaintPreview.Player.TimeToFirstBitmap",
                              delta);
    }
  }
}

ScopedJavaLocalRef<jstring> PlayerCompositorDelegateAndroid::OnClick(
    JNIEnv* env,
    std::optional<base::UnguessableToken>& frame_guid,
    jint j_x,
    jint j_y) {
  if (!frame_guid.has_value()) {
    return base::android::ConvertUTF8ToJavaString(env, "");
  }
  auto res = PlayerCompositorDelegate::OnClick(
      frame_guid.value(),
      gfx::Rect(static_cast<int>(j_x), static_cast<int>(j_y), 1U, 1U));
  if (res.empty())
    return base::android::ConvertUTF8ToJavaString(env, "");

  base::UmaHistogramBoolean("Browser.PaintPreview.Player.LinkClicked", true);
  // TODO(crbug.com/40122441): Resolve cases where there are multiple links.
  // For now just return the first in the list.
  return base::android::ConvertUTF8ToJavaString(env, res[0]->spec());
}

void PlayerCompositorDelegateAndroid::SetCompressOnClose(
    JNIEnv* env,
    jboolean compress_on_close) {
  PlayerCompositorDelegate::SetCompressOnClose(
      static_cast<bool>(compress_on_close));
}

void PlayerCompositorDelegateAndroid::Destroy(JNIEnv* env) {
  delete this;
}

PlayerCompositorDelegateAndroid::~PlayerCompositorDelegateAndroid() = default;

}  // namespace paint_preview