1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "components/open_from_clipboard/clipboard_recent_content_ios.h"
7 #import <CommonCrypto/CommonDigest.h>
8 #import <UIKit/UIKit.h>
10 #import "base/ios/ios_util.h"
11 #include "base/logging.h"
12 #include "base/macros.h"
13 #include "base/metrics/user_metrics.h"
14 #include "base/strings/sys_string_conversions.h"
15 #include "base/sys_info.h"
17 #include "url/url_constants.h"
19 // Bridge that forwards pasteboard change notifications to its delegate.
20 @interface PasteboardNotificationListenerBridge : NSObject
22 // Initialize the PasteboardNotificationListenerBridge with |delegate| which
24 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate
25 NS_DESIGNATED_INITIALIZER;
27 - (instancetype)init NS_UNAVAILABLE;
31 @implementation PasteboardNotificationListenerBridge {
32 ClipboardRecentContentIOS* _delegate;
35 - (instancetype)init {
40 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate {
45 [[NSNotificationCenter defaultCenter]
47 selector:@selector(pasteboardChangedNotification:)
48 name:UIPasteboardChangedNotification
49 object:[UIPasteboard generalPasteboard]];
50 [[NSNotificationCenter defaultCenter]
52 selector:@selector(didBecomeActive:)
53 name:UIApplicationDidBecomeActiveNotification
60 [[NSNotificationCenter defaultCenter] removeObserver:self];
64 - (void)pasteboardChangedNotification:(NSNotification*)notification {
66 _delegate->PasteboardChanged();
70 - (void)didBecomeActive:(NSNotification*)notification {
72 _delegate->LoadFromUserDefaults();
73 base::TimeDelta uptime =
74 base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime());
75 if (_delegate->HasPasteboardChanged(uptime)) {
76 _delegate->PasteboardChanged();
88 // Key used to store the pasteboard's current change count. If when resuming
89 // chrome the pasteboard's change count is different from the stored one, then
90 // it means that the pasteboard's content has changed.
91 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount";
92 // Key used to store the last date at which it was detected that the pasteboard
93 // changed. It is used to evaluate the age of the pasteboard's content.
94 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate";
95 // Key used to store the hash of the content of the pasteboard. Whenever the
96 // hash changed, the pasteboard content is considered to have changed.
97 NSString* kPasteboardEntryMD5Key = @"PasteboardEntryMD5";
98 // Key used to store the date of the latest pasteboard entry displayed in the
99 // omnibox. This is used to report metrics on pasteboard change.
100 NSString* kLastDisplayedPasteboardEntryKey = @"LastDisplayedPasteboardEntry";
101 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3);
102 // Schemes accepted by the ClipboardRecentContentIOS.
103 const char* kAuthorizedSchemes[] = {
110 // Compute a hash consisting of the first 4 bytes of the MD5 hash of |string|.
111 // This value is used to detect pasteboard content change. Keeping only 4 bytes
112 // is a privacy requirement to introduce collision and allow deniability of
113 // having copied a given string.
114 NSData* WeakMD5FromNSString(NSString* string) {
115 unsigned char hash[CC_MD5_DIGEST_LENGTH];
116 const char* c_string = [string UTF8String];
117 CC_MD5(c_string, strlen(c_string), hash);
118 NSData* data = [NSData dataWithBytes:hash length:4];
124 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const {
126 if (GetClipboardContentAge() > kMaximumAgeOfClipboard) {
130 if (url_from_pasteboard_cache_.is_valid()) {
131 *url = url_from_pasteboard_cache_;
137 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const {
138 return base::TimeDelta::FromSeconds(
139 static_cast<int64>(-[last_pasteboard_change_date_ timeIntervalSinceNow]));
142 void ClipboardRecentContentIOS::SuppressClipboardContent() {
143 // User cleared the user data. The pasteboard entry must be removed from the
144 // omnibox list. Force entry expiration by setting copy date to 1970.
145 last_pasteboard_change_date_.reset(
146 [[NSDate alloc] initWithTimeIntervalSince1970:0]);
147 SaveToUserDefaults();
150 void ClipboardRecentContentIOS::PasteboardChanged() {
151 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string];
152 if (!pasteboard_string)
154 url_from_pasteboard_cache_ = URLFromPasteboard();
155 if (!url_from_pasteboard_cache_.is_empty()) {
157 base::UserMetricsAction("MobileOmniboxClipboardChanged"));
159 last_pasteboard_change_date_.reset([[NSDate date] retain]);
160 last_pasteboard_change_count_ = [UIPasteboard generalPasteboard].changeCount;
161 NSData* MD5 = WeakMD5FromNSString(pasteboard_string);
162 last_pasteboard_entry_md5_.reset([MD5 retain]);
163 SaveToUserDefaults();
166 ClipboardRecentContentIOS::ClipboardRecentContentIOS(
167 const std::string& application_scheme,
168 NSUserDefaults* group_user_defaults)
169 : application_scheme_(application_scheme),
170 shared_user_defaults_([group_user_defaults retain]) {
171 Init(base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime()));
174 ClipboardRecentContentIOS::ClipboardRecentContentIOS(
175 const std::string& application_scheme,
176 base::TimeDelta uptime)
177 : application_scheme_(application_scheme),
178 shared_user_defaults_([[NSUserDefaults standardUserDefaults] retain]) {
182 bool ClipboardRecentContentIOS::HasPasteboardChanged(base::TimeDelta uptime) {
183 // If [[UIPasteboard generalPasteboard] string] is nil, the content of the
184 // pasteboard cannot be accessed. This case should not be considered as a
185 // pasteboard change.
186 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string];
187 if (!pasteboard_string)
190 // If |MD5Changed|, we know for sure there has been at least one pasteboard
191 // copy since last time it was checked.
192 // If the pasteboard content is still the same but the device was not
193 // rebooted, the change count can be checked to see if it changed.
194 // Note: due to a mismatch between the actual behavior and documentation, and
195 // lack of consistency on different reboot scenarios, the change count cannot
196 // be checked after a reboot.
197 // See radar://21833556 for more information.
198 NSInteger change_count = [UIPasteboard generalPasteboard].changeCount;
199 bool change_count_changed = change_count != last_pasteboard_change_count_;
201 bool not_rebooted = uptime > GetClipboardContentAge();
203 return change_count_changed;
205 NSData* md5 = WeakMD5FromNSString(pasteboard_string);
206 BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_];
211 void ClipboardRecentContentIOS::Init(base::TimeDelta uptime) {
212 last_pasteboard_change_count_ = NSIntegerMax;
213 url_from_pasteboard_cache_ = URLFromPasteboard();
214 LoadFromUserDefaults();
216 if (HasPasteboardChanged(uptime))
219 // Makes sure |last_pasteboard_change_count_| was properly initialized.
220 DCHECK_NE(last_pasteboard_change_count_, NSIntegerMax);
221 notification_bridge_.reset(
222 [[PasteboardNotificationListenerBridge alloc] initWithDelegate:this]);
225 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() {
226 [notification_bridge_ disconnect];
229 GURL ClipboardRecentContentIOS::URLFromPasteboard() {
230 NSString* clipboard_string = [[UIPasteboard generalPasteboard] string];
231 if (!clipboard_string) {
232 return GURL::EmptyGURL();
234 const std::string clipboard = base::SysNSStringToUTF8(clipboard_string);
235 GURL gurl = GURL(clipboard);
236 if (gurl.is_valid()) {
237 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) {
238 if (gurl.SchemeIs(kAuthorizedSchemes[i])) {
242 if (!application_scheme_.empty() &&
243 gurl.SchemeIs(application_scheme_.c_str())) {
247 return GURL::EmptyGURL();
250 void ClipboardRecentContentIOS::RecentURLDisplayed() {
251 if ([last_pasteboard_change_date_
252 isEqualToDate:last_displayed_pasteboard_entry_.get()]) {
255 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged"));
256 last_pasteboard_change_date_ = last_displayed_pasteboard_entry_;
257 SaveToUserDefaults();
260 void ClipboardRecentContentIOS::LoadFromUserDefaults() {
261 last_pasteboard_change_count_ =
262 [shared_user_defaults_ integerForKey:kPasteboardChangeCountKey];
263 last_pasteboard_change_date_.reset(
264 [[shared_user_defaults_ objectForKey:kPasteboardChangeDateKey] retain]);
265 last_pasteboard_entry_md5_.reset(
266 [[shared_user_defaults_ objectForKey:kPasteboardEntryMD5Key] retain]);
267 last_displayed_pasteboard_entry_.reset([[shared_user_defaults_
268 objectForKey:kLastDisplayedPasteboardEntryKey] retain]);
270 DCHECK(!last_pasteboard_change_date_ ||
271 [last_pasteboard_change_date_ isKindOfClass:[NSDate class]]);
274 void ClipboardRecentContentIOS::SaveToUserDefaults() {
275 [shared_user_defaults_ setInteger:last_pasteboard_change_count_
276 forKey:kPasteboardChangeCountKey];
277 [shared_user_defaults_ setObject:last_pasteboard_change_date_
278 forKey:kPasteboardChangeDateKey];
279 [shared_user_defaults_ setObject:last_pasteboard_entry_md5_
280 forKey:kPasteboardEntryMD5Key];
281 [shared_user_defaults_ setObject:last_displayed_pasteboard_entry_
282 forKey:kLastDisplayedPasteboardEntryKey];