Commit 15c22fa8 authored by Rouslan Solomakhin's avatar Rouslan Solomakhin Committed by Commit Bot

[Web Payment][Chrome OS] Android app communication - GetAppDescriptions.

Before this patch, Android payment app implementation lived exclusively
in Java, so sharing any of its logic across platforms was difficult.

This patch adds a cross-platform way for Web Payment feature to query a
list of installed Android apps. As a first step, only information about
Trusted Web Activities installed in the Android subsystem of Chrome OS
would be returned.

After this patch, it's possible to start creating a cross-platform
implementation of an Android payment app.

Bug: 1061503
Change-Id: Ic8320a902eec537d5aa37b3a6cf9b79b8c579c55
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2242639
Commit-Queue: Rouslan Solomakhin <rouslan@chromium.org>
Reviewed-by: default avatarSahel Sharify <sahel@chromium.org>
Cr-Commit-Position: refs/heads/master@{#797883}
parent 1b47a516
......@@ -4,6 +4,8 @@
static_library("content") {
sources = [
"android_app_communication.cc",
"android_app_communication.h",
"autofill_payment_app.cc",
"autofill_payment_app.h",
"autofill_payment_app_factory.cc",
......@@ -55,6 +57,19 @@ static_library("content") {
"//url",
]
if (is_chromeos) {
sources += [ "android_app_communication_chrome_os.cc" ]
deps += [
"//components/arc",
"//components/arc/mojom",
]
}
if (!is_chromeos) {
sources += [ "android_app_communication_stub.cc" ]
}
if (!is_android) {
sources += [
"content_payment_request_delegate.h",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/content/android_app_communication.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
namespace payments {
namespace {
const char kAndroidAppCommunicationKeyName[] =
"payment_android_app_communication";
} // namespace
// static
base::WeakPtr<AndroidAppCommunication>
AndroidAppCommunication::GetForBrowserContext(
content::BrowserContext* context) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(context);
base::SupportsUserData::Data* data =
context->GetUserData(kAndroidAppCommunicationKeyName);
if (!data) {
auto communication = AndroidAppCommunication::Create(context);
data = communication.get();
context->SetUserData(kAndroidAppCommunicationKeyName,
std::move(communication));
}
return static_cast<AndroidAppCommunication*>(data)
->weak_ptr_factory_.GetWeakPtr();
}
AndroidAppCommunication::~AndroidAppCommunication() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
AndroidAppCommunication::AndroidAppCommunication(
content::BrowserContext* context)
: context_(context) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(context_);
}
} // namespace payments
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_H_
#define COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_H_
#include <memory>
#include <string>
#include <vector>
#include "base/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/supports_user_data.h"
#include "components/payments/core/android_app_description.h"
namespace content {
class BrowserContext;
} // namespace content
namespace payments {
// Invokes Android payment apps. This object is owned by BrowserContext, so it
// should only be accessed on UI thread, where BrowserContext lives.
class AndroidAppCommunication : public base::SupportsUserData::Data {
public:
using GetAppDescriptionsCallback = base::OnceCallback<void(
const base::Optional<std::string>& error_message,
std::vector<std::unique_ptr<AndroidAppDescription>> app_descriptions)>;
// Returns a weak pointer to the instance of AndroidAppCommunication that is
// owned by the given |context|, which should not be null.
static base::WeakPtr<AndroidAppCommunication> GetForBrowserContext(
content::BrowserContext* context);
~AndroidAppCommunication() override;
// Disallow copy and assign.
AndroidAppCommunication(const AndroidAppCommunication& other) = delete;
AndroidAppCommunication& operator=(const AndroidAppCommunication& other) =
delete;
// Looks up installed Android apps that support making payments. If running in
// TWA mode, the |twa_package_name| parameter is the name of the Android
// package of the TWA that invoked Chrome, or an empty string otherwise.
virtual void GetAppDescriptions(const std::string& twa_package_name,
GetAppDescriptionsCallback callback) = 0;
// Enables the testing mode.
virtual void SetForTesting() = 0;
protected:
explicit AndroidAppCommunication(content::BrowserContext* context);
content::BrowserContext* context() { return context_; }
private:
// Defined in platform-specific implementation files. See:
// components/payments/content/android_app_communication_chromeos.cc
// components/payments/content/android_app_communication_stub.cc
static std::unique_ptr<AndroidAppCommunication> Create(
content::BrowserContext* context);
// Owns this object, so always valid.
content::BrowserContext* context_;
base::WeakPtrFactory<AndroidAppCommunication> weak_ptr_factory_{this};
};
} // namespace payments
#endif // COMPONENTS_PAYMENTS_CONTENT_ANDROID_APP_COMMUNICATION_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/content/android_app_communication.h"
#include <utility>
#include "components/arc/mojom/payment_app.mojom.h"
#include "components/arc/pay/arc_payment_app_bridge.h"
#include "components/payments/core/android_app_description.h"
#include "components/payments/core/chrome_os_error_strings.h"
#include "components/payments/core/method_strings.h"
#include "components/payments/core/native_error_strings.h"
#include "content/public/browser/browser_thread.h"
namespace payments {
namespace {
void OnIsImplemented(
const std::string& twa_package_name,
AndroidAppCommunication::GetAppDescriptionsCallback callback,
arc::mojom::IsPaymentImplementedResultPtr response) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(!twa_package_name.empty());
if (response.is_null()) {
std::move(callback).Run(errors::kEmptyResponse, /*app_descriptions=*/{});
return;
}
if (response->is_error()) {
std::move(callback).Run(response->get_error(), /*app_descriptions=*/{});
return;
}
if (!response->is_valid()) {
std::move(callback).Run(errors::kInvalidResponse, /*app_descriptions=*/{});
return;
}
if (response->get_valid()->activity_names.empty()) {
// If a TWA does not implement PAY intent in any of its activities, then
// |activity_names| is empty, which is not an error.
std::move(callback).Run(/*error_message=*/base::nullopt,
/*app_descriptions=*/{});
return;
}
if (response->get_valid()->activity_names.size() != 1U) {
std::move(callback).Run(errors::kMoreThanOneActivity,
/*app_descriptions=*/{});
return;
}
if (response->get_valid()->service_names.size() > 1U) {
std::move(callback).Run(errors::kMoreThanOneService,
/*app_descriptions=*/{});
return;
}
auto activity = std::make_unique<AndroidActivityDescription>();
activity->name = response->get_valid()->activity_names.front();
// The only available payment method identifier in a Chrome OS TWA at this
// time.
activity->default_payment_method = methods::kGooglePlayBilling;
auto app = std::make_unique<AndroidAppDescription>();
app->package = twa_package_name;
app->activities.emplace_back(std::move(activity));
app->service_names = response->get_valid()->service_names;
std::vector<std::unique_ptr<AndroidAppDescription>> app_descriptions;
app_descriptions.emplace_back(std::move(app));
std::move(callback).Run(/*error_message=*/base::nullopt,
std::move(app_descriptions));
}
// Invokes the TWA Android app in Android subsystem on Chrome OS.
class AndroidAppCommunicationChromeOS : public AndroidAppCommunication {
public:
explicit AndroidAppCommunicationChromeOS(content::BrowserContext* context)
: AndroidAppCommunication(context),
get_app_service_(base::BindRepeating(
&arc::ArcPaymentAppBridge::GetForBrowserContext)) {}
~AndroidAppCommunicationChromeOS() override = default;
// Disallow copy and assign.
AndroidAppCommunicationChromeOS(
const AndroidAppCommunicationChromeOS& other) = delete;
AndroidAppCommunicationChromeOS& operator=(
const AndroidAppCommunicationChromeOS& other) = delete;
// AndroidAppCommunication implementation:
void GetAppDescriptions(const std::string& twa_package_name,
GetAppDescriptionsCallback callback) override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (twa_package_name.empty()) {
// Chrome OS supports Android app payment only through a TWA. An empty
// |twa_package_name| indicates that Chrome was not launched from a TWA,
// so there're no payment apps available.
std::move(callback).Run(/*error_message=*/base::nullopt,
/*app_descriptions=*/{});
return;
}
auto* payment_app_service = get_app_service_.Run(context());
if (!payment_app_service) {
std::move(callback).Run(errors::kUnableToInvokeAndroidPaymentApps,
/*app_descriptions=*/{});
return;
}
payment_app_service->IsPaymentImplemented(
twa_package_name, base::BindOnce(&OnIsImplemented, twa_package_name,
std::move(callback)));
}
// AndroidAppCommunication implementation:
void SetForTesting() override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
get_app_service_ = base::BindRepeating(
&arc::ArcPaymentAppBridge::GetForBrowserContextForTesting);
}
private:
base::RepeatingCallback<arc::ArcPaymentAppBridge*(content::BrowserContext*)>
get_app_service_;
};
} // namespace
// Declared in cross-platform header file. See:
// //components/payments/content/android_app_communication.h
// static
std::unique_ptr<AndroidAppCommunication> AndroidAppCommunication::Create(
content::BrowserContext* context) {
return std::make_unique<AndroidAppCommunicationChromeOS>(context);
}
} // namespace payments
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/content/android_app_communication.h"
#include <utility>
#include "base/callback.h"
#include "base/optional.h"
namespace payments {
namespace {
class AndroidAppCommunicationStub : public AndroidAppCommunication {
public:
explicit AndroidAppCommunicationStub(content::BrowserContext* context)
: AndroidAppCommunication(context) {}
~AndroidAppCommunicationStub() override = default;
// AndroidAppCommunication implementation.
void GetAppDescriptions(const std::string& twa_package_name,
GetAppDescriptionsCallback callback) override {
std::move(callback).Run(/*error_message=*/base::nullopt,
/*app_descriptions=*/{});
}
// AndroidAppCommunication implementation.
void SetForTesting() override {}
};
} // namespace
// Declared in cross-platform header file. See:
// components/payments/content/android_app_communication.h
// static
std::unique_ptr<AndroidAppCommunication> AndroidAppCommunication::Create(
content::BrowserContext* context) {
return std::make_unique<AndroidAppCommunicationStub>(context);
}
} // namespace payments
......@@ -2,18 +2,252 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/content/android_app_communication.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback.h"
#include "base/optional.h"
#include "components/payments/content/android_app_communication_test_support.h"
#include "components/payments/core/android_app_description.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace payments {
namespace {
TEST(AndroidAppCommunicationTest, SmokeTest) {
auto support = AndroidAppCommunicationTestSupport::Create();
auto scoped_initialization = support->CreateScopedInitialization();
support->ExpectNoListOfPaymentAppsQuery();
support->ExpectNoIsReadyToPayQuery();
support->ExpectNoPaymentAppInvoke();
std::vector<std::unique_ptr<AndroidAppDescription>> createApp(
const std::vector<std::string>& activity_names,
const std::string& default_payment_method,
const std::vector<std::string>& service_names) {
auto app = std::make_unique<AndroidAppDescription>();
for (const auto& activity_name : activity_names) {
auto activity = std::make_unique<AndroidActivityDescription>();
activity->name = activity_name;
activity->default_payment_method = default_payment_method;
app->activities.emplace_back(std::move(activity));
}
app->package = "com.example.app";
app->service_names = service_names;
std::vector<std::unique_ptr<AndroidAppDescription>> apps;
apps.emplace_back(std::move(app));
return apps;
}
class AndroidAppCommunicationTest : public testing::Test {
public:
AndroidAppCommunicationTest()
: support_(AndroidAppCommunicationTestSupport::Create()) {}
~AndroidAppCommunicationTest() override = default;
AndroidAppCommunicationTest(const AndroidAppCommunicationTest& other) =
delete;
AndroidAppCommunicationTest& operator=(
const AndroidAppCommunicationTest& other) = delete;
void OnGetAppDescriptionsResponse(
const base::Optional<std::string>& error,
std::vector<std::unique_ptr<AndroidAppDescription>> apps) {
error_ = error;
apps_ = std::move(apps);
}
std::unique_ptr<AndroidAppCommunicationTestSupport> support_;
base::Optional<std::string> error_;
std::vector<std::unique_ptr<AndroidAppDescription>> apps_;
};
TEST_F(AndroidAppCommunicationTest, OneInstancePerBrowserContext) {
auto communication_one =
AndroidAppCommunication::GetForBrowserContext(support_->context());
auto communication_two =
AndroidAppCommunication::GetForBrowserContext(support_->context());
EXPECT_EQ(communication_one.get(), communication_two.get());
}
TEST_F(AndroidAppCommunicationTest, NoArcForGetAppDescriptions) {
// Intentionally do not set an instance.
support_->ExpectNoListOfPaymentAppsQuery();
auto communication =
AndroidAppCommunication::GetForBrowserContext(support_->context());
communication->SetForTesting();
communication->GetAppDescriptions(
"com.example.app",
base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse,
base::Unretained(this)));
if (support_->AreAndroidAppsSupportedOnThisPlatform()) {
ASSERT_TRUE(error_.has_value());
EXPECT_EQ("Unable to invoke Android apps.", error_.value());
} else {
EXPECT_FALSE(error_.has_value());
}
EXPECT_TRUE(apps_.empty());
}
TEST_F(AndroidAppCommunicationTest, NoAppDescriptions) {
auto scoped_initialization = support_->CreateScopedInitialization();
support_->ExpectQueryListOfPaymentAppsAndRespond({});
auto communication =
AndroidAppCommunication::GetForBrowserContext(support_->context());
communication->SetForTesting();
communication->GetAppDescriptions(
"com.example.app",
base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse,
base::Unretained(this)));
EXPECT_FALSE(error_.has_value());
EXPECT_TRUE(apps_.empty());
}
TEST_F(AndroidAppCommunicationTest, TwoActivitiesInPackage) {
auto scoped_initialization = support_->CreateScopedInitialization();
support_->ExpectQueryListOfPaymentAppsAndRespond(
createApp({"com.example.app.ActivityOne", "com.example.app.ActivityTwo"},
"https://play.google.com/billing", {}));
auto communication =
AndroidAppCommunication::GetForBrowserContext(support_->context());
communication->SetForTesting();
communication->GetAppDescriptions(
"com.example.app",
base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse,
base::Unretained(this)));
if (support_->AreAndroidAppsSupportedOnThisPlatform()) {
ASSERT_TRUE(error_.has_value());
EXPECT_EQ(
"Found more than one PAY activity in the Trusted Web Activity, but at "
"most one activity is supported.",
error_.value());
} else {
EXPECT_FALSE(error_.has_value());
}
EXPECT_TRUE(apps_.empty());
}
TEST_F(AndroidAppCommunicationTest, TwoServicesInPackage) {
auto scoped_initialization = support_->CreateScopedInitialization();
support_->ExpectQueryListOfPaymentAppsAndRespond(
createApp({"com.example.app.Activity"}, "https://play.google.com/billing",
{"com.example.app.ServiceOne", "com.example.app.ServiceTwo"}));
auto communication =
AndroidAppCommunication::GetForBrowserContext(support_->context());
communication->SetForTesting();
communication->GetAppDescriptions(
"com.example.app",
base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse,
base::Unretained(this)));
if (support_->AreAndroidAppsSupportedOnThisPlatform()) {
ASSERT_TRUE(error_.has_value());
EXPECT_EQ(
"Found more than one IS_READY_TO_PAY service in the Trusted Web "
"Activity, but at most one service is supported.",
error_.value());
} else {
EXPECT_FALSE(error_.has_value());
}
EXPECT_TRUE(apps_.empty());
}
TEST_F(AndroidAppCommunicationTest, ActivityAndService) {
auto scoped_initialization = support_->CreateScopedInitialization();
support_->ExpectQueryListOfPaymentAppsAndRespond(
createApp({"com.example.app.Activity"}, "https://play.google.com/billing",
{"com.example.app.Service"}));
auto communication =
AndroidAppCommunication::GetForBrowserContext(support_->context());
communication->SetForTesting();
communication->GetAppDescriptions(
"com.example.app",
base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse,
base::Unretained(this)));
EXPECT_FALSE(error_.has_value());
if (support_->AreAndroidAppsSupportedOnThisPlatform()) {
ASSERT_EQ(1u, apps_.size());
ASSERT_NE(nullptr, apps_.front().get());
EXPECT_EQ("com.example.app", apps_.front()->package);
EXPECT_EQ(std::vector<std::string>{"com.example.app.Service"},
apps_.front()->service_names);
ASSERT_EQ(1u, apps_.front()->activities.size());
ASSERT_NE(nullptr, apps_.front()->activities.front().get());
EXPECT_EQ("com.example.app.Activity",
apps_.front()->activities.front()->name);
EXPECT_EQ("https://play.google.com/billing",
apps_.front()->activities.front()->default_payment_method);
} else {
EXPECT_TRUE(apps_.empty());
}
}
TEST_F(AndroidAppCommunicationTest, OnlyActivity) {
auto scoped_initialization = support_->CreateScopedInitialization();
support_->ExpectQueryListOfPaymentAppsAndRespond(createApp(
{"com.example.app.Activity"}, "https://play.google.com/billing", {}));
auto communication =
AndroidAppCommunication::GetForBrowserContext(support_->context());
communication->SetForTesting();
communication->GetAppDescriptions(
"com.example.app",
base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse,
base::Unretained(this)));
EXPECT_FALSE(error_.has_value());
if (support_->AreAndroidAppsSupportedOnThisPlatform()) {
ASSERT_EQ(1u, apps_.size());
ASSERT_NE(nullptr, apps_.front().get());
EXPECT_EQ("com.example.app", apps_.front()->package);
EXPECT_TRUE(apps_.front()->service_names.empty());
ASSERT_EQ(1u, apps_.front()->activities.size());
ASSERT_NE(nullptr, apps_.front()->activities.front().get());
EXPECT_EQ("com.example.app.Activity",
apps_.front()->activities.front()->name);
EXPECT_EQ("https://play.google.com/billing",
apps_.front()->activities.front()->default_payment_method);
} else {
EXPECT_TRUE(apps_.empty());
}
}
TEST_F(AndroidAppCommunicationTest, OutsideOfTwa) {
auto scoped_initialization = support_->CreateScopedInitialization();
support_->ExpectNoListOfPaymentAppsQuery();
auto communication =
AndroidAppCommunication::GetForBrowserContext(support_->context());
communication->SetForTesting();
communication->GetAppDescriptions(
/*twa_package_name=*/"", // Empty string means this is not TWA.
base::BindOnce(&AndroidAppCommunicationTest::OnGetAppDescriptionsResponse,
base::Unretained(this)));
EXPECT_FALSE(error_.has_value());
EXPECT_TRUE(apps_.empty());
}
} // namespace
......
......@@ -106,6 +106,13 @@ static_library("error_strings") {
"native_error_strings.cc",
"native_error_strings.h",
]
if (is_chromeos) {
sources += [
"chrome_os_error_strings.cc",
"chrome_os_error_strings.h",
]
}
}
static_library("method_strings") {
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/payments/core/chrome_os_error_strings.h"
namespace payments {
namespace errors {
const char kEmptyResponse[] = "Android app response is empty.";
const char kInvalidResponse[] = "Android app response is not valid.";
const char kMoreThanOneActivity[] =
"Found more than one PAY activity in the Trusted Web Activity, but at most "
"one activity is supported.";
const char kMoreThanOneService[] =
"Found more than one IS_READY_TO_PAY service in the Trusted Web Activity, "
"but at most one service is supported.";
} // namespace errors
} // namespace payments
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_PAYMENTS_CORE_CHROME_OS_ERROR_STRINGS_H_
#define COMPONENTS_PAYMENTS_CORE_CHROME_OS_ERROR_STRINGS_H_
namespace payments {
namespace errors {
// Developer facing error messages that are used only on Chrome OS.
// Used if ARC sends a null object to the browser.
extern const char kEmptyResponse[];
// Used if ARC sends an object ot the browser that has neither an error message
// nor a valid response.
extern const char kInvalidResponse[];
// Used when the TWA declares more than one PAY activity.
extern const char kMoreThanOneActivity[];
// Used when the TWA declares more than one IS_READY_TO_PAY service.
extern const char kMoreThanOneService[];
} // namespace errors
} // namespace payments
#endif // COMPONENTS_PAYMENTS_CORE_CHROME_OS_ERROR_STRINGS_H_
......@@ -201,5 +201,8 @@ const char kNoContentAndNoLinkHeader[] =
const char kNoContentInPaymentManifest[] =
"No content found in payment manifest \"$1\".";
const char kUnableToInvokeAndroidPaymentApps[] =
"Unable to invoke Android apps.";
} // namespace errors
} // namespace payments
......@@ -222,10 +222,14 @@ extern const char kGenericPaymentMethodNotSupportedMessage[];
// be used with base::ReplaceStringPlaceholders(fmt, {A}, nullptr).
extern const char kNoContentAndNoLinkHeader[];
// User when the downloaded payment manifest A is empty. This format should be
// Used when the downloaded payment manifest A is empty. This format should be
// used with base::ReplaceStringPlaceholders(fmt, {A}, nullptr).
extern const char kNoContentInPaymentManifest[];
// Used when it's impossible to invoke Android payment apps, e.g., when ARC is
// disabled on Chrome OS.
extern const char kUnableToInvokeAndroidPaymentApps[];
} // namespace errors
} // namespace payments
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment