Chromium Code Reviews| Index: components/open_from_clipboard/clipboard_recent_content_ios.mm |
| diff --git a/components/open_from_clipboard/clipboard_recent_content_ios.mm b/components/open_from_clipboard/clipboard_recent_content_ios.mm |
| index d27d2566eb2d3f4b2f893cdbe50c517cd46fbaef..d343f4a723e0fd45a6648d8a1856568087207c4f 100644 |
| --- a/components/open_from_clipboard/clipboard_recent_content_ios.mm |
| +++ b/components/open_from_clipboard/clipboard_recent_content_ios.mm |
| @@ -4,6 +4,7 @@ |
| #import "components/open_from_clipboard/clipboard_recent_content_ios.h" |
| +#import <CommonCrypto/CommonDigest.h> |
| #import <UIKit/UIKit.h> |
| #import "base/ios/ios_util.h" |
| @@ -48,7 +49,7 @@ |
| object:[UIPasteboard generalPasteboard]]; |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| - selector:@selector(pasteboardChangedNotification:) |
| + selector:@selector(didBecomeActive:) |
| name:UIApplicationDidBecomeActiveNotification |
| object:nil]; |
| } |
| @@ -66,6 +67,17 @@ |
| } |
| } |
| +- (void)didBecomeActive:(NSNotification*)notification { |
| + if (_delegate) { |
| + _delegate->LoadFromUserDefaults(); |
| + base::TimeDelta uptime = |
| + base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime()); |
| + if (_delegate->HasPasteboardChanged(uptime)) { |
| + _delegate->PasteboardChanged(); |
| + } |
| + } |
| +} |
| + |
| - (void)disconnect { |
| _delegate = nullptr; |
| } |
| @@ -80,8 +92,12 @@ NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount"; |
| // Key used to store the last date at which it was detected that the pasteboard |
| // changed. It is used to evaluate the age of the pasteboard's content. |
| NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate"; |
| -// Key used to store the |
| -NSString* kSuppressedPasteboardEntryCountKey = @"PasteboardSupressedEntryCount"; |
| +// Key used to store the hash of the content of the pasteboard. Whenever the |
| +// hash changed, the pasteboard content is considered to have changed. |
| +NSString* kPasteboardEntryMD5Key = @"PasteboardEntryMD5"; |
| +// Key used to store the date of the latest pasteboard entry displayed in the |
| +// omnibox. This is used to report metrics on pasteboard change. |
| +NSString* kLastDisplayedPasteboardEntryKey = @"LastDisplayedPasteboardEntry"; |
| base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3); |
| // Schemes accepted by the ClipboardRecentContentIOS. |
| const char* kAuthorizedSchemes[] = { |
| @@ -90,18 +106,29 @@ const char* kAuthorizedSchemes[] = { |
| url::kDataScheme, |
| url::kAboutScheme, |
| }; |
| + |
| +// Compute a hash consisting of the first 4 bytes of the MD5 hash of |string|. |
| +// This value is used to detect pasteboard content change. Keeping only 4 bytes |
| +// is a privacy requirement to introduce collision and allow deniability of |
| +// having copied a given string. |
| +NSData* WeakMD5FromNSString(NSString* string) { |
| + unsigned char hash[CC_MD5_DIGEST_LENGTH]; |
| + const char* c_string = [string UTF8String]; |
| + CC_MD5(c_string, strlen(c_string), hash); |
| + NSData* data = [NSData dataWithBytes:hash length:4]; |
| + return data; |
| +} |
| + |
| } // namespace |
| bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const { |
| DCHECK(url); |
| - if (GetClipboardContentAge() > kMaximumAgeOfClipboard || |
| - [UIPasteboard generalPasteboard].changeCount == |
| - suppressedPasteboardEntryCount_) { |
| + if (GetClipboardContentAge() > kMaximumAgeOfClipboard) { |
| return false; |
| } |
| - if (urlFromPasteboardCache_.is_valid()) { |
| - *url = urlFromPasteboardCache_; |
| + if (url_from_pasteboard_cache_.is_valid()) { |
| + *url = url_from_pasteboard_cache_; |
| return true; |
| } |
| return false; |
| @@ -109,81 +136,90 @@ bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const { |
| base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { |
| return base::TimeDelta::FromSeconds( |
| - static_cast<int64>(-[lastPasteboardChangeDate_ timeIntervalSinceNow])); |
| + static_cast<int64>(-[last_pasteboard_change_date_ timeIntervalSinceNow])); |
| } |
| void ClipboardRecentContentIOS::SuppressClipboardContent() { |
| - suppressedPasteboardEntryCount_ = |
| - [UIPasteboard generalPasteboard].changeCount; |
| + // User cleared the user data. The pasteboard entry must be removed from the |
| + // omnibox list. Force entry expiration by setting copy date to 1970. |
| + last_pasteboard_change_date_.reset( |
| + [[NSDate alloc] initWithTimeIntervalSince1970:0]); |
| SaveToUserDefaults(); |
| } |
| void ClipboardRecentContentIOS::PasteboardChanged() { |
| - urlFromPasteboardCache_ = URLFromPasteboard(); |
| - if (!urlFromPasteboardCache_.is_empty()) { |
| + url_from_pasteboard_cache_ = URLFromPasteboard(); |
| + if (!url_from_pasteboard_cache_.is_empty()) { |
| base::RecordAction( |
| base::UserMetricsAction("MobileOmniboxClipboardChanged")); |
| } |
| - lastPasteboardChangeDate_.reset([[NSDate date] retain]); |
| - lastPasteboardChangeCount_ = [UIPasteboard generalPasteboard].changeCount; |
| - if (lastPasteboardChangeCount_ != suppressedPasteboardEntryCount_) { |
| - suppressedPasteboardEntryCount_ = NSIntegerMax; |
| + last_pasteboard_change_date_.reset([[NSDate date] retain]); |
| + last_pasteboard_change_count_ = [UIPasteboard generalPasteboard].changeCount; |
| + NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; |
| + if (!pasteboard_string) { |
| + pasteboard_string = @""; |
| } |
| + NSData* MD5 = WeakMD5FromNSString(pasteboard_string); |
| + last_pasteboard_entry_md5_.reset([MD5 retain]); |
| + SaveToUserDefaults(); |
| } |
| ClipboardRecentContentIOS::ClipboardRecentContentIOS( |
| - const std::string& application_scheme) |
| - : application_scheme_(application_scheme) { |
| + const std::string& application_scheme, |
| + NSUserDefaults* group_user_defaults) |
| + : application_scheme_(application_scheme), |
| + shared_user_defaults_([group_user_defaults retain]) { |
| Init(base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime())); |
| } |
| ClipboardRecentContentIOS::ClipboardRecentContentIOS( |
| const std::string& application_scheme, |
| base::TimeDelta uptime) |
| - : application_scheme_(application_scheme) { |
| + : application_scheme_(application_scheme), |
| + shared_user_defaults_([[NSUserDefaults standardUserDefaults] retain]) { |
| Init(uptime); |
| } |
| +bool ClipboardRecentContentIOS::HasPasteboardChanged(base::TimeDelta uptime) { |
| + NSInteger change_count = [UIPasteboard generalPasteboard].changeCount; |
| + bool change_count_changed = change_count != last_pasteboard_change_count_; |
| + |
| + bool not_rebooted = uptime > GetClipboardContentAge(); |
|
droger
2015/08/13 11:42:21
Optional: should we early return here if (not_rebo
Olivier
2015/08/13 12:01:58
Done.
Olivier
2015/08/13 12:01:58
Done.
|
| + |
| + NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; |
| + if (!pasteboard_string) { |
| + pasteboard_string = @""; |
| + } |
| + NSData* md5 = WeakMD5FromNSString(pasteboard_string); |
| + BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_]; |
|
droger
2015/08/13 11:42:21
What happens if there is a collision? Is this a pr
droger
2015/08/13 11:51:52
Resolved by offline discussion: hash collisions ar
|
| + |
| + // If |MD5Changed|, we know for sure there has been at least one pasteboard |
| + // copy since last time it was checked. |
| + // If the pasteboard content is still the same but the device was not |
| + // rebooted, the change count can be checked to see if it changed. |
| + // Note: due to a mismatch between the actual behavior and documentation, and |
| + // lack of consistency on different reboot scenarios, the change count cannot |
| + // be checked after a reboot. |
| + // See radar://21833556 for more information. |
| + return md5_changed || (not_rebooted && change_count_changed); |
| +} |
| + |
| void ClipboardRecentContentIOS::Init(base::TimeDelta uptime) { |
| - lastPasteboardChangeCount_ = NSIntegerMax; |
| - suppressedPasteboardEntryCount_ = NSIntegerMax; |
| - urlFromPasteboardCache_ = URLFromPasteboard(); |
| + last_pasteboard_change_count_ = NSIntegerMax; |
| + url_from_pasteboard_cache_ = URLFromPasteboard(); |
| LoadFromUserDefaults(); |
| - // On iOS 7 (unlike on iOS 8, despite what the documentation says), the change |
| - // count is reset when the device is rebooted. |
| - if (uptime < GetClipboardContentAge() && |
| - !base::ios::IsRunningOnIOS8OrLater()) { |
| - if ([UIPasteboard generalPasteboard].changeCount == 0) { |
| - // The user hasn't pasted anything in the clipboard since the device's |
| - // reboot. |PasteboardChanged| isn't called because it would update |
| - // |lastPasteboardChangeData_|, and record metrics. |
| - lastPasteboardChangeCount_ = 0; |
| - if (suppressedPasteboardEntryCount_ != NSIntegerMax) { |
| - // If the last time Chrome was running the pasteboard was suppressed, |
| - // and the user has not copied anything since the device launched, then |
| - // supress this entry. |
| - suppressedPasteboardEntryCount_ = 0; |
| - } |
| - SaveToUserDefaults(); |
| - } else { |
| - // The user pasted something in the clipboard since the device's reboot. |
| - PasteboardChanged(); |
| - } |
| - } else { |
| - NSInteger changeCount = [UIPasteboard generalPasteboard].changeCount; |
| - if (changeCount != lastPasteboardChangeCount_) { |
| - PasteboardChanged(); |
| - } |
| - } |
| - // Makes sure |lastPasteboardChangeCount_| was properly initialized. |
| - DCHECK_NE(lastPasteboardChangeCount_, NSIntegerMax); |
| - notificationBridge_.reset( |
| + if (HasPasteboardChanged(uptime)) |
| + PasteboardChanged(); |
| + |
| + // Makes sure |last_pasteboard_change_count_| was properly initialized. |
| + DCHECK_NE(last_pasteboard_change_count_, NSIntegerMax); |
| + notification_bridge_.reset( |
| [[PasteboardNotificationListenerBridge alloc] initWithDelegate:this]); |
| } |
| ClipboardRecentContentIOS::~ClipboardRecentContentIOS() { |
| - [notificationBridge_ disconnect]; |
| + [notification_bridge_ disconnect]; |
| } |
| GURL ClipboardRecentContentIOS::URLFromPasteboard() { |
| @@ -204,32 +240,37 @@ GURL ClipboardRecentContentIOS::URLFromPasteboard() { |
| return GURL::EmptyGURL(); |
| } |
| -void ClipboardRecentContentIOS::LoadFromUserDefaults() { |
| - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; |
| - |
| - lastPasteboardChangeCount_ = |
| - [defaults integerForKey:kPasteboardChangeCountKey]; |
| - lastPasteboardChangeDate_.reset( |
| - [[defaults objectForKey:kPasteboardChangeDateKey] retain]); |
| - |
| - if ([[[defaults dictionaryRepresentation] allKeys] |
| - containsObject:kSuppressedPasteboardEntryCountKey]) { |
| - suppressedPasteboardEntryCount_ = |
| - [defaults integerForKey:kSuppressedPasteboardEntryCountKey]; |
| - } else { |
| - suppressedPasteboardEntryCount_ = NSIntegerMax; |
| +void ClipboardRecentContentIOS::RecentURLDisplayed() { |
| + if ([last_pasteboard_change_date_ |
| + isEqualToDate:last_displayed_pasteboard_entry_.get()]) { |
| + return; |
| } |
| + base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged")); |
| + last_pasteboard_change_date_ = last_displayed_pasteboard_entry_; |
| + SaveToUserDefaults(); |
| +} |
| + |
| +void ClipboardRecentContentIOS::LoadFromUserDefaults() { |
| + last_pasteboard_change_count_ = |
| + [shared_user_defaults_ integerForKey:kPasteboardChangeCountKey]; |
| + last_pasteboard_change_date_.reset( |
| + [[shared_user_defaults_ objectForKey:kPasteboardChangeDateKey] retain]); |
| + last_pasteboard_entry_md5_.reset( |
| + [[shared_user_defaults_ objectForKey:kPasteboardEntryMD5Key] retain]); |
| + last_displayed_pasteboard_entry_.reset([[shared_user_defaults_ |
| + objectForKey:kLastDisplayedPasteboardEntryKey] retain]); |
| - DCHECK(!lastPasteboardChangeDate_ || |
| - [lastPasteboardChangeDate_ isKindOfClass:[NSDate class]]); |
| + DCHECK(!last_pasteboard_change_date_ || |
| + [last_pasteboard_change_date_ isKindOfClass:[NSDate class]]); |
| } |
| void ClipboardRecentContentIOS::SaveToUserDefaults() { |
| - NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; |
| - [defaults setInteger:lastPasteboardChangeCount_ |
| - forKey:kPasteboardChangeCountKey]; |
| - [defaults setObject:lastPasteboardChangeDate_ |
| - forKey:kPasteboardChangeDateKey]; |
| - [defaults setInteger:suppressedPasteboardEntryCount_ |
| - forKey:kSuppressedPasteboardEntryCountKey]; |
| + [shared_user_defaults_ setInteger:last_pasteboard_change_count_ |
| + forKey:kPasteboardChangeCountKey]; |
| + [shared_user_defaults_ setObject:last_pasteboard_change_date_ |
| + forKey:kPasteboardChangeDateKey]; |
| + [shared_user_defaults_ setObject:last_pasteboard_entry_md5_ |
| + forKey:kPasteboardEntryMD5Key]; |
| + [shared_user_defaults_ setObject:last_displayed_pasteboard_entry_ |
| + forKey:kLastDisplayedPasteboardEntryKey]; |
| } |