// Copyright 2021 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/ui/android/webid/account_selection_view_android.h"
#include <memory>
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/ui/webid/account_selection_view.h"
#include "content/public/browser/identity_request_dialog_controller.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom-shared.h"
#include "third_party/blink/public/mojom/webid/federated_auth_request.mojom.h"
#include "ui/android/color_utils_android.h"
#include "ui/android/window_android.h"
#include "ui/gfx/android/java_bitmap.h"
#include "url/android/gurl_android.h"
#include "url/gurl.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/browser/ui/android/webid/internal/jni/AccountSelectionBridge_jni.h"
#include "chrome/browser/ui/android/webid/jni_headers/Account_jni.h"
#include "chrome/browser/ui/android/webid/jni_headers/ClientIdMetadata_jni.h"
#include "chrome/browser/ui/android/webid/jni_headers/IdentityCredentialTokenError_jni.h"
#include "chrome/browser/ui/android/webid/jni_headers/IdentityProviderData_jni.h"
#include "chrome/browser/ui/android/webid/jni_headers/IdentityProviderMetadata_jni.h"
using base::android::AppendJavaStringArrayToStringVector;
using base::android::AttachCurrentThread;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
using DismissReason = content::IdentityRequestDialogController::DismissReason;
namespace {
ScopedJavaLocalRef<jobject> ConvertToJavaAccount(JNIEnv* env,
const Account& account) {
ScopedJavaLocalRef<jobject> decoded_picture = nullptr;
if (!account.decoded_picture.IsEmpty()) {
decoded_picture =
gfx::ConvertToJavaBitmap(*account.decoded_picture.ToSkBitmap());
}
return Java_Account_Constructor(
env, ConvertUTF8ToJavaString(env, account.id),
ConvertUTF8ToJavaString(env, account.email),
ConvertUTF8ToJavaString(env, account.name),
ConvertUTF8ToJavaString(env, account.given_name),
url::GURLAndroid::FromNativeGURL(env, account.picture), decoded_picture,
account.login_state == Account::LoginState::kSignIn,
account.browser_trusted_login_state == Account::LoginState::kSignIn);
}
ScopedJavaLocalRef<jobject> ConvertToJavaIdentityProviderMetadata(
JNIEnv* env,
const content::IdentityProviderMetadata& metadata) {
ScopedJavaLocalRef<jstring> java_brand_icon_url =
base::android::ConvertUTF8ToJavaString(env,
metadata.brand_icon_url.spec());
return Java_IdentityProviderMetadata_Constructor(
env, ui::OptionalSkColorToJavaColor(metadata.brand_text_color),
ui::OptionalSkColorToJavaColor(metadata.brand_background_color),
java_brand_icon_url,
url::GURLAndroid::FromNativeGURL(env, metadata.config_url),
url::GURLAndroid::FromNativeGURL(env, metadata.idp_login_url),
metadata.supports_add_account);
}
ScopedJavaLocalRef<jobject> ConvertToJavaIdentityCredentialTokenError(
JNIEnv* env,
const std::optional<TokenError>& error) {
return Java_IdentityCredentialTokenError_Constructor(
env,
base::android::ConvertUTF8ToJavaString(env, error ? error->code : ""),
url::GURLAndroid::FromNativeGURL(env, error ? error->url : GURL()));
}
ScopedJavaLocalRef<jobject> ConvertToJavaClientIdMetadata(
JNIEnv* env,
const content::ClientMetadata& metadata) {
ScopedJavaLocalRef<jstring> java_brand_icon_url =
base::android::ConvertUTF8ToJavaString(env,
metadata.brand_icon_url.spec());
return Java_ClientIdMetadata_Constructor(
env, url::GURLAndroid::FromNativeGURL(env, metadata.terms_of_service_url),
url::GURLAndroid::FromNativeGURL(env, metadata.privacy_policy_url),
java_brand_icon_url);
}
ScopedJavaLocalRef<jobjectArray> ConvertToJavaAccounts(
JNIEnv* env,
const std::vector<Account>& accounts) {
ScopedJavaLocalRef<jclass> account_clazz = base::android::GetClass(
env, "org/chromium/chrome/browser/ui/android/webid/data/Account");
ScopedJavaLocalRef<jobjectArray> array(
env, env->NewObjectArray(accounts.size(), account_clazz.obj(), nullptr));
base::android::CheckException(env);
for (size_t i = 0; i < accounts.size(); ++i) {
ScopedJavaLocalRef<jobject> item = ConvertToJavaAccount(env, accounts[i]);
env->SetObjectArrayElement(array.obj(), i, item.obj());
}
return array;
}
ScopedJavaLocalRef<jobject> ConvertToJavaIdentityProviderData(
JNIEnv* env,
const std::optional<content::IdentityProviderData>& new_accounts_idp) {
if (!new_accounts_idp) {
return ScopedJavaLocalRef<jobject>(env, nullptr);
}
return Java_IdentityProviderData_Constructor(
env, new_accounts_idp->idp_for_display,
ConvertToJavaAccounts(env, new_accounts_idp->accounts),
ConvertToJavaIdentityProviderMetadata(env,
new_accounts_idp->idp_metadata),
ConvertToJavaClientIdMetadata(env, new_accounts_idp->client_metadata),
static_cast<jint>(new_accounts_idp->rp_context),
new_accounts_idp->request_permission,
new_accounts_idp->has_login_status_mismatch);
}
Account ConvertFieldsToAccount(JNIEnv* env,
const std::vector<std::string>& string_fields,
const GURL& picture_url,
bool is_sign_in) {
auto account_id = string_fields[0];
auto email = string_fields[1];
auto name = string_fields[2];
auto given_name = string_fields[3];
Account::LoginState login_state =
is_sign_in ? Account::LoginState::kSignIn : Account::LoginState::kSignUp;
// The following fields are only used before account selection.
std::vector<std::string> login_hints;
std::vector<std::string> domain_hints;
std::vector<std::string> labels;
Account::LoginState browser_trusted_login_state =
Account::LoginState::kSignUp;
return Account(account_id, email, name, given_name, picture_url,
std::move(login_hints), std::move(domain_hints),
std::move(labels), login_state, browser_trusted_login_state);
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class FedCmJavaObjectCreationOutcome {
kNewObjectCreated = 0,
kObjectReused = 1,
kObjectCreationFailed = 2,
kNoNativeView = 3,
kNoWindow = 4,
kMaxValue = kNoWindow
};
void RecordJavaObjectCreationOutcome(
std::optional<blink::mojom::RpMode> rp_mode,
FedCmJavaObjectCreationOutcome outcome) {
// Rp mode may be unavailable in cases that the request is invoked from CCT.
// There's no need to record metrics in such case.
if (!rp_mode) {
return;
}
const char* mode =
*rp_mode == blink::mojom::RpMode::kWidget ? "Widget" : "Button";
base::UmaHistogramEnumeration(
base::StringPrintf("Blink.FedCm.JavaObjectCreationOutcome.%s", mode),
outcome);
}
} // namespace
AccountSelectionViewAndroid::AccountSelectionViewAndroid(
AccountSelectionView::Delegate* delegate)
: AccountSelectionView(delegate) {}
AccountSelectionViewAndroid::~AccountSelectionViewAndroid() {
if (java_object_internal_) {
// Don't create an object just for destruction.
Java_AccountSelectionBridge_destroy(AttachCurrentThread(),
java_object_internal_);
}
}
bool AccountSelectionViewAndroid::Show(
const std::string& rp_for_display,
const std::vector<content::IdentityProviderData>& identity_provider_data,
Account::SignInMode sign_in_mode,
blink::mojom::RpMode rp_mode,
const std::optional<content::IdentityProviderData>& new_account_idp) {
if (!MaybeCreateJavaObject(rp_mode)) {
// It's possible that the constructor cannot access the bottom sheet clank
// component. That case may be temporary but we can't let users in a
// waiting state so report that AccountSelectionView is dismissed instead.
delegate_->OnDismiss(DismissReason::kOther);
return false;
}
// Serialize the `identity_provider_data.accounts` into a Java array and
// instruct the bridge to show it together with |url| to the user.
JNIEnv* env = AttachCurrentThread();
// Multi IDP support does not currently work on mobile. Hence, we use the
// first index from the `identity_provider_data` for the IDP-specific
// information.
ScopedJavaLocalRef<jobjectArray> accounts_obj =
ConvertToJavaAccounts(env, identity_provider_data[0].accounts);
ScopedJavaLocalRef<jobject> idp_metadata_obj =
ConvertToJavaIdentityProviderMetadata(
env, identity_provider_data[0].idp_metadata);
ScopedJavaLocalRef<jobject> client_id_metadata_obj =
ConvertToJavaClientIdMetadata(env,
identity_provider_data[0].client_metadata);
ScopedJavaLocalRef<jobject> new_account_idp_obj =
ConvertToJavaIdentityProviderData(env, new_account_idp);
// TODO(crbug.com/329235198): Support auto re-authn on Android.
Java_AccountSelectionBridge_showAccounts(
env, java_object_internal_, rp_for_display,
identity_provider_data[0].idp_for_display, accounts_obj, idp_metadata_obj,
client_id_metadata_obj,
sign_in_mode == Account::SignInMode::kAuto &&
rp_mode == blink::mojom::RpMode::kWidget,
static_cast<jint>(identity_provider_data[0].rp_context),
identity_provider_data[0].request_permission, new_account_idp_obj);
return true;
}
bool AccountSelectionViewAndroid::ShowFailureDialog(
const std::string& rp_for_display,
const std::string& idp_for_display,
blink::mojom::RpContext rp_context,
blink::mojom::RpMode rp_mode,
const content::IdentityProviderMetadata& idp_metadata) {
// ShowFailureDialog is never called in button mode.
// TODO(crbug.com/347736746): Remove rp_mode from this method.
CHECK(rp_mode == blink::mojom::RpMode::kWidget);
if (!MaybeCreateJavaObject(rp_mode)) {
// It's possible that the constructor cannot access the bottom sheet clank
// component. That case may be temporary but we can't let users in a
// waiting state so report that AccountSelectionView is dismissed instead.
delegate_->OnDismiss(DismissReason::kOther);
return false;
}
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> idp_metadata_obj =
ConvertToJavaIdentityProviderMetadata(env, idp_metadata);
Java_AccountSelectionBridge_showFailureDialog(
env, java_object_internal_, rp_for_display, idp_for_display,
idp_metadata_obj, static_cast<jint>(rp_context));
return true;
}
bool AccountSelectionViewAndroid::ShowErrorDialog(
const std::string& rp_for_display,
const std::string& idp_for_display,
blink::mojom::RpContext rp_context,
blink::mojom::RpMode rp_mode,
const content::IdentityProviderMetadata& idp_metadata,
const std::optional<TokenError>& error) {
// TODO(crbug.com/347117752): Implement button mode error dialog.
if (rp_mode == blink::mojom::RpMode::kButton ||
!MaybeCreateJavaObject(rp_mode)) {
// It's possible that the constructor cannot access the bottom sheet clank
// component. That case may be temporary but we can't let users in a
// waiting state so report that AccountSelectionView is dismissed instead.
delegate_->OnDismiss(DismissReason::kOther);
return false;
}
JNIEnv* env = AttachCurrentThread();
ScopedJavaLocalRef<jobject> idp_metadata_obj =
ConvertToJavaIdentityProviderMetadata(env, idp_metadata);
Java_AccountSelectionBridge_showErrorDialog(
env, java_object_internal_, rp_for_display, idp_for_display,
idp_metadata_obj, static_cast<jint>(rp_context),
ConvertToJavaIdentityCredentialTokenError(env, error));
return true;
}
bool AccountSelectionViewAndroid::ShowLoadingDialog(
const std::string& rp_for_display,
const std::string& idp_for_display,
blink::mojom::RpContext rp_context,
blink::mojom::RpMode rp_mode) {
if (!MaybeCreateJavaObject(rp_mode)) {
// It's possible that the constructor cannot access the bottom sheet clank
// component. That case may be temporary but we can't let users in a
// waiting state so report that AccountSelectionView is dismissed instead.
delegate_->OnDismiss(DismissReason::kOther);
return false;
}
JNIEnv* env = AttachCurrentThread();
Java_AccountSelectionBridge_showLoadingDialog(env, java_object_internal_,
rp_for_display, idp_for_display,
static_cast<jint>(rp_context));
return true;
}
std::string AccountSelectionViewAndroid::GetTitle() const {
JNIEnv* env = AttachCurrentThread();
return Java_AccountSelectionBridge_getTitle(env, java_object_internal_);
}
std::optional<std::string> AccountSelectionViewAndroid::GetSubtitle() const {
JNIEnv* env = AttachCurrentThread();
return Java_AccountSelectionBridge_getSubtitle(env, java_object_internal_);
}
void AccountSelectionViewAndroid::ShowUrl(LinkType link_type, const GURL& url) {
JNIEnv* env = AttachCurrentThread();
Java_AccountSelectionBridge_showUrl(env, java_object_internal_,
static_cast<int>(link_type), url);
}
content::WebContents* AccountSelectionViewAndroid::ShowModalDialog(
const GURL& url,
blink::mojom::RpMode rp_mode) {
if (!MaybeCreateJavaObject(rp_mode)) {
// The Java object is tied to the bottomsheet availability, so if we hadn't
// created one and the bottomsheet is not available then the CCT will not be
// opened.
delegate_->OnDismiss(DismissReason::kOther);
return nullptr;
}
JNIEnv* env = AttachCurrentThread();
return content::WebContents::FromJavaWebContents(
Java_AccountSelectionBridge_showModalDialog(env, java_object_internal_,
url));
}
void AccountSelectionViewAndroid::CloseModalDialog() {
// Since this is triggered only after the CCT is opened, leaving it out of the metrics
// to focus on cases where a UI cannot be displayed.
if (!MaybeCreateJavaObject(/*rp_mode=*/std::nullopt)) {
return;
}
JNIEnv* env = AttachCurrentThread();
Java_AccountSelectionBridge_closeModalDialog(env, java_object_internal_);
}
content::WebContents* AccountSelectionViewAndroid::GetRpWebContents() {
// The Java object needs to be recreated, as this is invoked for the
// CCT. Rp mode isn't meaningful in this case so we don't pass it for metrics.
if (!MaybeCreateJavaObject(/*rp_mode=*/std::nullopt)) {
return nullptr;
}
JNIEnv* env = AttachCurrentThread();
return content::WebContents::FromJavaWebContents(
Java_AccountSelectionBridge_getRpWebContents(env, java_object_internal_));
}
void AccountSelectionViewAndroid::OnAccountSelected(
JNIEnv* env,
const GURL& idp_config_url,
const std::vector<std::string>& account_string_fields,
const GURL& account_picture_url,
bool is_sign_in) {
delegate_->OnAccountSelected(
idp_config_url, ConvertFieldsToAccount(env, account_string_fields,
account_picture_url, is_sign_in));
// The AccountSelectionViewAndroid may be destroyed.
// AccountSelectionView::Delegate::OnAccountSelected() might delete this.
// See https://crbug.com/1393650 for details.
}
void AccountSelectionViewAndroid::OnDismiss(JNIEnv* env, jint dismiss_reason) {
delegate_->OnDismiss(static_cast<DismissReason>(dismiss_reason));
}
void AccountSelectionViewAndroid::OnLoginToIdP(JNIEnv* env,
const GURL& idp_config_url,
const GURL& idp_login_url) {
delegate_->OnLoginToIdP(idp_config_url, idp_login_url);
}
void AccountSelectionViewAndroid::OnMoreDetails(JNIEnv* env) {
delegate_->OnMoreDetails();
}
void AccountSelectionViewAndroid::OnAccountsDisplayed(JNIEnv* env) {
delegate_->OnAccountsDisplayed();
}
bool AccountSelectionViewAndroid::MaybeCreateJavaObject(
std::optional<blink::mojom::RpMode> rp_mode) {
if (!delegate_->GetNativeView()) {
RecordJavaObjectCreationOutcome(
rp_mode, FedCmJavaObjectCreationOutcome::kNoNativeView);
return false;
}
if (!delegate_->GetNativeView()->GetWindowAndroid()) {
RecordJavaObjectCreationOutcome(rp_mode,
FedCmJavaObjectCreationOutcome::kNoWindow);
return false; // No window attached (yet or anymore).
}
if (java_object_internal_) {
RecordJavaObjectCreationOutcome(
rp_mode, FedCmJavaObjectCreationOutcome::kObjectReused);
return true;
}
JNIEnv* env = AttachCurrentThread();
java_object_internal_ = Java_AccountSelectionBridge_create(
env, reinterpret_cast<intptr_t>(this),
delegate_->GetWebContents()->GetJavaWebContents(),
delegate_->GetNativeView()->GetWindowAndroid()->GetJavaObject(),
static_cast<jint>(rp_mode.value_or(blink::mojom::RpMode::kWidget)));
if (!!java_object_internal_) {
RecordJavaObjectCreationOutcome(
rp_mode, FedCmJavaObjectCreationOutcome::kNewObjectCreated);
} else {
RecordJavaObjectCreationOutcome(
rp_mode, FedCmJavaObjectCreationOutcome::kObjectCreationFailed);
}
return !!java_object_internal_;
}
// static
std::unique_ptr<AccountSelectionView> AccountSelectionView::Create(
AccountSelectionView::Delegate* delegate) {
return std::make_unique<AccountSelectionViewAndroid>(delegate);
}
// static
int AccountSelectionView::GetBrandIconMinimumSize(
blink::mojom::RpMode rp_mode) {
return Java_AccountSelectionBridge_getBrandIconMinimumSize(
base::android::AttachCurrentThread(), static_cast<jint>(rp_mode));
}
// static
int AccountSelectionView::GetBrandIconIdealSize(blink::mojom::RpMode rp_mode) {
return Java_AccountSelectionBridge_getBrandIconIdealSize(
base::android::AttachCurrentThread(), static_cast<jint>(rp_mode));
}