// Copyright 2013 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/page_info/android/page_info_controller_android.h"

#include <algorithm>
#include <string>

#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "base/notimplemented.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/browser/permission_settings_registry.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/content_settings/core/common/features.h"
#include "components/page_info/android/page_info_client.h"
#include "components/page_info/core/features.h"
#include "components/page_info/page_info.h"
#include "components/page_info/page_info_ui.h"
#include "components/permissions/features.h"
#include "components/security_state/core/security_state.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "device/vr/buildflags/buildflags.h"
#include "media/base/media_switches.h"
#include "net/base/features.h"
#include "services/network/public/cpp/features.h"
#include "third_party/blink/public/common/features.h"
#include "ui/base/window_open_disposition.h"
#include "ui/events/event.h"
#include "url/origin.h"
#include "url/url_constants.h"

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

#if BUILDFLAG(ENABLE_VR)
#include "device/vr/public/cpp/features.h"
#endif

using base::android::ConvertUTF16ToJavaString;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaRef;

// static
static int64_t JNI_PageInfoController_Init(
    JNIEnv* env,
    const JavaRef<jobject>& obj,
    const JavaRef<jobject>& java_web_contents) {
  content::WebContents* web_contents =
      content::WebContents::FromJavaWebContents(java_web_contents);

  // Important to use GetVisibleEntry to match what's showing in the omnibox.
  content::NavigationEntry* nav_entry =
      web_contents->GetController().GetVisibleEntry();
  if (nav_entry->IsInitialEntry())
    return 0;

  return reinterpret_cast<intptr_t>(
      new PageInfoControllerAndroid(env, obj, web_contents));
}

PageInfoControllerAndroid::PageInfoControllerAndroid(
    JNIEnv* env,
    const base::android::JavaRef<jobject>& java_page_info_pop,
    content::WebContents* web_contents) {
  content::NavigationEntry* nav_entry =
      web_contents->GetController().GetVisibleEntry();

  url_ = nav_entry->GetURL();
  web_contents_ = web_contents;

  controller_jobject_.Reset(env, java_page_info_pop);

  page_info::PageInfoClient* page_info_client = page_info::GetPageInfoClient();
  DCHECK(page_info_client);
  presenter_ = std::make_unique<PageInfo>(
      page_info_client->CreatePageInfoDelegate(web_contents), web_contents,
      nav_entry->GetURL());
  presenter_->InitializeUiState(this, base::DoNothing());
}

PageInfoControllerAndroid::~PageInfoControllerAndroid() = default;

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

void PageInfoControllerAndroid::RecordPageInfoAction(JNIEnv* env,
                                                     int32_t action) {
  presenter_->RecordPageInfoAction(
      static_cast<page_info::PageInfoAction>(action));
}

void PageInfoControllerAndroid::UpdatePermissions(JNIEnv* env) {
  presenter_->UpdatePermissions();
}

void PageInfoControllerAndroid::SetIdentityInfo(
    const IdentityInfo& identity_info) {
  JNIEnv* env = base::android::AttachCurrentThread();
  std::unique_ptr<PageInfoUI::SecurityDescription> security_description =
      GetSecurityDescription(identity_info);

  is_suspicious_site_ =
      (identity_info.safe_browsing_status ==
       PageInfo::SAFE_BROWSING_STATUS_WARNABLE_SUSPICIOUS_SITE);

  if (base::FeatureList::IsEnabled(net::features::kVerifyQWACs)) {
    if (PageInfo::IsFileOrInternalPage(url_)) {
      // On Desktop, when PageInfo::IsFileOrInternalPage returns true, an
      // InternalPageInfoBubbleView is used rather than the regular
      // PageInfoBubbleView. That view displays only an info icon and a summary
      // string.
      Java_PageInfoController_showConnectionSecurityInfo(env,
                                                         controller_jobject_);
    } else if (is_suspicious_site_) {
      // Have the controller set security description and configure suspicious
      // site UI directly.
      Java_PageInfoController_setSecurityDescription(
          env, controller_jobject_,
          ConvertUTF16ToJavaString(env, security_description->summary),
          ConvertUTF16ToJavaString(env, security_description->details),
          is_suspicious_site_);
    } else if (security_description->summary_style ==
               SecuritySummaryColor::GREEN) {
      // Have the controller set up the button that will show the connection
      // security subpage.
      Java_PageInfoController_showOpenSecurityPageButton(
          env, controller_jobject_,
          ConvertUTF16ToJavaString(env, security_description->summary));
    } else {
      // Have the controller add the connection security UI directly to the page
      // info UI.
      Java_PageInfoController_showConnectionSecurityInfo(env,
                                                         controller_jobject_);
    }
    return;
  }

  Java_PageInfoController_setSecurityDescription(
      env, controller_jobject_,
      ConvertUTF16ToJavaString(env, security_description->summary),
      ConvertUTF16ToJavaString(env, security_description->details),
      is_suspicious_site_);
}

