Commit a23c5648 authored by Yuwei Huang's avatar Yuwei Huang Committed by Commit Bot

[remoting][mobile] Notification client

This CL implements a notification client for mobile clients to fetch
notification from gstatic. Currently it only has logic to match rules
and select a notification. It isn't connected to gstatic or any UI
component yet.

Bug: 1001291
Change-Id: I2d58b53363b1db0b9bb836dfcfb5ec2a36126d19
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1812398
Commit-Queue: Yuwei Huang <yuweih@chromium.org>
Reviewed-by: default avatarJamie Walch <jamiewalch@chromium.org>
Cr-Commit-Position: refs/heads/master@{#699582}
parent a734a1c3
......@@ -158,6 +158,7 @@ test("remoting_unittests") {
"//remoting/client:unit_tests",
"//remoting/client/audio:unit_tests",
"//remoting/client/input:unit_tests",
"//remoting/client/notification:unit_tests",
"//remoting/client/ui:unit_tests",
"//remoting/protocol:unit_tests",
"//remoting/signaling:unit_tests",
......
......@@ -41,6 +41,7 @@ static_library("client") {
deps = [
"//remoting/client/audio",
"//remoting/client/notification",
"//remoting/codec:decoder",
"//remoting/protocol",
"//third_party/libyuv",
......
# Copyright 2019 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.
source_set("notification") {
sources = [
"json_fetcher.h",
"notification_client.cc",
"notification_client.h",
"notification_message.cc",
"notification_message.h",
"version_range.cc",
"version_range.h",
]
deps = [
"//base",
]
}
source_set("unit_tests") {
testonly = true
sources = [
"notification_client_unittest.cc",
"version_range_unittest.cc",
]
deps = [
":notification",
"//testing/gmock",
"//testing/gtest",
]
}
// Copyright 2019 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 REMOTING_CLIENT_NOTIFICATION_JSON_FETCHER_H_
#define REMOTING_CLIENT_NOTIFICATION_JSON_FETCHER_H_
#include <string>
#include "base/callback_forward.h"
namespace base {
class Value;
} // namespace base
namespace remoting {
// Interface for fetching a JSON file under https://www.gstatic.com/chromoting.
class JsonFetcher {
public:
JsonFetcher() = default;
virtual ~JsonFetcher() = default;
// |relative_path| is relative to https://www.gstatic.com/chromoting/.
// Runs |done| with the decoded value if the file is successfully fetched,
// otherwise runs |done| with base::nullopt.
//
// Note that the implementation MUST be able to handle concurrent requests and
// MUST NOT keep |done| after its destructor is called.
virtual void FetchJsonFile(
const std::string& relative_path,
base::OnceCallback<void(base::Optional<base::Value>)> done) const = 0;
};
} // namespace remoting
#endif // REMOTING_CLIENT_NOTIFICATION_JSON_FETCHER_H_
\ No newline at end of file
// Copyright 2019 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 "remoting/client/notification/notification_client.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_refptr.h"
#include "base/values.h"
#include "remoting/client/notification/json_fetcher.h"
#include "remoting/client/notification/notification_message.h"
#include "remoting/client/notification/version_range.h"
namespace remoting {
namespace {
constexpr char kDefaultLocale[] = "en-US";
constexpr char kNotificationRootPath[] = "notification/";
constexpr char kNotificationRulesPath[] = "notification/rules.json";
template <typename T, typename IsTChecker, typename TGetter>
bool FindKeyAndGet(const base::Value& dict,
const std::string& key,
T* out,
IsTChecker is_t_checker,
TGetter t_getter,
const std::string& type_name) {
const base::Value* value = dict.FindKey(key);
if (!value) {
LOG(ERROR) << "|" << key << "| not found in dictionary.";
return false;
}
if (!(value->*is_t_checker)()) {
LOG(ERROR) << "Value is not " << type_name;
return false;
}
*out = (value->*t_getter)();
return true;
}
bool FindKeyAndGet(const base::Value& dict,
const std::string& key,
std::string* out) {
const std::string& (base::Value::*get_string_fp)() const =
&base::Value::GetString;
return FindKeyAndGet(dict, key, out, &base::Value::is_string, get_string_fp,
"string");
}
bool FindKeyAndGet(const base::Value& dict, const std::string& key, int* out) {
return FindKeyAndGet(dict, key, out, &base::Value::is_int,
&base::Value::GetInt, "int");
}
bool ShouldShowNotificationForUser(const std::string& user_email,
int percent_int) {
DCHECK_GE(percent_int, 0);
DCHECK_LE(percent_int, 100);
return (base::FastHash(user_email) % 100) <
static_cast<unsigned int>(percent_int);
}
class MessageAndLinkTextResults
: public base::RefCounted<MessageAndLinkTextResults> {
public:
using Callback = base::OnceCallback<void(bool is_successful)>;
MessageAndLinkTextResults(const std::string& locale,
Callback done,
std::string* out_message_translation,
std::string* out_link_translation)
: locale_(locale),
done_(std::move(done)),
out_message_translation_(out_message_translation),
out_link_translation_(out_link_translation) {}
void OnMessageTranslationsFetched(base::Optional<base::Value> translations) {
is_message_translation_fetched_ = true;
OnTranslationsFetched(std::move(translations), out_message_translation_);
}
void OnLinkTranslationsFetched(base::Optional<base::Value> translations) {
is_link_translation_fetched_ = true;
OnTranslationsFetched(std::move(translations), out_link_translation_);
}
private:
friend class base::RefCounted<MessageAndLinkTextResults>;
~MessageAndLinkTextResults() = default;
void OnTranslationsFetched(base::Optional<base::Value> translations,
std::string* string_to_update) {
if (!done_) {
LOG(WARNING) << "Received new translations after some translations have "
<< "failed to fetch";
return;
}
if (!translations) {
LOG(ERROR) << "Failed to fetch translation file";
NotifyFetchFailed();
return;
}
if (!FindKeyAndGet(*translations, locale_, string_to_update)) {
LOG(WARNING) << "Failed to find translation for locale " << locale_
<< ". Falling back to default locale: " << kDefaultLocale;
if (!FindKeyAndGet(*translations, kDefaultLocale, string_to_update)) {
LOG(ERROR) << "Failed to find translation for default locale";
NotifyFetchFailed();
return;
}
}
if (done_ && is_message_translation_fetched_ &&
is_link_translation_fetched_) {
std::move(done_).Run(true);
}
}
void NotifyFetchFailed() {
DCHECK(done_);
std::move(done_).Run(false);
}
std::string locale_;
Callback done_;
std::string* out_message_translation_;
std::string* out_link_translation_;
bool is_message_translation_fetched_ = false;
bool is_link_translation_fetched_ = false;
};
} // namespace
NotificationClient::~NotificationClient() = default;
NotificationClient::NotificationClient(std::unique_ptr<JsonFetcher> fetcher,
const std::string& current_platform,
const std::string& current_version,
const std::string& locale)
: fetcher_(std::move(fetcher)),
current_platform_(current_platform),
current_version_(current_version),
locale_(locale) {}
void NotificationClient::GetNotification(const std::string& user_email,
NotificationCallback callback) {
fetcher_->FetchJsonFile(
kNotificationRulesPath,
base::BindOnce(&NotificationClient::OnRulesFetched,
base::Unretained(this), user_email, std::move(callback)));
}
void NotificationClient::OnRulesFetched(const std::string& user_email,
NotificationCallback callback,
base::Optional<base::Value> rules) {
if (!rules) {
LOG(ERROR) << "Rules not found";
std::move(callback).Run(base::nullopt);
return;
}
if (!rules->is_list()) {
LOG(ERROR) << "Rules should be list";
std::move(callback).Run(base::nullopt);
return;
}
for (const auto& rule : rules->GetList()) {
std::string message_text_filename;
std::string link_text_filename;
auto message = ParseAndMatchRule(rule, user_email, &message_text_filename,
&link_text_filename);
if (message) {
FetchTranslatedTexts(message_text_filename, link_text_filename,
std::move(message), std::move(callback));
return;
}
}
// No matching rule is found.
std::move(callback).Run(base::nullopt);
}
base::Optional<NotificationMessage> NotificationClient::ParseAndMatchRule(
const base::Value& rule,
const std::string& user_email,
std::string* out_message_text_filename,
std::string* out_link_text_filename) {
std::string appearance_string;
std::string target_platform;
std::string version_spec_string;
std::string message_text_filename;
std::string link_text_filename;
std::string link_url;
int percent;
if (!FindKeyAndGet(rule, "appearance", &appearance_string) ||
!FindKeyAndGet(rule, "target_platform", &target_platform) ||
!FindKeyAndGet(rule, "version", &version_spec_string) ||
!FindKeyAndGet(rule, "message_text", &message_text_filename) ||
!FindKeyAndGet(rule, "link_text", &link_text_filename) ||
!FindKeyAndGet(rule, "link_url", &link_url) ||
!FindKeyAndGet(rule, "percent", &percent)) {
return base::nullopt;
}
if (target_platform != current_platform_) {
VLOG(1) << "Notification ignored. Target platform: " << target_platform
<< "; current platform: " << current_platform_;
return base::nullopt;
}
VersionRange version_range(version_spec_string);
if (!version_range.IsValid()) {
LOG(ERROR) << "Invalid version range: " << version_spec_string;
return base::nullopt;
}
if (!version_range.ContainsVersion(current_version_)) {
VLOG(1) << "Current version " << current_version_ << " not in range "
<< version_spec_string;
return base::nullopt;
}
if (!ShouldShowNotificationForUser(user_email, percent)) {
VLOG(1) << "User is not selected for notification";
return base::nullopt;
}
auto message = base::make_optional<NotificationMessage>();
if (appearance_string == "TOAST") {
message->appearance = NotificationMessage::Appearance::TOAST;
} else if (appearance_string == "DIALOG") {
message->appearance = NotificationMessage::Appearance::DIALOG;
} else {
LOG(ERROR) << "Unknown appearance: " << appearance_string;
return base::nullopt;
}
message->link_url = link_url;
*out_message_text_filename = message_text_filename;
*out_link_text_filename = link_text_filename;
return message;
}
void NotificationClient::FetchTranslatedTexts(
const std::string& message_text_filename,
const std::string& link_text_filename,
base::Optional<NotificationMessage> partial_message,
NotificationCallback done) {
// Copy the message into a unique_ptr since moving base::Optional does not
// move the internal storage.
auto message_copy = std::make_unique<NotificationMessage>(*partial_message);
std::string* message_text_ptr = &message_copy->message_text;
std::string* link_text_ptr = &message_copy->link_text;
auto on_translated_texts_fetched = base::BindOnce(
[](std::unique_ptr<NotificationMessage> message,
NotificationCallback done, bool is_successful) {
std::move(done).Run(
is_successful ? base::make_optional<NotificationMessage>(*message)
: base::nullopt);
},
std::move(message_copy), std::move(done));
auto results = base::MakeRefCounted<MessageAndLinkTextResults>(
locale_, std::move(on_translated_texts_fetched), message_text_ptr,
link_text_ptr);
fetcher_->FetchJsonFile(
kNotificationRootPath + message_text_filename,
base::BindOnce(&MessageAndLinkTextResults::OnMessageTranslationsFetched,
results));
fetcher_->FetchJsonFile(
kNotificationRootPath + link_text_filename,
base::BindOnce(&MessageAndLinkTextResults::OnLinkTranslationsFetched,
results));
}
} // namespace remoting
// Copyright 2019 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 REMOTING_CLIENT_NOTIFICATION_NOTIFICATION_CLIENT_H_
#define REMOTING_CLIENT_NOTIFICATION_NOTIFICATION_CLIENT_H_
#include <memory>
#include <string>
#include "base/callback_forward.h"
#include "base/macros.h"
#include "base/optional.h"
namespace base {
class Value;
} // namespace base
namespace remoting {
class JsonFetcher;
struct NotificationMessage;
// Class for fetching a notification from the server so that the caller can
// show that on some UI component when the app is launched.
class NotificationClient final {
public:
using NotificationCallback =
base::OnceCallback<void(base::Optional<NotificationMessage>)>;
~NotificationClient();
// Fetches notifications from the server and calls |callback| with the
// best matched notification. If notifications failed to fetch or no matching
// notification is found then base::nullopt will be returned. |callback| will
// be silently dropped if |this| is deleted before the notification is
// fetched.
void GetNotification(const std::string& user_email,
NotificationCallback callback);
private:
friend class NotificationClientTest;
// Constructor for unittest.
NotificationClient(std::unique_ptr<JsonFetcher> fetcher,
const std::string& current_platform,
const std::string& current_version,
const std::string& locale);
void OnRulesFetched(const std::string& user_email,
NotificationCallback callback,
base::Optional<base::Value> rules);
// Returns non-empty NotificationMessage if the rule is parsed successfully
// and the rule should apply to the user. |message_text| and |link_text| will
// not be set and caller needs to call FetchTranslatedText to fill them up.
base::Optional<NotificationMessage> ParseAndMatchRule(
const base::Value& rule,
const std::string& user_email,
std::string* out_message_text_filename,
std::string* out_link_text_filename);
void FetchTranslatedTexts(const std::string& message_text_filename,
const std::string& link_text_filename,
base::Optional<NotificationMessage> partial_message,
NotificationCallback done);
std::unique_ptr<JsonFetcher> fetcher_;
std::string current_platform_;
std::string current_version_;
std::string locale_;
DISALLOW_COPY_AND_ASSIGN(NotificationClient);
};
} // namespace remoting
#endif // REMOTING_CLIENT_NOTIFICATION_NOTIFICATION_CLIENT_H_
// Copyright 2019 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 "remoting/client/notification/notification_client.h"
#include <memory>
#include "base/memory/ptr_util.h"
#include "base/optional.h"
#include "base/test/mock_callback.h"
#include "base/values.h"
#include "remoting/client/notification/json_fetcher.h"
#include "remoting/client/notification/notification_message.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace remoting {
namespace {
using ::testing::ByMove;
using ::testing::Return;
constexpr char kTestEmail[] = "test@example.com";
constexpr char kTestPlatform[] = "IOS";
constexpr char kTestVersion[] = "76.0.3809.13";
constexpr char kTestLocale[] = "zh-CN";
class MockJsonFetcher : public JsonFetcher {
public:
// GMock doesn't work with rvalue parameters. This works around it.
MOCK_CONST_METHOD1(FetchJsonFile,
base::Optional<base::Value>(const std::string&));
void FetchJsonFile(const std::string& relative_path,
base::OnceCallback<void(base::Optional<base::Value>)> done)
const override {
auto value_opt = FetchJsonFile(relative_path);
std::move(done).Run(std::move(value_opt));
}
};
MATCHER(NoMessage, "") {
return !arg.has_value();
}
MATCHER_P(MessageMatches, expected, "") {
return arg->appearance == expected.appearance &&
arg->message_text == expected.message_text &&
arg->link_text == expected.link_text &&
arg->link_url == expected.link_url;
}
template <typename T>
decltype(auto) ReturnByMove(T t) {
return Return(ByMove(std::move(t)));
}
base::Value CreateDefaultRule() {
base::Value rule(base::Value::Type::DICTIONARY);
rule.SetStringKey("appearance", "TOAST");
rule.SetStringKey("target_platform", "IOS");
rule.SetStringKey("version", "[75-)");
rule.SetStringKey("message_text", "message_text.json");
rule.SetStringKey("link_text", "link_text.json");
rule.SetStringKey("link_url", "https://example.com/some_link");
rule.SetIntKey("percent", 100);
return rule;
}
base::Value CreateDefaultTranslations(const std::string& text) {
base::Value translations(base::Value::Type::DICTIONARY);
translations.SetStringKey("en-US", "en-US:" + text);
translations.SetStringKey("zh-CN", "zh-CN:" + text);
return translations;
}
NotificationMessage CreateDefaultNotification() {
NotificationMessage message;
message.appearance = NotificationMessage::Appearance::TOAST;
message.message_text = "zh-CN:message";
message.link_text = "zh-CN:link";
message.link_url = "https://example.com/some_link";
return message;
}
} // namespace
class NotificationClientTest : public ::testing::Test {
public:
NotificationClientTest() {
auto fetcher = std::make_unique<MockJsonFetcher>();
fetcher_ = fetcher.get();
client_ = base::WrapUnique(new NotificationClient(
std::move(fetcher), kTestPlatform, kTestVersion, kTestLocale));
}
~NotificationClientTest() override = default;
protected:
MockJsonFetcher* fetcher_;
std::unique_ptr<NotificationClient> client_;
};
TEST_F(NotificationClientTest, NoRule) {
base::Value rules(base::Value::Type::LIST);
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(NoMessage()));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, DefaultRule) {
base::Value rules(base::Value::Type::LIST);
rules.Append(CreateDefaultRule());
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/message_text.json"))
.WillOnce(ReturnByMove(CreateDefaultTranslations("message")));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/link_text.json"))
.WillOnce(ReturnByMove(CreateDefaultTranslations("link")));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(MessageMatches(CreateDefaultNotification())));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, PlatformNotMatched) {
base::Value rule = CreateDefaultRule();
rule.SetStringKey("target_platform", "ANDROID");
base::Value rules(base::Value::Type::LIST);
rules.Append(std::move(rule));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(NoMessage()));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, VersionNotMatched) {
base::Value rule = CreateDefaultRule();
rule.SetStringKey("version", "[77-)");
base::Value rules(base::Value::Type::LIST);
rules.Append(std::move(rule));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(NoMessage()));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, UserNotSelected) {
base::Value rule = CreateDefaultRule();
rule.SetIntKey("percent", 0);
base::Value rules(base::Value::Type::LIST);
rules.Append(std::move(rule));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(NoMessage()));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, SecondRuleMatches) {
base::Value rules(base::Value::Type::LIST);
base::Value rule_1 = CreateDefaultRule();
rule_1.SetStringKey("target_platform", "ANDROID");
rule_1.SetStringKey("message_text", "message_text_1.json");
rules.Append(std::move(rule_1));
rules.Append(CreateDefaultRule());
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/message_text.json"))
.WillOnce(ReturnByMove(CreateDefaultTranslations("message")));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/link_text.json"))
.WillOnce(ReturnByMove(CreateDefaultTranslations("link")));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(MessageMatches(CreateDefaultNotification())));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, MultipleMatchingRules_FirstRuleSelected) {
base::Value rules(base::Value::Type::LIST);
rules.Append(CreateDefaultRule());
base::Value rule_2 = CreateDefaultRule();
rule_2.SetStringKey("message_text", "message_text_2.json");
rules.Append(std::move(rule_2));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/message_text.json"))
.WillOnce(ReturnByMove(CreateDefaultTranslations("message")));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/link_text.json"))
.WillOnce(ReturnByMove(CreateDefaultTranslations("link")));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(MessageMatches(CreateDefaultNotification())));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, TextFilesNotFound) {
base::Value rules(base::Value::Type::LIST);
rules.Append(CreateDefaultRule());
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
base::Value translation = CreateDefaultTranslations("message");
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/message_text.json"))
.WillOnce(ReturnByMove(base::nullopt));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/link_text.json"))
.WillOnce(ReturnByMove(base::nullopt));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(NoMessage()));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, TranslationNotFound_FallbackToEnglish) {
base::Value rules(base::Value::Type::LIST);
rules.Append(CreateDefaultRule());
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
base::Value translations = CreateDefaultTranslations("message");
translations.RemoveKey("zh-CN");
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/message_text.json"))
.WillOnce(ReturnByMove(std::move(translations)));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/link_text.json"))
.WillOnce(ReturnByMove(CreateDefaultTranslations("link")));
NotificationMessage notification = CreateDefaultNotification();
notification.message_text = "en-US:message";
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(MessageMatches(notification)));
client_->GetNotification(kTestEmail, callback.Get());
}
TEST_F(NotificationClientTest, NoAvailableTranslation) {
base::Value rules(base::Value::Type::LIST);
rules.Append(CreateDefaultRule());
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/rules.json"))
.WillOnce(ReturnByMove(std::move(rules)));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/message_text.json"))
.WillOnce(ReturnByMove(base::Value(base::Value::Type::DICTIONARY)));
EXPECT_CALL(*fetcher_, FetchJsonFile("notification/link_text.json"))
.WillOnce(ReturnByMove(base::Value(base::Value::Type::DICTIONARY)));
base::MockCallback<NotificationClient::NotificationCallback> callback;
EXPECT_CALL(callback, Run(NoMessage()));
client_->GetNotification(kTestEmail, callback.Get());
}
} // namespace remoting
\ No newline at end of file
// Copyright 2019 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 "remoting/client/notification/notification_message.h"
namespace remoting {
NotificationMessage::NotificationMessage() = default;
NotificationMessage::NotificationMessage(const NotificationMessage&) = default;
NotificationMessage::NotificationMessage(NotificationMessage&&) = default;
NotificationMessage::~NotificationMessage() = default;
NotificationMessage& NotificationMessage::operator=(
const NotificationMessage&) = default;
NotificationMessage& NotificationMessage::operator=(NotificationMessage&&) =
default;
} // namespace remoting
// Copyright 2019 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 REMOTING_CLIENT_NOTIFICATION_NOTIFICATION_MESSAGE_H_
#define REMOTING_CLIENT_NOTIFICATION_NOTIFICATION_MESSAGE_H_
#include <string>
namespace remoting {
struct NotificationMessage final {
enum class Appearance {
TOAST,
DIALOG,
};
NotificationMessage();
NotificationMessage(const NotificationMessage&);
NotificationMessage(NotificationMessage&&);
~NotificationMessage();
NotificationMessage& operator=(const NotificationMessage&);
NotificationMessage& operator=(NotificationMessage&&);
Appearance appearance;
std::string message_text;
std::string link_text;
std::string link_url;
};
} // namespace remoting
#endif // REMOTING_CLIENT_NOTIFICATION_NOTIFICATION_MESSAGE_H_
// Copyright 2019 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 "remoting/client/notification/version_range.h"
#include <stdint.h>
#include <limits>
#include "base/logging.h"
namespace remoting {
namespace {
constexpr uint16_t kUnboundMinVersionNumber = 0;
constexpr uint16_t kUnboundMaxVersionNumber =
std::numeric_limits<uint16_t>::max();
} // namespace
VersionRange::VersionRange(const std::string& range_spec) {
if (range_spec.empty()) {
// Invalid.
return;
}
size_t dash_pos = range_spec.find('-');
if (dash_pos == std::string::npos) {
// May be a single version string.
min_version_ = base::make_optional<base::Version>(range_spec);
max_version_ = base::make_optional<base::Version>(*min_version_);
is_min_version_inclusive_ = true;
is_max_version_inclusive_ = true;
return;
}
char left_bracket = range_spec.front();
if (left_bracket != '[' && left_bracket != '(') {
// Invalid.
return;
}
is_min_version_inclusive_ = (left_bracket == '[');
char right_bracket = range_spec.back();
if (right_bracket != ']' && right_bracket != ')') {
// Invalid.
return;
}
is_max_version_inclusive_ = (right_bracket == ']');
DCHECK_GE(range_spec.size(), 3u);
DCHECK_GT(dash_pos, 0u);
DCHECK_LT(dash_pos, range_spec.size() - 1);
std::string min_version_string = range_spec.substr(1, dash_pos - 1);
if (min_version_string.empty()) {
// Unbound min version.
std::vector<uint32_t> version_components{kUnboundMinVersionNumber};
min_version_ =
base::make_optional<base::Version>(std::move(version_components));
} else {
min_version_ = base::make_optional<base::Version>(min_version_string);
}
std::string max_version_string = range_spec.substr(dash_pos + 1);
max_version_string.pop_back();
if (max_version_string.empty()) {
// Unbound max version.
std::vector<uint32_t> version_components{kUnboundMaxVersionNumber};
max_version_ =
base::make_optional<base::Version>(std::move(version_components));
} else {
max_version_ = base::make_optional<base::Version>(max_version_string);
}
}
VersionRange::~VersionRange() = default;
bool VersionRange::IsValid() const {
return min_version_ && min_version_->IsValid() && max_version_ &&
max_version_->IsValid() && *min_version_ <= *max_version_;
}
bool VersionRange::ContainsVersion(const std::string& version_string) const {
if (!IsValid()) {
return false;
}
base::Version version(version_string);
if (!version.IsValid()) {
LOG(ERROR) << "Invalid version number: " << version_string;
return false;
}
int min_version_compare_result = min_version_->CompareTo(version);
if (min_version_compare_result > 0 ||
(min_version_compare_result == 0 && !is_min_version_inclusive_)) {
return false;
}
int max_version_compare_result = max_version_->CompareTo(version);
if (max_version_compare_result < 0 ||
(max_version_compare_result == 0 && !is_max_version_inclusive_)) {
return false;
}
return true;
}
} // namespace remoting
// Copyright 2019 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 REMOTING_CLIENT_NOTIFICATION_VERSION_RANGE_H_
#define REMOTING_CLIENT_NOTIFICATION_VERSION_RANGE_H_
#include <string>
#include "base/macros.h"
#include "base/optional.h"
#include "base/version.h"
namespace remoting {
// Class representing a range of dotted version numbers. Supporting parsing and
// in-range checking.
class VersionRange final {
public:
// Parses a range spec into range.
//
// Format (regex):
// ([\(\[]((\d+\.)*\d+)?-((\d+\.)*\d+)?[\)\]]|(\d+\.)*\d+)
//
// Examples:
// 1.2.3 Exactly 1.2.3
// [-1.2.3) Anything up to but not including 1.2.3
// [1.2-3) Anything between 1.2 (inclusive) and 3 (exclusive)
// (1.2-3] Anything between 1.2 (exclusive) and 3 (inclusive)
// [1.2-) 1.2 (inclusive) and higher
// [-] Anything
//
// Min version must be less than or equal to max version.
explicit VersionRange(const std::string& range_spec);
~VersionRange();
bool IsValid() const;
bool ContainsVersion(const std::string& version_string) const;
private:
base::Optional<base::Version> min_version_;
base::Optional<base::Version> max_version_;
bool is_min_version_inclusive_ = false;
bool is_max_version_inclusive_ = false;
DISALLOW_COPY_AND_ASSIGN(VersionRange);
};
} // namespace remoting
#endif // REMOTING_CLIENT_NOTIFICATION_VERSION_RANGE_H_
// Copyright 2019 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 "remoting/client/notification/version_range.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace remoting {
TEST(VersionRangeTest, ExactVersionMatch) {
VersionRange range("1.2.3");
ASSERT_TRUE(range.IsValid());
ASSERT_TRUE(range.ContainsVersion("1.2.3"));
ASSERT_FALSE(range.ContainsVersion("1.2.1"));
ASSERT_FALSE(range.ContainsVersion("1.2.5"));
}
TEST(VersionRangeTest, UnboundMinimumExclusiveMaximum) {
VersionRange range("[-1.2.3)");
ASSERT_TRUE(range.IsValid());
ASSERT_TRUE(range.ContainsVersion("0.1"));
ASSERT_TRUE(range.ContainsVersion("1.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.2"));
ASSERT_FALSE(range.ContainsVersion("1.2.3"));
ASSERT_FALSE(range.ContainsVersion("1.2.5"));
ASSERT_FALSE(range.ContainsVersion("2"));
}
TEST(VersionRangeTest, UnboundMinimumInclusiveMaximum) {
VersionRange range("[-1.2.3]");
ASSERT_TRUE(range.IsValid());
ASSERT_TRUE(range.ContainsVersion("0.1"));
ASSERT_TRUE(range.ContainsVersion("1.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.3"));
ASSERT_FALSE(range.ContainsVersion("1.2.5"));
ASSERT_FALSE(range.ContainsVersion("2"));
}
TEST(VersionRangeTest, InclusiveMinimumUnboundMaximum) {
VersionRange range("[1.2.3-)");
ASSERT_TRUE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0.1"));
ASSERT_FALSE(range.ContainsVersion("1.2.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.3"));
ASSERT_TRUE(range.ContainsVersion("1.2.5"));
ASSERT_TRUE(range.ContainsVersion("2"));
}
TEST(VersionRangeTest, ExclusiveMinimumUnboundMaximum) {
VersionRange range("(1.2.3-)");
ASSERT_TRUE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0.1"));
ASSERT_FALSE(range.ContainsVersion("1.2.2"));
ASSERT_FALSE(range.ContainsVersion("1.2.3"));
ASSERT_TRUE(range.ContainsVersion("1.2.5"));
ASSERT_TRUE(range.ContainsVersion("2"));
}
TEST(VersionRangeTest, InclusiveMinimumExclusiveMaximum) {
VersionRange range("[1.2-3)");
ASSERT_TRUE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0.1"));
ASSERT_FALSE(range.ContainsVersion("1.1"));
ASSERT_TRUE(range.ContainsVersion("1.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.3"));
ASSERT_TRUE(range.ContainsVersion("2"));
ASSERT_FALSE(range.ContainsVersion("3"));
ASSERT_FALSE(range.ContainsVersion("4"));
}
TEST(VersionRangeTest, ExclusiveMinimumInclusiveMaximum) {
VersionRange range("(1.2-3]");
ASSERT_TRUE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0.1"));
ASSERT_FALSE(range.ContainsVersion("1.1"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.3"));
ASSERT_TRUE(range.ContainsVersion("2"));
ASSERT_TRUE(range.ContainsVersion("3"));
ASSERT_FALSE(range.ContainsVersion("3.1"));
ASSERT_FALSE(range.ContainsVersion("4"));
}
TEST(VersionRangeTest, ExclusiveMinimumExclusiveMaximum) {
VersionRange range("(1.2-3)");
ASSERT_TRUE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0.1"));
ASSERT_FALSE(range.ContainsVersion("1.1"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.2"));
ASSERT_TRUE(range.ContainsVersion("1.2.3"));
ASSERT_TRUE(range.ContainsVersion("2"));
ASSERT_FALSE(range.ContainsVersion("3"));
ASSERT_FALSE(range.ContainsVersion("3.1"));
ASSERT_FALSE(range.ContainsVersion("4"));
}
TEST(VersionRangeTest, AnyVersion1) {
VersionRange range("(-)");
ASSERT_TRUE(range.IsValid());
ASSERT_TRUE(range.ContainsVersion("1"));
ASSERT_TRUE(range.ContainsVersion("1.1"));
ASSERT_TRUE(range.ContainsVersion("2"));
ASSERT_TRUE(range.ContainsVersion("3"));
ASSERT_TRUE(range.ContainsVersion("4"));
}
TEST(VersionRangeTest, AnyVersion2) {
VersionRange range("[-]");
ASSERT_TRUE(range.IsValid());
ASSERT_TRUE(range.ContainsVersion("1"));
ASSERT_TRUE(range.ContainsVersion("1.1"));
ASSERT_TRUE(range.ContainsVersion("2"));
ASSERT_TRUE(range.ContainsVersion("3"));
ASSERT_TRUE(range.ContainsVersion("4"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError1) {
VersionRange range("");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError2) {
VersionRange range("90j3awef");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError3) {
VersionRange range("[1.2)");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError4) {
VersionRange range("[1.2-");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError5) {
VersionRange range("1.2-)");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError6) {
VersionRange range("1.2-");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError7) {
VersionRange range("-1.2");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError8) {
VersionRange range("[-1.2");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
}
TEST(VersionRangeTest, InvalidRangeSpec_SyntaxError9) {
VersionRange range("-1.2)");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("0"));
ASSERT_FALSE(range.ContainsVersion("1.2"));
}
TEST(VersionRangeTest, InvalidRangeSpec_MinBiggerThanMax) {
VersionRange range("[3.4-1.2)");
ASSERT_FALSE(range.IsValid());
ASSERT_FALSE(range.ContainsVersion("1.2"));
ASSERT_FALSE(range.ContainsVersion("2.0"));
ASSERT_FALSE(range.ContainsVersion("3.4"));
}
} // namespace remoting
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