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..7665e46731dd30992369991cabd68b0035c3ae39 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,92 @@ 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) { |
+ // 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. |
+ NSInteger change_count = [UIPasteboard generalPasteboard].changeCount; |
+ bool change_count_changed = change_count != last_pasteboard_change_count_; |
+ |
+ bool not_rebooted = uptime > GetClipboardContentAge(); |
+ if (not_rebooted) |
+ return change_count_changed; |
+ |
+ 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_]; |
+ |
+ return md5_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 +242,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]; |
} |