void PageInfoControllerAndroid::OnSuspiciousSiteBackToSafety(JNIEnv* env) {
  if (presenter_) {
    presenter_->OnSuspiciousSiteBackToSafety();
  }
}

void PageInfoControllerAndroid::OnSuspiciousSiteMarkAsSafe(JNIEnv* env) {
  if (presenter_) {
    presenter_->OnSuspiciousSiteMarkAsSafe();
  }
}

void PageInfoControllerAndroid::OpenSafeBrowsingHelpCenter(JNIEnv* env) {
  if (presenter_) {
    presenter_->OpenSafeBrowsingHelpCenterPage(/*event=*/nullptr,
                                               is_suspicious_site_);
  }
}

void PageInfoControllerAndroid::SetIsSuspiciousSite(JNIEnv* env,
                                                    bool is_suspicious_site) {
  is_suspicious_site_ = is_suspicious_site;
}

void PageInfoControllerAndroid::SetPageFeatureInfo(
    const PageFeatureInfo& info) {
  NOTIMPLEMENTED();
}

void PageInfoControllerAndroid::SetPermissionInfo(
    const PermissionInfoList& permission_info_list,
    ChosenObjectInfoList chosen_object_info_list) {
  JNIEnv* env = base::android::AttachCurrentThread();

  // Exit without permissions if it is an internal page.
  if (PageInfo::IsFileOrInternalPage(url_)) {
    Java_PageInfoController_updatePermissionDisplay(env, controller_jobject_);
    return;
  }

  // On Android, we only want to display a subset of the available options in
  // a particular order, but only if their value is different from the
  // default. This order comes from https://crbug.com/610358.
  std::vector<ContentSettingsType> permissions_to_display;
  permissions_to_display.push_back(
      content_settings::GeolocationContentSettingsType());
  permissions_to_display.push_back(ContentSettingsType::MEDIASTREAM_CAMERA);
  permissions_to_display.push_back(ContentSettingsType::MEDIASTREAM_MIC);
  permissions_to_display.push_back(ContentSettingsType::NOTIFICATIONS);
  permissions_to_display.push_back(ContentSettingsType::IDLE_DETECTION);
  permissions_to_display.push_back(ContentSettingsType::IMAGES);
  permissions_to_display.push_back(ContentSettingsType::JAVASCRIPT);
  permissions_to_display.push_back(ContentSettingsType::POPUPS);
  if (base::FeatureList::IsEnabled(
          permissions::features::kAndroidWindowManagementWebApi)) {
    permissions_to_display.push_back(ContentSettingsType::WINDOW_MANAGEMENT);
  }
  permissions_to_display.push_back(ContentSettingsType::ADS);
  permissions_to_display.push_back(
      ContentSettingsType::PROTECTED_MEDIA_IDENTIFIER);
  permissions_to_display.push_back(ContentSettingsType::SOUND);
  permissions_to_display.push_back(ContentSettingsType::NFC);
  base::CommandLine* cmd = base::CommandLine::ForCurrentProcess();
  permissions_to_display.push_back(
      ContentSettingsType::FILE_SYSTEM_WRITE_GUARD);
  permissions_to_display.push_back(ContentSettingsType::SERIAL_GUARD);
  if (cmd->HasSwitch(switches::kEnableExperimentalWebPlatformFeatures)) {
    permissions_to_display.push_back(ContentSettingsType::BLUETOOTH_SCANNING);
  }
  if (base::FeatureList::IsEnabled(blink::features::kWebHID)) {
    permissions_to_display.push_back(ContentSettingsType::HID_GUARD);
  }
  permissions_to_display.push_back(ContentSettingsType::VR);
  permissions_to_display.push_back(ContentSettingsType::AR);
#if BUILDFLAG(ENABLE_VR)
  if (device::features::IsHandTrackingEnabled()) {
    permissions_to_display.push_back(ContentSettingsType::HAND_TRACKING);
  }
#endif
  if (base::FeatureList::IsEnabled(features::kFedCm)) {
    permissions_to_display.push_back(
        ContentSettingsType::FEDERATED_IDENTITY_API);
  }
  if (base::FeatureList::IsEnabled(media::kAutoPictureInPictureAndroid)) {
    permissions_to_display.push_back(
        ContentSettingsType::AUTO_PICTURE_IN_PICTURE);
  }
  permissions_to_display.push_back(ContentSettingsType::STORAGE_ACCESS);
  if (base::FeatureList::IsEnabled(
          network::features::kLocalNetworkAccessChecks)) {
    permissions_to_display.push_back(ContentSettingsType::LOCAL_NETWORK);
    permissions_to_display.push_back(ContentSettingsType::LOOPBACK_NETWORK);
  }
  permissions_to_display.push_back(ContentSettingsType::SENSORS);

  std::map<ContentSettingsType, /*allowed*/ bool>
      user_specified_settings_to_display;

  // Whether the notifications permission is being requested. This is needed to
  // determine whether to show the permission in Page Info while or after it is
  // being requested. This is needed for the Clapper experiment
  // (crbug.com/458351800) and (crbug.com/463333225).
  bool requested_notifications = false;

  for (const auto& permission : permission_info_list) {
    if (std::ranges::contains(permissions_to_display, permission.type)) {
      std::optional<PermissionSetting> setting_to_display =
          GetSettingToDisplay(permission);
      if (setting_to_display) {
        auto* info =
            content_settings::PermissionSettingsRegistry::GetInstance()->Get(
                permission.type);

        user_specified_settings_to_display[permission.type] =
            info->delegate().IsAnyPermissionAllowed(*setting_to_display);
      }

      // It might be that we decided to show a Notification permission entry
      // even if the setting to display is DEFAULT. That must be either because
      // kPermanentNotificationSubscribeInPageInfo is enabled or because of the
      // Clapper experiment.
      if (permission.type == ContentSettingsType::NOTIFICATIONS) {
        if (permission.is_requested ||
            (setting_to_display.has_value() &&
             setting_to_display.value() == permission.default_setting)) {
          requested_notifications = true;
        }
      }
    }
  }

  for (const auto& permission : permissions_to_display) {
    if (user_specified_settings_to_display.contains(permission)) {
      std::u16string setting_title =
          PageInfoUI::PermissionTypeToUIString(permission);
      std::u16string setting_title_mid_sentence =
          PageInfoUI::PermissionTypeToUIStringMidSentence(permission);

      Java_PageInfoController_addPermissionSection(
          env, controller_jobject_,
          ConvertUTF16ToJavaString(env, setting_title),
          ConvertUTF16ToJavaString(env, setting_title_mid_sentence),
          static_cast<int32_t>(permission),
          user_specified_settings_to_display[permission],
          requested_notifications);
    }
  }

  for (const auto& chosen_object : chosen_object_info_list) {
    std::u16string object_title =
        presenter_->GetChooserContextFromUIInfo(*chosen_object->ui_info)
            ->GetObjectDisplayName(chosen_object->chooser_object->value);

    Java_PageInfoController_addPermissionSection(
        env, controller_jobject_, ConvertUTF16ToJavaString(env, object_title),
        ConvertUTF16ToJavaString(env, object_title),
        static_cast<int32_t>(chosen_object->ui_info->content_settings_type),
        static_cast<int32_t>(CONTENT_SETTING_ALLOW), requested_notifications);
  }

  Java_PageInfoController_updatePermissionDisplay(env, controller_jobject_);
}

