| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #import "components/open_from_clipboard/clipboard_recent_content_ios.h" | 5 #import "components/open_from_clipboard/clipboard_recent_content_ios.h" |
| 6 | 6 |
| 7 #import <CommonCrypto/CommonDigest.h> |
| 7 #import <UIKit/UIKit.h> | 8 #import <UIKit/UIKit.h> |
| 8 | 9 |
| 9 #import "base/ios/ios_util.h" | 10 #import "base/ios/ios_util.h" |
| 10 #include "base/logging.h" | 11 #include "base/logging.h" |
| 11 #include "base/macros.h" | 12 #include "base/macros.h" |
| 12 #include "base/metrics/user_metrics.h" | 13 #include "base/metrics/user_metrics.h" |
| 13 #include "base/strings/sys_string_conversions.h" | 14 #include "base/strings/sys_string_conversions.h" |
| 14 #include "base/sys_info.h" | 15 #include "base/sys_info.h" |
| 15 #include "url/gurl.h" | 16 #include "url/gurl.h" |
| 16 #include "url/url_constants.h" | 17 #include "url/url_constants.h" |
| (...skipping 24 matching lines...) Expand all Loading... |
| 41 self = [super init]; | 42 self = [super init]; |
| 42 if (self) { | 43 if (self) { |
| 43 _delegate = delegate; | 44 _delegate = delegate; |
| 44 [[NSNotificationCenter defaultCenter] | 45 [[NSNotificationCenter defaultCenter] |
| 45 addObserver:self | 46 addObserver:self |
| 46 selector:@selector(pasteboardChangedNotification:) | 47 selector:@selector(pasteboardChangedNotification:) |
| 47 name:UIPasteboardChangedNotification | 48 name:UIPasteboardChangedNotification |
| 48 object:[UIPasteboard generalPasteboard]]; | 49 object:[UIPasteboard generalPasteboard]]; |
| 49 [[NSNotificationCenter defaultCenter] | 50 [[NSNotificationCenter defaultCenter] |
| 50 addObserver:self | 51 addObserver:self |
| 51 selector:@selector(pasteboardChangedNotification:) | 52 selector:@selector(didBecomeActive:) |
| 52 name:UIApplicationDidBecomeActiveNotification | 53 name:UIApplicationDidBecomeActiveNotification |
| 53 object:nil]; | 54 object:nil]; |
| 54 } | 55 } |
| 55 return self; | 56 return self; |
| 56 } | 57 } |
| 57 | 58 |
| 58 - (void)dealloc { | 59 - (void)dealloc { |
| 59 [[NSNotificationCenter defaultCenter] removeObserver:self]; | 60 [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 60 [super dealloc]; | 61 [super dealloc]; |
| 61 } | 62 } |
| 62 | 63 |
| 63 - (void)pasteboardChangedNotification:(NSNotification*)notification { | 64 - (void)pasteboardChangedNotification:(NSNotification*)notification { |
| 64 if (_delegate) { | 65 if (_delegate) { |
| 65 _delegate->PasteboardChanged(); | 66 _delegate->PasteboardChanged(); |
| 66 } | 67 } |
| 67 } | 68 } |
| 68 | 69 |
| 70 - (void)didBecomeActive:(NSNotification*)notification { |
| 71 if (_delegate) { |
| 72 _delegate->LoadFromUserDefaults(); |
| 73 base::TimeDelta uptime = |
| 74 base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime()); |
| 75 if (_delegate->HasPasteboardChanged(uptime)) { |
| 76 _delegate->PasteboardChanged(); |
| 77 } |
| 78 } |
| 79 } |
| 80 |
| 69 - (void)disconnect { | 81 - (void)disconnect { |
| 70 _delegate = nullptr; | 82 _delegate = nullptr; |
| 71 } | 83 } |
| 72 | 84 |
| 73 @end | 85 @end |
| 74 | 86 |
| 75 namespace { | 87 namespace { |
| 76 // Key used to store the pasteboard's current change count. If when resuming | 88 // Key used to store the pasteboard's current change count. If when resuming |
| 77 // chrome the pasteboard's change count is different from the stored one, then | 89 // chrome the pasteboard's change count is different from the stored one, then |
| 78 // it means that the pasteboard's content has changed. | 90 // it means that the pasteboard's content has changed. |
| 79 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount"; | 91 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount"; |
| 80 // Key used to store the last date at which it was detected that the pasteboard | 92 // Key used to store the last date at which it was detected that the pasteboard |
| 81 // changed. It is used to evaluate the age of the pasteboard's content. | 93 // changed. It is used to evaluate the age of the pasteboard's content. |
| 82 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate"; | 94 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate"; |
| 83 // Key used to store the | 95 // Key used to store the hash of the content of the pasteboard. Whenever the |
| 84 NSString* kSuppressedPasteboardEntryCountKey = @"PasteboardSupressedEntryCount"; | 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"; |
| 85 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3); | 101 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3); |
| 86 // Schemes accepted by the ClipboardRecentContentIOS. | 102 // Schemes accepted by the ClipboardRecentContentIOS. |
| 87 const char* kAuthorizedSchemes[] = { | 103 const char* kAuthorizedSchemes[] = { |
| 88 url::kHttpScheme, | 104 url::kHttpScheme, |
| 89 url::kHttpsScheme, | 105 url::kHttpsScheme, |
| 90 url::kDataScheme, | 106 url::kDataScheme, |
| 91 url::kAboutScheme, | 107 url::kAboutScheme, |
| 92 }; | 108 }; |
| 109 |
| 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]; |
| 119 return data; |
| 120 } |
| 121 |
| 93 } // namespace | 122 } // namespace |
| 94 | 123 |
| 95 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const { | 124 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const { |
| 96 DCHECK(url); | 125 DCHECK(url); |
| 97 if (GetClipboardContentAge() > kMaximumAgeOfClipboard || | 126 if (GetClipboardContentAge() > kMaximumAgeOfClipboard) { |
| 98 [UIPasteboard generalPasteboard].changeCount == | |
| 99 suppressedPasteboardEntryCount_) { | |
| 100 return false; | 127 return false; |
| 101 } | 128 } |
| 102 | 129 |
| 103 if (urlFromPasteboardCache_.is_valid()) { | 130 if (url_from_pasteboard_cache_.is_valid()) { |
| 104 *url = urlFromPasteboardCache_; | 131 *url = url_from_pasteboard_cache_; |
| 105 return true; | 132 return true; |
| 106 } | 133 } |
| 107 return false; | 134 return false; |
| 108 } | 135 } |
| 109 | 136 |
| 110 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { | 137 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { |
| 111 return base::TimeDelta::FromSeconds( | 138 return base::TimeDelta::FromSeconds( |
| 112 static_cast<int64>(-[lastPasteboardChangeDate_ timeIntervalSinceNow])); | 139 static_cast<int64>(-[last_pasteboard_change_date_ timeIntervalSinceNow])); |
| 113 } | 140 } |
| 114 | 141 |
| 115 void ClipboardRecentContentIOS::SuppressClipboardContent() { | 142 void ClipboardRecentContentIOS::SuppressClipboardContent() { |
| 116 suppressedPasteboardEntryCount_ = | 143 // User cleared the user data. The pasteboard entry must be removed from the |
| 117 [UIPasteboard generalPasteboard].changeCount; | 144 // omnibox list. Force entry expiration by setting copy date to 1970. |
| 145 last_pasteboard_change_date_.reset( |
| 146 [[NSDate alloc] initWithTimeIntervalSince1970:0]); |
| 118 SaveToUserDefaults(); | 147 SaveToUserDefaults(); |
| 119 } | 148 } |
| 120 | 149 |
| 121 void ClipboardRecentContentIOS::PasteboardChanged() { | 150 void ClipboardRecentContentIOS::PasteboardChanged() { |
| 122 urlFromPasteboardCache_ = URLFromPasteboard(); | 151 url_from_pasteboard_cache_ = URLFromPasteboard(); |
| 123 if (!urlFromPasteboardCache_.is_empty()) { | 152 if (!url_from_pasteboard_cache_.is_empty()) { |
| 124 base::RecordAction( | 153 base::RecordAction( |
| 125 base::UserMetricsAction("MobileOmniboxClipboardChanged")); | 154 base::UserMetricsAction("MobileOmniboxClipboardChanged")); |
| 126 } | 155 } |
| 127 lastPasteboardChangeDate_.reset([[NSDate date] retain]); | 156 last_pasteboard_change_date_.reset([[NSDate date] retain]); |
| 128 lastPasteboardChangeCount_ = [UIPasteboard generalPasteboard].changeCount; | 157 last_pasteboard_change_count_ = [UIPasteboard generalPasteboard].changeCount; |
| 129 if (lastPasteboardChangeCount_ != suppressedPasteboardEntryCount_) { | 158 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; |
| 130 suppressedPasteboardEntryCount_ = NSIntegerMax; | 159 if (!pasteboard_string) { |
| 160 pasteboard_string = @""; |
| 131 } | 161 } |
| 162 NSData* MD5 = WeakMD5FromNSString(pasteboard_string); |
| 163 last_pasteboard_entry_md5_.reset([MD5 retain]); |
| 164 SaveToUserDefaults(); |
| 132 } | 165 } |
| 133 | 166 |
| 134 ClipboardRecentContentIOS::ClipboardRecentContentIOS( | 167 ClipboardRecentContentIOS::ClipboardRecentContentIOS( |
| 135 const std::string& application_scheme) | 168 const std::string& application_scheme, |
| 136 : application_scheme_(application_scheme) { | 169 NSUserDefaults* group_user_defaults) |
| 170 : application_scheme_(application_scheme), |
| 171 shared_user_defaults_([group_user_defaults retain]) { |
| 137 Init(base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime())); | 172 Init(base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime())); |
| 138 } | 173 } |
| 139 | 174 |
| 140 ClipboardRecentContentIOS::ClipboardRecentContentIOS( | 175 ClipboardRecentContentIOS::ClipboardRecentContentIOS( |
| 141 const std::string& application_scheme, | 176 const std::string& application_scheme, |
| 142 base::TimeDelta uptime) | 177 base::TimeDelta uptime) |
| 143 : application_scheme_(application_scheme) { | 178 : application_scheme_(application_scheme), |
| 179 shared_user_defaults_([[NSUserDefaults standardUserDefaults] retain]) { |
| 144 Init(uptime); | 180 Init(uptime); |
| 145 } | 181 } |
| 146 | 182 |
| 183 bool ClipboardRecentContentIOS::HasPasteboardChanged(base::TimeDelta uptime) { |
| 184 // If |MD5Changed|, we know for sure there has been at least one pasteboard |
| 185 // copy since last time it was checked. |
| 186 // If the pasteboard content is still the same but the device was not |
| 187 // rebooted, the change count can be checked to see if it changed. |
| 188 // Note: due to a mismatch between the actual behavior and documentation, and |
| 189 // lack of consistency on different reboot scenarios, the change count cannot |
| 190 // be checked after a reboot. |
| 191 // See radar://21833556 for more information. |
| 192 NSInteger change_count = [UIPasteboard generalPasteboard].changeCount; |
| 193 bool change_count_changed = change_count != last_pasteboard_change_count_; |
| 194 |
| 195 bool not_rebooted = uptime > GetClipboardContentAge(); |
| 196 if (not_rebooted) |
| 197 return change_count_changed; |
| 198 |
| 199 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; |
| 200 if (!pasteboard_string) { |
| 201 pasteboard_string = @""; |
| 202 } |
| 203 NSData* md5 = WeakMD5FromNSString(pasteboard_string); |
| 204 BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_]; |
| 205 |
| 206 return md5_changed; |
| 207 } |
| 208 |
| 147 void ClipboardRecentContentIOS::Init(base::TimeDelta uptime) { | 209 void ClipboardRecentContentIOS::Init(base::TimeDelta uptime) { |
| 148 lastPasteboardChangeCount_ = NSIntegerMax; | 210 last_pasteboard_change_count_ = NSIntegerMax; |
| 149 suppressedPasteboardEntryCount_ = NSIntegerMax; | 211 url_from_pasteboard_cache_ = URLFromPasteboard(); |
| 150 urlFromPasteboardCache_ = URLFromPasteboard(); | |
| 151 LoadFromUserDefaults(); | 212 LoadFromUserDefaults(); |
| 152 | 213 |
| 153 // On iOS 7 (unlike on iOS 8, despite what the documentation says), the change | 214 if (HasPasteboardChanged(uptime)) |
| 154 // count is reset when the device is rebooted. | 215 PasteboardChanged(); |
| 155 if (uptime < GetClipboardContentAge() && | 216 |
| 156 !base::ios::IsRunningOnIOS8OrLater()) { | 217 // Makes sure |last_pasteboard_change_count_| was properly initialized. |
| 157 if ([UIPasteboard generalPasteboard].changeCount == 0) { | 218 DCHECK_NE(last_pasteboard_change_count_, NSIntegerMax); |
| 158 // The user hasn't pasted anything in the clipboard since the device's | 219 notification_bridge_.reset( |
| 159 // reboot. |PasteboardChanged| isn't called because it would update | |
| 160 // |lastPasteboardChangeData_|, and record metrics. | |
| 161 lastPasteboardChangeCount_ = 0; | |
| 162 if (suppressedPasteboardEntryCount_ != NSIntegerMax) { | |
| 163 // If the last time Chrome was running the pasteboard was suppressed, | |
| 164 // and the user has not copied anything since the device launched, then | |
| 165 // supress this entry. | |
| 166 suppressedPasteboardEntryCount_ = 0; | |
| 167 } | |
| 168 SaveToUserDefaults(); | |
| 169 } else { | |
| 170 // The user pasted something in the clipboard since the device's reboot. | |
| 171 PasteboardChanged(); | |
| 172 } | |
| 173 } else { | |
| 174 NSInteger changeCount = [UIPasteboard generalPasteboard].changeCount; | |
| 175 if (changeCount != lastPasteboardChangeCount_) { | |
| 176 PasteboardChanged(); | |
| 177 } | |
| 178 } | |
| 179 // Makes sure |lastPasteboardChangeCount_| was properly initialized. | |
| 180 DCHECK_NE(lastPasteboardChangeCount_, NSIntegerMax); | |
| 181 notificationBridge_.reset( | |
| 182 [[PasteboardNotificationListenerBridge alloc] initWithDelegate:this]); | 220 [[PasteboardNotificationListenerBridge alloc] initWithDelegate:this]); |
| 183 } | 221 } |
| 184 | 222 |
| 185 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() { | 223 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() { |
| 186 [notificationBridge_ disconnect]; | 224 [notification_bridge_ disconnect]; |
| 187 } | 225 } |
| 188 | 226 |
| 189 GURL ClipboardRecentContentIOS::URLFromPasteboard() { | 227 GURL ClipboardRecentContentIOS::URLFromPasteboard() { |
| 190 const std::string clipboard = | 228 const std::string clipboard = |
| 191 base::SysNSStringToUTF8([[UIPasteboard generalPasteboard] string]); | 229 base::SysNSStringToUTF8([[UIPasteboard generalPasteboard] string]); |
| 192 GURL gurl = GURL(clipboard); | 230 GURL gurl = GURL(clipboard); |
| 193 if (gurl.is_valid()) { | 231 if (gurl.is_valid()) { |
| 194 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { | 232 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { |
| 195 if (gurl.SchemeIs(kAuthorizedSchemes[i])) { | 233 if (gurl.SchemeIs(kAuthorizedSchemes[i])) { |
| 196 return gurl; | 234 return gurl; |
| 197 } | 235 } |
| 198 } | 236 } |
| 199 if (!application_scheme_.empty() && | 237 if (!application_scheme_.empty() && |
| 200 gurl.SchemeIs(application_scheme_.c_str())) { | 238 gurl.SchemeIs(application_scheme_.c_str())) { |
| 201 return gurl; | 239 return gurl; |
| 202 } | 240 } |
| 203 } | 241 } |
| 204 return GURL::EmptyGURL(); | 242 return GURL::EmptyGURL(); |
| 205 } | 243 } |
| 206 | 244 |
| 245 void ClipboardRecentContentIOS::RecentURLDisplayed() { |
| 246 if ([last_pasteboard_change_date_ |
| 247 isEqualToDate:last_displayed_pasteboard_entry_.get()]) { |
| 248 return; |
| 249 } |
| 250 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged")); |
| 251 last_pasteboard_change_date_ = last_displayed_pasteboard_entry_; |
| 252 SaveToUserDefaults(); |
| 253 } |
| 254 |
| 207 void ClipboardRecentContentIOS::LoadFromUserDefaults() { | 255 void ClipboardRecentContentIOS::LoadFromUserDefaults() { |
| 208 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; | 256 last_pasteboard_change_count_ = |
| 257 [shared_user_defaults_ integerForKey:kPasteboardChangeCountKey]; |
| 258 last_pasteboard_change_date_.reset( |
| 259 [[shared_user_defaults_ objectForKey:kPasteboardChangeDateKey] retain]); |
| 260 last_pasteboard_entry_md5_.reset( |
| 261 [[shared_user_defaults_ objectForKey:kPasteboardEntryMD5Key] retain]); |
| 262 last_displayed_pasteboard_entry_.reset([[shared_user_defaults_ |
| 263 objectForKey:kLastDisplayedPasteboardEntryKey] retain]); |
| 209 | 264 |
| 210 lastPasteboardChangeCount_ = | 265 DCHECK(!last_pasteboard_change_date_ || |
| 211 [defaults integerForKey:kPasteboardChangeCountKey]; | 266 [last_pasteboard_change_date_ isKindOfClass:[NSDate class]]); |
| 212 lastPasteboardChangeDate_.reset( | |
| 213 [[defaults objectForKey:kPasteboardChangeDateKey] retain]); | |
| 214 | |
| 215 if ([[[defaults dictionaryRepresentation] allKeys] | |
| 216 containsObject:kSuppressedPasteboardEntryCountKey]) { | |
| 217 suppressedPasteboardEntryCount_ = | |
| 218 [defaults integerForKey:kSuppressedPasteboardEntryCountKey]; | |
| 219 } else { | |
| 220 suppressedPasteboardEntryCount_ = NSIntegerMax; | |
| 221 } | |
| 222 | |
| 223 DCHECK(!lastPasteboardChangeDate_ || | |
| 224 [lastPasteboardChangeDate_ isKindOfClass:[NSDate class]]); | |
| 225 } | 267 } |
| 226 | 268 |
| 227 void ClipboardRecentContentIOS::SaveToUserDefaults() { | 269 void ClipboardRecentContentIOS::SaveToUserDefaults() { |
| 228 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; | 270 [shared_user_defaults_ setInteger:last_pasteboard_change_count_ |
| 229 [defaults setInteger:lastPasteboardChangeCount_ | 271 forKey:kPasteboardChangeCountKey]; |
| 230 forKey:kPasteboardChangeCountKey]; | 272 [shared_user_defaults_ setObject:last_pasteboard_change_date_ |
| 231 [defaults setObject:lastPasteboardChangeDate_ | 273 forKey:kPasteboardChangeDateKey]; |
| 232 forKey:kPasteboardChangeDateKey]; | 274 [shared_user_defaults_ setObject:last_pasteboard_entry_md5_ |
| 233 [defaults setInteger:suppressedPasteboardEntryCount_ | 275 forKey:kPasteboardEntryMD5Key]; |
| 234 forKey:kSuppressedPasteboardEntryCountKey]; | 276 [shared_user_defaults_ setObject:last_displayed_pasteboard_entry_ |
| 277 forKey:kLastDisplayedPasteboardEntryKey]; |
| 235 } | 278 } |
| OLD | NEW |