| // Copyright 2015 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/open_from_clipboard/clipboard_recent_content_ios.h" |
| |
| #import <CoreGraphics/CoreGraphics.h> |
| #import <UIKit/UIKit.h> |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #import "base/test/ios/wait_util.h" |
| #include "base/test/task_environment.h" |
| #import "components/open_from_clipboard/clipboard_recent_content_impl_ios.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "testing/platform_test.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| using base::test::ios::WaitUntilConditionOrTimeout; |
| using base::test::ios::kWaitForCookiesTimeout; |
| using base::test::ios::kWaitForActionTimeout; |
| |
| namespace { |
| |
| UIImage* TestUIImage(UIColor* color = [UIColor redColor]) { |
| CGRect frame = CGRectMake(0, 0, 1.0, 1.0); |
| UIGraphicsBeginImageContext(frame.size); |
| |
| CGContextRef context = UIGraphicsGetCurrentContext(); |
| CGContextSetFillColorWithColor(context, color.CGColor); |
| CGContextFillRect(context, frame); |
| |
| UIImage* image = UIGraphicsGetImageFromCurrentImageContext(); |
| UIGraphicsEndImageContext(); |
| |
| return image; |
| } |
| |
| void SetPasteboardImage(UIImage* image) { |
| [[UIPasteboard generalPasteboard] setImage:image]; |
| } |
| |
| void SetPasteboardContent(const char* data) { |
| [[UIPasteboard generalPasteboard] |
| setValue:[NSString stringWithUTF8String:data] |
| forPasteboardType:@"public.plain-text"]; |
| } |
| const char kUnrecognizedURL[] = "bad://foo/"; |
| const char kRecognizedURL[] = "good://bar/"; |
| const char kRecognizedURL2[] = "good://bar/2"; |
| const char kAppSpecificURL[] = "test://qux/"; |
| const char kAppSpecificScheme[] = "test"; |
| const char kRecognizedScheme[] = "good"; |
| NSTimeInterval kLongerThanMaxAge = 60 * 60 * 7; |
| NSTimeInterval kMaxAge = 60 * 60 * 1; |
| } // namespace |
| |
| @interface ClipboardRecentContentImplIOSWithFakeUptime |
| : ClipboardRecentContentImplIOS |
| @property(nonatomic) NSTimeInterval fakeUptime; |
| |
| - (instancetype)initWithMaxAge:(NSTimeInterval)maxAge |
| authorizedSchemes:(NSArray*)authorizedSchemes |
| userDefaults:(NSUserDefaults*)groupUserDefaults |
| uptime:(NSTimeInterval)uptime; |
| |
| @end |
| |
| @implementation ClipboardRecentContentImplIOSWithFakeUptime |
| |
| @synthesize fakeUptime = _fakeUptime; |
| |
| - (instancetype)initWithMaxAge:(NSTimeInterval)maxAge |
| authorizedSchemes:(NSSet*)authorizedSchemes |
| userDefaults:(NSUserDefaults*)groupUserDefaults |
| uptime:(NSTimeInterval)uptime { |
| self = [super initWithMaxAge:maxAge |
| authorizedSchemes:authorizedSchemes |
| userDefaults:groupUserDefaults |
| delegate:nil]; |
| if (self) { |
| _fakeUptime = uptime; |
| } |
| return self; |
| } |
| |
| - (NSTimeInterval)uptime { |
| return self.fakeUptime; |
| } |
| |
| @end |
| |
| class ClipboardRecentContentIOSWithFakeUptime |
| : public ClipboardRecentContentIOS { |
| public: |
| ClipboardRecentContentIOSWithFakeUptime( |
| ClipboardRecentContentImplIOS* implementation) |
| : ClipboardRecentContentIOS(implementation) {} |
| }; |
| |
| class ClipboardRecentContentIOSTest : public ::testing::Test { |
| protected: |
| ClipboardRecentContentIOSTest() { |
| // By default, set that the device booted 10 days ago. |
| ResetClipboardRecentContent(kAppSpecificScheme, base::Days(10)); |
| } |
| |
| void SimulateDeviceRestart() { |
| ResetClipboardRecentContent(kAppSpecificScheme, base::Seconds(0)); |
| } |
| |
| void ResetClipboardRecentContent(const std::string& application_scheme, |
| base::TimeDelta time_delta) { |
| ClipboardRecentContentImplIOSWithFakeUptime* |
| clipboard_content_implementation = |
| [[ClipboardRecentContentImplIOSWithFakeUptime alloc] |
| initWithMaxAge:kMaxAge |
| authorizedSchemes:@[ |
| base::SysUTF8ToNSString(kRecognizedScheme), |
| base::SysUTF8ToNSString(application_scheme) |
| ] |
| userDefaults:[NSUserDefaults standardUserDefaults] |
| uptime:time_delta.InSecondsF()]; |
| |
| clipboard_content_ = |
| std::make_unique<ClipboardRecentContentIOSWithFakeUptime>( |
| clipboard_content_implementation); |
| |
| // Keep a weak pointer to the ClipboardRecentContentImplIOS to allow |
| // updating the fake pasteboard change date. |
| clipboard_content_implementation_ = clipboard_content_implementation; |
| } |
| |
| void SetStoredPasteboardChangeDate(NSDate* change_date) { |
| clipboard_content_implementation_.lastPasteboardChangeDate = change_date; |
| [clipboard_content_implementation_ saveToUserDefaults]; |
| } |
| |
| protected: |
| std::unique_ptr<ClipboardRecentContentIOSWithFakeUptime> clipboard_content_; |
| __weak ClipboardRecentContentImplIOSWithFakeUptime* |
| clipboard_content_implementation_; |
| |
| base::test::TaskEnvironment task_environment_; |
| |
| void VerifyClipboardTypeExists(ClipboardContentType type, bool exists) { |
| __block BOOL callback_called = NO; |
| __block BOOL type_exists = NO; |
| std::set<ClipboardContentType> types; |
| types.insert(type); |
| clipboard_content_->HasRecentContentFromClipboard( |
| types, base::BindOnce(^(std::set<ClipboardContentType> found_types) { |
| callback_called = YES; |
| type_exists = found_types.find(type) != found_types.end(); |
| })); |
| |
| EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForCookiesTimeout, ^bool { |
| base::RunLoop().RunUntilIdle(); |
| return callback_called; |
| })); |
| EXPECT_EQ(exists, type_exists); |
| } |
| |
| void VerifyClipboardURLExists(const char* expected_url) { |
| VerifyClipboardTypeExists(ClipboardContentType::URL, true); |
| |
| __block BOOL callback_called = NO; |
| __block absl::optional<GURL> optional_gurl; |
| clipboard_content_->GetRecentURLFromClipboard( |
| base::BindOnce(^(absl::optional<GURL> copied_url) { |
| optional_gurl = copied_url; |
| callback_called = YES; |
| })); |
| EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForCookiesTimeout, ^bool { |
| base::RunLoop().RunUntilIdle(); |
| return callback_called; |
| })); |
| ASSERT_TRUE(optional_gurl.has_value()); |
| EXPECT_STREQ(expected_url, optional_gurl.value().spec().c_str()); |
| } |
| |
| bool VerifyCacheClipboardContentTypeExists(ClipboardContentType type) { |
| absl::optional<std::set<ClipboardContentType>> cached_content_types = |
| clipboard_content_->GetCachedClipboardContentTypes(); |
| if (cached_content_types.has_value()) { |
| return cached_content_types.value().find(type) != |
| cached_content_types.value().end(); |
| } else { |
| return false; |
| } |
| } |
| |
| void VerifiyClipboardURLIsInvalid() { |
| // On iOS 13, the url can be instantly read and marked as "does not exist". |
| // On iOS 14, the URL will appear as "exists" until it is actually checked. |
| if (@available(iOS 14, *)) { |
| VerifyClipboardTypeExists(ClipboardContentType::URL, true); |
| } else { |
| VerifyClipboardTypeExists(ClipboardContentType::URL, false); |
| return; |
| } |
| |
| __block BOOL callback_called = NO; |
| __block absl::optional<GURL> optional_gurl; |
| clipboard_content_->GetRecentURLFromClipboard( |
| base::BindOnce(^(absl::optional<GURL> copied_url) { |
| optional_gurl = copied_url; |
| callback_called = YES; |
| })); |
| EXPECT_TRUE(WaitUntilConditionOrTimeout(kWaitForCookiesTimeout, ^bool { |
| base::RunLoop().RunUntilIdle(); |
| return callback_called; |
| })); |
| EXPECT_FALSE(optional_gurl.has_value()); |
| } |
| |
| bool WaitForClipboardContentTypesRefresh() { |
| bool success = WaitUntilConditionOrTimeout(kWaitForActionTimeout, ^bool() { |
| return clipboard_content_->GetCachedClipboardContentTypes().has_value(); |
| }); |
| |
| return success; |
| } |
| }; |
| |
| TEST_F(ClipboardRecentContentIOSTest, SchemeFiltering) { |
| // Test unrecognized URL. |
| SetPasteboardContent(kUnrecognizedURL); |
| VerifiyClipboardURLIsInvalid(); |
| |
| // Test recognized URL. |
| SetPasteboardContent(kRecognizedURL); |
| VerifyClipboardURLExists(kRecognizedURL); |
| |
| // Test URL with app specific scheme. |
| SetPasteboardContent(kAppSpecificURL); |
| VerifyClipboardURLExists(kAppSpecificURL); |
| |
| // Test URL without app specific scheme. |
| ResetClipboardRecentContent(std::string(), base::Days(10)); |
| |
| SetPasteboardContent(kAppSpecificURL); |
| VerifiyClipboardURLIsInvalid(); |
| } |
| |
| TEST_F(ClipboardRecentContentIOSTest, PasteboardURLObsolescence) { |
| SetPasteboardContent(kRecognizedURL); |
| |
| // Test that recent pasteboard data is provided. |
| VerifyClipboardURLExists(kRecognizedURL); |
| |
| // Test that old pasteboard data is not provided. |
| SetStoredPasteboardChangeDate( |
| [NSDate dateWithTimeIntervalSinceNow:-kLongerThanMaxAge]); |
| |
| VerifyClipboardTypeExists(ClipboardContentType::URL, false); |
| VerifyClipboardTypeExists(ClipboardContentType::Text, false); |
| |
| // Tests that if chrome is relaunched, old pasteboard data is still |
| // not provided. |
| ResetClipboardRecentContent(kAppSpecificScheme, base::Days(10)); |
| VerifyClipboardTypeExists(ClipboardContentType::URL, false); |
| VerifyClipboardTypeExists(ClipboardContentType::Text, false); |
| |
| SimulateDeviceRestart(); |
| // Tests that if the device is restarted, old pasteboard data is still |
| // not provided. |
| VerifyClipboardTypeExists(ClipboardContentType::URL, false); |
| VerifyClipboardTypeExists(ClipboardContentType::Text, false); |
| } |
| |
| TEST_F(ClipboardRecentContentIOSTest, |
| CacheClipboardContentTypesUpdatesForCopiedURL) { |
| SetPasteboardContent(kRecognizedURL); |
| ASSERT_TRUE(WaitForClipboardContentTypesRefresh()); |
| |
| EXPECT_TRUE(VerifyCacheClipboardContentTypeExists(ClipboardContentType::URL)); |
| EXPECT_FALSE( |
| VerifyCacheClipboardContentTypeExists(ClipboardContentType::Image)); |
| EXPECT_FALSE( |
| VerifyCacheClipboardContentTypeExists(ClipboardContentType::Text)); |
| } |
| |
| TEST_F(ClipboardRecentContentIOSTest, |
| CacheClipboardContentTypesUpdatesForCopiedImage) { |
| SetPasteboardImage(TestUIImage()); |
| ASSERT_TRUE(WaitForClipboardContentTypesRefresh()); |
| |
| EXPECT_TRUE( |
| VerifyCacheClipboardContentTypeExists(ClipboardContentType::Image)); |
| EXPECT_FALSE( |
| VerifyCacheClipboardContentTypeExists(ClipboardContentType::URL)); |
| EXPECT_FALSE( |
| VerifyCacheClipboardContentTypeExists(ClipboardContentType::Text)); |
| } |
| |
| TEST_F(ClipboardRecentContentIOSTest, |
| CacheClipboardContentTypesUpdatesForCopiedText) { |
| SetPasteboardContent("foobar"); |
| ASSERT_TRUE(WaitForClipboardContentTypesRefresh()); |
| |
| EXPECT_TRUE( |
| VerifyCacheClipboardContentTypeExists(ClipboardContentType::Text)); |
| EXPECT_FALSE( |
| VerifyCacheClipboardContentTypeExists(ClipboardContentType::Image)); |
| EXPECT_FALSE( |
| VerifyCacheClipboardContentTypeExists(ClipboardContentType::URL)); |
| } |
| |
| // Checks that if the pasteboard is marked as having confidential data, it is |
| // not returned. |
| TEST_F(ClipboardRecentContentIOSTest, ConfidentialPasteboardText) { |
| [[UIPasteboard generalPasteboard] |
| setItems:@[ @{ |
| @"public.plain-text" : @"hunter2", |
| @"org.nspasteboard.ConcealedType" : @"hunter2" |
| } ] |
| options:@{}]; |
| |
| VerifyClipboardTypeExists(ClipboardContentType::Text, false); |
| } |
| |
| // Checks that if the user suppresses content, no text will be returned, |
| // and if the text changes, the new text will be returned again. |
| TEST_F(ClipboardRecentContentIOSTest, SuppressedPasteboardContent) { |
| SetPasteboardContent(kRecognizedURL); |
| |
| // Test that recent pasteboard data is provided. |
| VerifyClipboardURLExists(kRecognizedURL); |
| |
| // Suppress the content of the pasteboard. |
| clipboard_content_->SuppressClipboardContent(); |
| |
| // Check that the pasteboard content is suppressed. |
| VerifyClipboardTypeExists(ClipboardContentType::URL, false); |
| |
| // Create a new clipboard content to test persistence. |
| ResetClipboardRecentContent(kAppSpecificScheme, base::Days(10)); |
| |
| // Check that the pasteboard content is still suppressed. |
| VerifyClipboardTypeExists(ClipboardContentType::URL, false); |
| |
| // Check that even if the device is restarted, pasteboard content is |
| // still suppressed. |
| SimulateDeviceRestart(); |
| VerifyClipboardTypeExists(ClipboardContentType::URL, false); |
| |
| // Check that if the pasteboard changes, the new content is not |
| // suppressed anymore. |
| SetPasteboardContent(kRecognizedURL2); |
| VerifyClipboardURLExists(kRecognizedURL2); |
| } |
| |
| // Checks that if the user suppresses content, no image will be returned, |
| // and if the image changes, the new image will be returned again. |
| TEST_F(ClipboardRecentContentIOSTest, SuppressedPasteboardImage) { |
| SetPasteboardImage(TestUIImage()); |
| |
| // Test that recent pasteboard data is provided. |
| VerifyClipboardTypeExists(ClipboardContentType::Image, true); |
| |
| // Suppress the content of the pasteboard. |
| clipboard_content_->SuppressClipboardContent(); |
| |
| // Check that the pasteboard content is suppressed. |
| VerifyClipboardTypeExists(ClipboardContentType::Image, false); |
| |
| // Create a new clipboard content to test persistence. |
| ResetClipboardRecentContent(kAppSpecificScheme, base::Days(10)); |
| |
| // Check that the pasteboard content is still suppressed. |
| VerifyClipboardTypeExists(ClipboardContentType::Image, false); |
| |
| // Check that even if the device is restarted, pasteboard content is |
| // still suppressed. |
| SimulateDeviceRestart(); |
| VerifyClipboardTypeExists(ClipboardContentType::Image, false); |
| |
| // Check that if the pasteboard changes, the new content is not |
| // suppressed anymore. |
| SetPasteboardImage(TestUIImage([UIColor greenColor])); |
| VerifyClipboardTypeExists(ClipboardContentType::Image, true); |
| } |
| |
| // Checks that if user copies something other than a string we don't cache the |
| // string in pasteboard. |
| TEST_F(ClipboardRecentContentIOSTest, AddingNonStringRemovesCachedString) { |
| SetPasteboardContent(kRecognizedURL); |
| |
| // Test that recent pasteboard data is provided as url. |
| VerifyClipboardURLExists(kRecognizedURL); |
| // Because iOS 14 has to use a different API to detect clipboard contents, it |
| // is empty, while the clipboard type should exist on iOS 13. |
| if (@available(iOS 14, *)) { |
| VerifyClipboardTypeExists(ClipboardContentType::Text, false); |
| } else { |
| VerifyClipboardTypeExists(ClipboardContentType::Text, true); |
| } |
| // Image pasteboard should be empty. |
| VerifyClipboardTypeExists(ClipboardContentType::Image, false); |
| |
| // Overwrite pasteboard with an image. |
| SetPasteboardImage(TestUIImage()); |
| |
| // Url and text pasteboard should appear empty. |
| VerifyClipboardTypeExists(ClipboardContentType::URL, false); |
| VerifyClipboardTypeExists(ClipboardContentType::Text, false); |
| // Image pasteboard should be full |
| VerifyClipboardTypeExists(ClipboardContentType::Image, true); |
| |
| // Tests that if URL is added again, pasteboard provides it normally. |
| SetPasteboardContent(kRecognizedURL); |
| VerifyClipboardURLExists(kRecognizedURL); |
| // Because iOS 14 has to use a different API to detect clipboard contents, it |
| // is empty, while the clipboard type should exist on iOS 13. |
| if (@available(iOS 14, *)) { |
| VerifyClipboardTypeExists(ClipboardContentType::Text, false); |
| } else { |
| VerifyClipboardTypeExists(ClipboardContentType::Text, true); |
| } |
| // Image pasteboard should be empty. |
| VerifyClipboardTypeExists(ClipboardContentType::Image, false); |
| } |