std::optional<PermissionSetting> PageInfoControllerAndroid::GetSettingToDisplay(
    const PageInfo::PermissionInfo& permission) {
  // All permissions should be displayed if they are non-default.
  if (permission.setting && permission.setting != permission.default_setting) {
    return permission.setting;
  }

  // Handle exceptions for permissions which need to be displayed even if they
  // are set to the default.
  if (permission.type == ContentSettingsType::ADS) {
    // The subresource filter permission should always display the default
    // setting if it is showing up in Page Info. Logic for whether the
    // setting should show up in Page Info is in ShouldShowPermission in
    // page_info.cc.
    return permission.default_setting;
  } else if (permission.type == ContentSettingsType::JAVASCRIPT) {
    // The javascript content setting should show up if it is blocked globally
    // to give users an easy way to create exceptions.
    return permission.default_setting;
  } else if (permission.type == ContentSettingsType::NOTIFICATIONS) {
    // Notifications permission should be displayed while it is being requested.
    return permission.default_setting;
  } else if (permission.type == ContentSettingsType::SOUND) {
    // The sound content setting should always show up when the tab has played
    // audio since last navigation.
    if (web_contents_->WasEverAudible())
      return permission.default_setting;
  } else if (permission.type == ContentSettingsType::FILE_SYSTEM_WRITE_GUARD) {
    // The file editing permission should show up if there are any open files.
    return permission.default_setting;
  } else if (permission.type == ContentSettingsType::AUTO_PICTURE_IN_PICTURE) {
    // The auto-pip permission should always display the default setting if it
    // is showing up in Page Info before it's set by the user. Logic for whether
    // the setting should show up in Page Info is in ShouldShowPermission in
    // page_info.cc.
    return permission.default_setting;
  }

  // TODO(crbug.com/40129299): Also return permissions that are non
  // factory-default after we add the functionality to populate the permissions
  // subpage directly from the permissions returned from this controller.

  return std::nullopt;
}

DEFINE_JNI(PageInfoController)
