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 <CommonCrypto/CommonDigest.h> |
8 #include <stddef.h> | 8 #include <stddef.h> |
9 #include <stdint.h> | 9 #include <stdint.h> |
10 #import <UIKit/UIKit.h> | 10 #import <UIKit/UIKit.h> |
11 | 11 |
12 #import "base/ios/ios_util.h" | 12 #import "base/ios/ios_util.h" |
13 #include "base/logging.h" | 13 #include "base/logging.h" |
14 #include "base/macros.h" | 14 #include "base/macros.h" |
15 #include "base/metrics/user_metrics.h" | 15 #include "base/metrics/user_metrics.h" |
16 #include "base/strings/sys_string_conversions.h" | 16 #include "base/strings/sys_string_conversions.h" |
17 #include "base/sys_info.h" | 17 #include "base/sys_info.h" |
18 #include "url/gurl.h" | 18 #include "url/gurl.h" |
19 #include "url/url_constants.h" | 19 #include "url/url_constants.h" |
20 | 20 |
21 // Bridge that forwards pasteboard change notifications to its delegate. | 21 // Bridge that forwards UIApplicationDidBecomeActiveNotification notifications |
22 @interface PasteboardNotificationListenerBridge : NSObject | 22 // to its delegate. |
| 23 @interface ApplicationDidBecomeActiveNotificationListenerBridge : NSObject |
23 | 24 |
24 // Initialize the PasteboardNotificationListenerBridge with |delegate| which | 25 // Initialize the ApplicationDidBecomeActiveNotificationListenerBridge with |
25 // must not be null. | 26 // |delegate| which must not be null. |
26 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate | 27 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate |
27 NS_DESIGNATED_INITIALIZER; | 28 NS_DESIGNATED_INITIALIZER; |
28 | 29 |
29 - (instancetype)init NS_UNAVAILABLE; | 30 - (instancetype)init NS_UNAVAILABLE; |
30 | 31 |
31 @end | 32 @end |
32 | 33 |
33 @implementation PasteboardNotificationListenerBridge { | 34 @implementation ApplicationDidBecomeActiveNotificationListenerBridge { |
34 ClipboardRecentContentIOS* _delegate; | 35 ClipboardRecentContentIOS* _delegate; |
35 } | 36 } |
36 | 37 |
37 - (instancetype)init { | 38 - (instancetype)init { |
38 NOTREACHED(); | 39 NOTREACHED(); |
39 return nil; | 40 return nil; |
40 } | 41 } |
41 | 42 |
42 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate { | 43 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate { |
43 DCHECK(delegate); | 44 DCHECK(delegate); |
44 self = [super init]; | 45 self = [super init]; |
45 if (self) { | 46 if (self) { |
46 _delegate = delegate; | 47 _delegate = delegate; |
47 [[NSNotificationCenter defaultCenter] | 48 [[NSNotificationCenter defaultCenter] |
48 addObserver:self | 49 addObserver:self |
49 selector:@selector(pasteboardChangedNotification:) | |
50 name:UIPasteboardChangedNotification | |
51 object:[UIPasteboard generalPasteboard]]; | |
52 [[NSNotificationCenter defaultCenter] | |
53 addObserver:self | |
54 selector:@selector(didBecomeActive:) | 50 selector:@selector(didBecomeActive:) |
55 name:UIApplicationDidBecomeActiveNotification | 51 name:UIApplicationDidBecomeActiveNotification |
56 object:nil]; | 52 object:nil]; |
57 } | 53 } |
58 return self; | 54 return self; |
59 } | 55 } |
60 | 56 |
61 - (void)dealloc { | 57 - (void)dealloc { |
62 [[NSNotificationCenter defaultCenter] removeObserver:self]; | 58 [[NSNotificationCenter defaultCenter] removeObserver:self]; |
63 [super dealloc]; | 59 [super dealloc]; |
64 } | 60 } |
65 | 61 |
66 - (void)pasteboardChangedNotification:(NSNotification*)notification { | |
67 if (_delegate) { | |
68 _delegate->PasteboardChanged(); | |
69 } | |
70 } | |
71 | |
72 - (void)didBecomeActive:(NSNotification*)notification { | 62 - (void)didBecomeActive:(NSNotification*)notification { |
73 if (_delegate) { | 63 if (_delegate) { |
74 _delegate->LoadFromUserDefaults(); | 64 _delegate->LoadFromUserDefaults(); |
75 if (_delegate->HasPasteboardChanged(base::SysInfo::Uptime())) { | 65 _delegate->UpdateIfNeeded(); |
76 _delegate->PasteboardChanged(); | |
77 } | |
78 } | 66 } |
79 } | 67 } |
80 | 68 |
81 - (void)disconnect { | 69 - (void)disconnect { |
82 _delegate = nullptr; | 70 _delegate = nullptr; |
83 } | 71 } |
84 | 72 |
85 @end | 73 @end |
86 | 74 |
87 namespace { | 75 namespace { |
88 // Key used to store the pasteboard's current change count. If when resuming | 76 // 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 | 77 // chrome the pasteboard's change count is different from the stored one, then |
90 // it means that the pasteboard's content has changed. | 78 // it means that the pasteboard's content has changed. |
91 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount"; | 79 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount"; |
92 // Key used to store the last date at which it was detected that the pasteboard | 80 // 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. | 81 // changed. It is used to evaluate the age of the pasteboard's content. |
94 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate"; | 82 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate"; |
95 // Key used to store the hash of the content of the pasteboard. Whenever the | 83 // 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. | 84 // hash changed, the pasteboard content is considered to have changed. |
97 NSString* kPasteboardEntryMD5Key = @"PasteboardEntryMD5"; | 85 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); | 86 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3); |
102 // Schemes accepted by the ClipboardRecentContentIOS. | 87 // Schemes accepted by the ClipboardRecentContentIOS. |
103 const char* kAuthorizedSchemes[] = { | 88 const char* kAuthorizedSchemes[] = { |
104 url::kHttpScheme, | 89 url::kHttpScheme, |
105 url::kHttpsScheme, | 90 url::kHttpsScheme, |
106 url::kDataScheme, | 91 url::kDataScheme, |
107 url::kAboutScheme, | 92 url::kAboutScheme, |
108 }; | 93 }; |
109 | 94 |
110 // Compute a hash consisting of the first 4 bytes of the MD5 hash of |string|. | 95 // 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 | 96 // 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 | 97 // is a privacy requirement to introduce collision and allow deniability of |
113 // having copied a given string. | 98 // having copied a given string. |
114 NSData* WeakMD5FromNSString(NSString* string) { | 99 NSData* WeakMD5FromNSString(NSString* string) { |
115 unsigned char hash[CC_MD5_DIGEST_LENGTH]; | 100 unsigned char hash[CC_MD5_DIGEST_LENGTH]; |
116 const char* c_string = [string UTF8String]; | 101 const char* c_string = [string UTF8String]; |
117 CC_MD5(c_string, strlen(c_string), hash); | 102 CC_MD5(c_string, strlen(c_string), hash); |
118 NSData* data = [NSData dataWithBytes:hash length:4]; | 103 NSData* data = [NSData dataWithBytes:hash length:4]; |
119 return data; | 104 return data; |
120 } | 105 } |
121 | 106 |
122 } // namespace | 107 } // namespace |
123 | 108 |
124 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const { | 109 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) { |
125 DCHECK(url); | 110 DCHECK(url); |
| 111 UpdateIfNeeded(); |
126 if (GetClipboardContentAge() > kMaximumAgeOfClipboard) { | 112 if (GetClipboardContentAge() > kMaximumAgeOfClipboard) { |
127 return false; | 113 return false; |
128 } | 114 } |
129 | 115 |
130 if (url_from_pasteboard_cache_.is_valid()) { | 116 GURL url_from_pasteboard = URLFromPasteboard(); |
131 *url = url_from_pasteboard_cache_; | 117 if (url_from_pasteboard.is_valid()) { |
| 118 *url = url_from_pasteboard; |
132 return true; | 119 return true; |
133 } | 120 } |
134 return false; | 121 return false; |
135 } | 122 } |
136 | 123 |
137 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { | 124 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { |
138 return base::TimeDelta::FromSeconds(static_cast<int64_t>( | 125 return base::TimeDelta::FromSeconds(static_cast<int64_t>( |
139 -[last_pasteboard_change_date_ timeIntervalSinceNow])); | 126 -[last_pasteboard_change_date_ timeIntervalSinceNow])); |
140 } | 127 } |
141 | 128 |
142 void ClipboardRecentContentIOS::SuppressClipboardContent() { | 129 void ClipboardRecentContentIOS::SuppressClipboardContent() { |
143 // User cleared the user data. The pasteboard entry must be removed from the | 130 // 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. | 131 // omnibox list. Force entry expiration by setting copy date to 1970. |
145 last_pasteboard_change_date_.reset( | 132 last_pasteboard_change_date_.reset( |
146 [[NSDate alloc] initWithTimeIntervalSince1970:0]); | 133 [[NSDate alloc] initWithTimeIntervalSince1970:0]); |
147 SaveToUserDefaults(); | 134 SaveToUserDefaults(); |
148 } | 135 } |
149 | 136 |
150 void ClipboardRecentContentIOS::PasteboardChanged() { | 137 void ClipboardRecentContentIOS::UpdateIfNeeded() { |
151 url_from_pasteboard_cache_ = URLFromPasteboard(); | 138 if (!HasPasteboardChanged()) |
152 if (!url_from_pasteboard_cache_.is_empty()) { | 139 return; |
153 base::RecordAction( | 140 |
154 base::UserMetricsAction("MobileOmniboxClipboardChanged")); | 141 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged")); |
155 } | 142 |
| 143 GURL url_from_pasteboard = URLFromPasteboard(); |
156 last_pasteboard_change_date_.reset([[NSDate date] retain]); | 144 last_pasteboard_change_date_.reset([[NSDate date] retain]); |
157 last_pasteboard_change_count_ = [UIPasteboard generalPasteboard].changeCount; | 145 last_pasteboard_change_count_ = [UIPasteboard generalPasteboard].changeCount; |
158 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; | 146 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; |
159 if (!pasteboard_string) { | 147 if (!pasteboard_string) { |
160 pasteboard_string = @""; | 148 pasteboard_string = @""; |
161 } | 149 } |
162 NSData* MD5 = WeakMD5FromNSString(pasteboard_string); | 150 NSData* MD5 = WeakMD5FromNSString(pasteboard_string); |
163 last_pasteboard_entry_md5_.reset([MD5 retain]); | 151 last_pasteboard_entry_md5_.reset([MD5 retain]); |
164 SaveToUserDefaults(); | 152 SaveToUserDefaults(); |
165 } | 153 } |
166 | 154 |
167 ClipboardRecentContentIOS::ClipboardRecentContentIOS( | 155 ClipboardRecentContentIOS::ClipboardRecentContentIOS( |
168 const std::string& application_scheme, | 156 const std::string& application_scheme, |
169 NSUserDefaults* group_user_defaults) | 157 NSUserDefaults* group_user_defaults) |
170 : application_scheme_(application_scheme), | 158 : application_scheme_(application_scheme), |
171 shared_user_defaults_([group_user_defaults retain]) { | 159 shared_user_defaults_([group_user_defaults retain]) { |
172 Init(base::SysInfo::Uptime()); | 160 last_pasteboard_change_count_ = NSIntegerMax; |
| 161 LoadFromUserDefaults(); |
| 162 |
| 163 UpdateIfNeeded(); |
| 164 |
| 165 // Makes sure |last_pasteboard_change_count_| was properly initialized. |
| 166 DCHECK_NE(last_pasteboard_change_count_, NSIntegerMax); |
| 167 notification_bridge_.reset( |
| 168 [[ApplicationDidBecomeActiveNotificationListenerBridge alloc] |
| 169 initWithDelegate:this]); |
173 } | 170 } |
174 | 171 |
175 ClipboardRecentContentIOS::ClipboardRecentContentIOS( | 172 bool ClipboardRecentContentIOS::HasPasteboardChanged() const { |
176 const std::string& application_scheme, | |
177 base::TimeDelta uptime) | |
178 : application_scheme_(application_scheme), | |
179 shared_user_defaults_([[NSUserDefaults standardUserDefaults] retain]) { | |
180 Init(uptime); | |
181 } | |
182 | |
183 bool ClipboardRecentContentIOS::HasPasteboardChanged(base::TimeDelta uptime) { | |
184 // If |MD5Changed|, we know for sure there has been at least one pasteboard | 173 // If |MD5Changed|, we know for sure there has been at least one pasteboard |
185 // copy since last time it was checked. | 174 // copy since last time it was checked. |
186 // If the pasteboard content is still the same but the device was not | 175 // 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. | 176 // 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 | 177 // Note: due to a mismatch between the actual behavior and documentation, and |
189 // lack of consistency on different reboot scenarios, the change count cannot | 178 // lack of consistency on different reboot scenarios, the change count cannot |
190 // be checked after a reboot. | 179 // be checked after a reboot. |
191 // See radar://21833556 for more information. | 180 // See radar://21833556 for more information. |
192 NSInteger change_count = [UIPasteboard generalPasteboard].changeCount; | 181 NSInteger change_count = [UIPasteboard generalPasteboard].changeCount; |
193 bool change_count_changed = change_count != last_pasteboard_change_count_; | 182 bool change_count_changed = change_count != last_pasteboard_change_count_; |
194 | 183 |
195 bool not_rebooted = uptime > GetClipboardContentAge(); | 184 bool not_rebooted = Uptime() > GetClipboardContentAge(); |
196 if (not_rebooted) | 185 if (not_rebooted) |
197 return change_count_changed; | 186 return change_count_changed; |
198 | 187 |
199 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; | 188 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; |
200 if (!pasteboard_string) { | 189 if (!pasteboard_string) { |
201 pasteboard_string = @""; | 190 pasteboard_string = @""; |
202 } | 191 } |
203 NSData* md5 = WeakMD5FromNSString(pasteboard_string); | 192 NSData* md5 = WeakMD5FromNSString(pasteboard_string); |
204 BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_]; | 193 BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_]; |
205 | 194 |
206 return md5_changed; | 195 return md5_changed; |
207 } | 196 } |
208 | 197 |
209 bool ClipboardRecentContentIOS::GetCurrentURLFromClipboard(GURL* url) { | |
210 if (HasPasteboardChanged(base::SysInfo::Uptime())) { | |
211 PasteboardChanged(); | |
212 } | |
213 return GetRecentURLFromClipboard(url); | |
214 } | |
215 | |
216 void ClipboardRecentContentIOS::Init(base::TimeDelta uptime) { | |
217 last_pasteboard_change_count_ = NSIntegerMax; | |
218 url_from_pasteboard_cache_ = URLFromPasteboard(); | |
219 LoadFromUserDefaults(); | |
220 | |
221 if (HasPasteboardChanged(uptime)) | |
222 PasteboardChanged(); | |
223 | |
224 // Makes sure |last_pasteboard_change_count_| was properly initialized. | |
225 DCHECK_NE(last_pasteboard_change_count_, NSIntegerMax); | |
226 notification_bridge_.reset( | |
227 [[PasteboardNotificationListenerBridge alloc] initWithDelegate:this]); | |
228 } | |
229 | |
230 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() { | 198 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() { |
231 [notification_bridge_ disconnect]; | 199 [notification_bridge_ disconnect]; |
232 } | 200 } |
233 | 201 |
234 GURL ClipboardRecentContentIOS::URLFromPasteboard() { | 202 GURL ClipboardRecentContentIOS::URLFromPasteboard() { |
235 NSString* clipboard_string = [[UIPasteboard generalPasteboard] string]; | 203 NSString* clipboard_string = [[UIPasteboard generalPasteboard] string]; |
236 if (!clipboard_string) { | 204 if (!clipboard_string) { |
237 return GURL::EmptyGURL(); | 205 return GURL::EmptyGURL(); |
238 } | 206 } |
239 const std::string clipboard = base::SysNSStringToUTF8(clipboard_string); | 207 const std::string clipboard = base::SysNSStringToUTF8(clipboard_string); |
240 GURL gurl = GURL(clipboard); | 208 GURL gurl = GURL(clipboard); |
241 if (gurl.is_valid()) { | 209 if (gurl.is_valid()) { |
242 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { | 210 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { |
243 if (gurl.SchemeIs(kAuthorizedSchemes[i])) { | 211 if (gurl.SchemeIs(kAuthorizedSchemes[i])) { |
244 return gurl; | 212 return gurl; |
245 } | 213 } |
246 } | 214 } |
247 if (!application_scheme_.empty() && | 215 if (!application_scheme_.empty() && |
248 gurl.SchemeIs(application_scheme_.c_str())) { | 216 gurl.SchemeIs(application_scheme_.c_str())) { |
249 return gurl; | 217 return gurl; |
250 } | 218 } |
251 } | 219 } |
252 return GURL::EmptyGURL(); | 220 return GURL::EmptyGURL(); |
253 } | 221 } |
254 | 222 |
255 void ClipboardRecentContentIOS::RecentURLDisplayed() { | |
256 if ([last_pasteboard_change_date_ | |
257 isEqualToDate:last_displayed_pasteboard_entry_.get()]) { | |
258 return; | |
259 } | |
260 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged")); | |
261 last_pasteboard_change_date_ = last_displayed_pasteboard_entry_; | |
262 SaveToUserDefaults(); | |
263 } | |
264 | |
265 void ClipboardRecentContentIOS::LoadFromUserDefaults() { | 223 void ClipboardRecentContentIOS::LoadFromUserDefaults() { |
266 last_pasteboard_change_count_ = | 224 last_pasteboard_change_count_ = |
267 [shared_user_defaults_ integerForKey:kPasteboardChangeCountKey]; | 225 [shared_user_defaults_ integerForKey:kPasteboardChangeCountKey]; |
268 last_pasteboard_change_date_.reset( | 226 last_pasteboard_change_date_.reset( |
269 [[shared_user_defaults_ objectForKey:kPasteboardChangeDateKey] retain]); | 227 [[shared_user_defaults_ objectForKey:kPasteboardChangeDateKey] retain]); |
270 last_pasteboard_entry_md5_.reset( | 228 last_pasteboard_entry_md5_.reset( |
271 [[shared_user_defaults_ objectForKey:kPasteboardEntryMD5Key] retain]); | 229 [[shared_user_defaults_ objectForKey:kPasteboardEntryMD5Key] retain]); |
272 last_displayed_pasteboard_entry_.reset([[shared_user_defaults_ | |
273 objectForKey:kLastDisplayedPasteboardEntryKey] retain]); | |
274 | 230 |
275 DCHECK(!last_pasteboard_change_date_ || | 231 DCHECK(!last_pasteboard_change_date_ || |
276 [last_pasteboard_change_date_ isKindOfClass:[NSDate class]]); | 232 [last_pasteboard_change_date_ isKindOfClass:[NSDate class]]); |
277 } | 233 } |
278 | 234 |
279 void ClipboardRecentContentIOS::SaveToUserDefaults() { | 235 void ClipboardRecentContentIOS::SaveToUserDefaults() { |
280 [shared_user_defaults_ setInteger:last_pasteboard_change_count_ | 236 [shared_user_defaults_ setInteger:last_pasteboard_change_count_ |
281 forKey:kPasteboardChangeCountKey]; | 237 forKey:kPasteboardChangeCountKey]; |
282 [shared_user_defaults_ setObject:last_pasteboard_change_date_ | 238 [shared_user_defaults_ setObject:last_pasteboard_change_date_ |
283 forKey:kPasteboardChangeDateKey]; | 239 forKey:kPasteboardChangeDateKey]; |
284 [shared_user_defaults_ setObject:last_pasteboard_entry_md5_ | 240 [shared_user_defaults_ setObject:last_pasteboard_entry_md5_ |
285 forKey:kPasteboardEntryMD5Key]; | 241 forKey:kPasteboardEntryMD5Key]; |
286 [shared_user_defaults_ setObject:last_displayed_pasteboard_entry_ | |
287 forKey:kLastDisplayedPasteboardEntryKey]; | |
288 } | 242 } |
| 243 |
| 244 base::TimeDelta ClipboardRecentContentIOS::Uptime() const { |
| 245 return base::SysInfo::Uptime(); |
| 246 } |
OLD | NEW |