Roll src/third_party/WebKit 605a979:06cb9e9 (svn 202556:202558)
[chromium-blink-merge.git] / components / open_from_clipboard / clipboard_recent_content_ios.mm
blob123c4aa5afd4287356e2bec0c02aec567884d80c
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"
16 #include "url/gurl.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
23 // must not be null.
24 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate
25     NS_DESIGNATED_INITIALIZER;
27 - (instancetype)init NS_UNAVAILABLE;
29 @end
31 @implementation PasteboardNotificationListenerBridge {
32   ClipboardRecentContentIOS* _delegate;
35 - (instancetype)init {
36   NOTREACHED();
37   return nil;
40 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate {
41   DCHECK(delegate);
42   self = [super init];
43   if (self) {
44     _delegate = delegate;
45     [[NSNotificationCenter defaultCenter]
46         addObserver:self
47            selector:@selector(pasteboardChangedNotification:)
48                name:UIPasteboardChangedNotification
49              object:[UIPasteboard generalPasteboard]];
50     [[NSNotificationCenter defaultCenter]
51         addObserver:self
52            selector:@selector(didBecomeActive:)
53                name:UIApplicationDidBecomeActiveNotification
54              object:nil];
55   }
56   return self;
59 - (void)dealloc {
60   [[NSNotificationCenter defaultCenter] removeObserver:self];
61   [super dealloc];
64 - (void)pasteboardChangedNotification:(NSNotification*)notification {
65   if (_delegate) {
66     _delegate->PasteboardChanged();
67   }
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   }
81 - (void)disconnect {
82   _delegate = nullptr;
85 @end
87 namespace {
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[] = {
104     url::kHttpScheme,
105     url::kHttpsScheme,
106     url::kDataScheme,
107     url::kAboutScheme,
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;
122 }  // namespace
124 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const {
125   DCHECK(url);
126   if (GetClipboardContentAge() > kMaximumAgeOfClipboard) {
127     return false;
128   }
130   if (url_from_pasteboard_cache_.is_valid()) {
131     *url = url_from_pasteboard_cache_;
132     return true;
133   }
134   return false;
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)
153     return;
154   url_from_pasteboard_cache_ = URLFromPasteboard();
155   if (!url_from_pasteboard_cache_.is_empty()) {
156     base::RecordAction(
157         base::UserMetricsAction("MobileOmniboxClipboardChanged"));
158   }
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]) {
179   Init(uptime);
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)
188     return NO;
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();
202   if (not_rebooted)
203     return change_count_changed;
205   NSData* md5 = WeakMD5FromNSString(pasteboard_string);
206   BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_];
208   return md5_changed;
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))
217     PasteboardChanged();
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();
233   }
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])) {
239         return gurl;
240       }
241     }
242     if (!application_scheme_.empty() &&
243         gurl.SchemeIs(application_scheme_.c_str())) {
244       return gurl;
245     }
246   }
247   return GURL::EmptyGURL();
250 void ClipboardRecentContentIOS::RecentURLDisplayed() {
251   if ([last_pasteboard_change_date_
252           isEqualToDate:last_displayed_pasteboard_entry_.get()]) {
253     return;
254   }
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];