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 #import "components/open_from_clipboard/clipboard_recent_content_ios_impl.h" | |
19 #import "net/base/mac/url_conversions.h" | |
18 #include "url/gurl.h" | 20 #include "url/gurl.h" |
19 #include "url/url_constants.h" | 21 #include "url/url_constants.h" |
20 | 22 |
21 // Bridge that forwards UIApplicationDidBecomeActiveNotification notifications | 23 namespace { |
22 // to its delegate. | 24 // Schemes accepted by the ClipboardRecentContentIOS. |
23 @interface ApplicationDidBecomeActiveNotificationListenerBridge : NSObject | 25 const char* kAuthorizedSchemes[] = { |
26 url::kHttpScheme, url::kHttpsScheme, url::kDataScheme, url::kAboutScheme, | |
27 }; | |
28 } | |
24 | 29 |
25 // Initialize the ApplicationDidBecomeActiveNotificationListenerBridge with | 30 @interface ClipboardRecentContentMetricsDelegateImpl |
26 // |delegate| which must not be null. | 31 : NSObject<ClipboardRecentContentMetricsDelegate> |
27 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate | |
28 NS_DESIGNATED_INITIALIZER; | |
29 | |
30 - (instancetype)init NS_UNAVAILABLE; | |
31 | |
32 @end | 32 @end |
33 | 33 |
34 @implementation ApplicationDidBecomeActiveNotificationListenerBridge { | 34 @implementation ClipboardRecentContentMetricsDelegateImpl |
35 ClipboardRecentContentIOS* _delegate; | |
36 } | |
37 | 35 |
38 - (instancetype)init { | 36 - (void)onClipboardChanged { |
39 NOTREACHED(); | 37 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged")); |
sdefresne
2017/04/03 09:27:16
(outside of the CL) I think action that is recorde
| |
40 return nil; | |
41 } | |
42 | |
43 - (instancetype)initWithDelegate:(ClipboardRecentContentIOS*)delegate { | |
44 DCHECK(delegate); | |
45 self = [super init]; | |
46 if (self) { | |
47 _delegate = delegate; | |
48 [[NSNotificationCenter defaultCenter] | |
49 addObserver:self | |
50 selector:@selector(didBecomeActive:) | |
51 name:UIApplicationDidBecomeActiveNotification | |
52 object:nil]; | |
53 } | |
54 return self; | |
55 } | |
56 | |
57 - (void)dealloc { | |
58 [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
59 [super dealloc]; | |
60 } | |
61 | |
62 - (void)didBecomeActive:(NSNotification*)notification { | |
63 if (_delegate) { | |
64 _delegate->LoadFromUserDefaults(); | |
65 _delegate->UpdateIfNeeded(); | |
66 } | |
67 } | |
68 | |
69 - (void)disconnect { | |
70 _delegate = nullptr; | |
71 } | 38 } |
72 | 39 |
73 @end | 40 @end |
74 | 41 |
75 namespace { | 42 ClipboardRecentContentIOS::ClipboardRecentContentIOS( |
76 // Key used to store the pasteboard's current change count. If when resuming | 43 const std::string& application_scheme, |
77 // chrome the pasteboard's change count is different from the stored one, then | 44 NSUserDefaults* group_user_defaults) { |
78 // it means that the pasteboard's content has changed. | 45 ClipboardRecentContentMetricsDelegateImpl* metricsDelegate = |
sdefresne
2017/04/03 09:27:16
s/metricsDelegate/metrics_delegate/.
lody
2017/04/04 13:42:18
Done.
| |
79 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount"; | 46 [[ClipboardRecentContentMetricsDelegateImpl alloc] init]; |
80 // 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. | |
82 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate"; | |
83 // Key used to store the hash of the content of the pasteboard. Whenever the | |
84 // hash changed, the pasteboard content is considered to have changed. | |
85 NSString* kPasteboardEntryMD5Key = @"PasteboardEntryMD5"; | |
86 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3); | |
87 // Schemes accepted by the ClipboardRecentContentIOS. | |
88 const char* kAuthorizedSchemes[] = { | |
89 url::kHttpScheme, | |
90 url::kHttpsScheme, | |
91 url::kDataScheme, | |
92 url::kAboutScheme, | |
93 }; | |
94 | 47 |
95 // Compute a hash consisting of the first 4 bytes of the MD5 hash of |string|. | 48 implementation_.reset([[ClipboardRecentContentIOSImpl alloc] |
96 // This value is used to detect pasteboard content change. Keeping only 4 bytes | 49 initWithDelegate:metricsDelegate |
97 // is a privacy requirement to introduce collision and allow deniability of | 50 authorizedSchemes:GetAuthorizedSchemeList(application_scheme) |
98 // having copied a given string. | 51 userDefaults:group_user_defaults]); |
99 NSData* WeakMD5FromNSString(NSString* string) { | |
100 unsigned char hash[CC_MD5_DIGEST_LENGTH]; | |
101 const std::string clipboard = base::SysNSStringToUTF8(string); | |
102 const char* c_string = clipboard.c_str(); | |
103 CC_MD5(c_string, strlen(c_string), hash); | |
104 NSData* data = [NSData dataWithBytes:hash length:4]; | |
105 return data; | |
106 } | 52 } |
107 | 53 |
108 } // namespace | |
109 | |
110 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) { | 54 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) { |
111 DCHECK(url); | 55 DCHECK(url); |
112 UpdateIfNeeded(); | 56 NSURL* nsurl = [implementation_ getRecentURLFromClipboard]; |
sdefresne
2017/04/03 09:27:16
url_from_pasteboard
lody
2017/04/04 13:42:18
Done.
| |
113 if (GetClipboardContentAge() > kMaximumAgeOfClipboard) { | 57 if (nsurl != nil) { |
sdefresne
2017/04/03 09:27:16
No need to test nsurl for nil, net::GURLWithNSURL(
lody
2017/04/04 13:42:18
Done.
| |
114 return false; | 58 GURL tempUrl = net::GURLWithNSURL(nsurl); |
sdefresne
2017/04/03 09:27:16
NSURL* url_from_pasteboard = [implementation_ getR
lody
2017/04/04 13:42:18
Done.
| |
115 } | 59 if (tempUrl.is_valid()) { |
116 | 60 *url = std::move(tempUrl); |
117 GURL url_from_pasteboard = URLFromPasteboard(); | 61 return true; |
118 if (url_from_pasteboard.is_valid()) { | 62 } |
119 *url = url_from_pasteboard; | |
120 return true; | |
121 } | 63 } |
122 return false; | 64 return false; |
123 } | 65 } |
124 | 66 |
67 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() {} | |
68 | |
125 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { | 69 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const { |
126 return base::TimeDelta::FromSeconds(static_cast<int64_t>( | 70 return base::TimeDelta::FromSeconds( |
127 -[last_pasteboard_change_date_ timeIntervalSinceNow])); | 71 static_cast<int64_t>([implementation_ getClipboardContentAge])); |
128 } | 72 } |
129 | 73 |
130 void ClipboardRecentContentIOS::SuppressClipboardContent() { | 74 void ClipboardRecentContentIOS::SuppressClipboardContent() { |
131 // User cleared the user data. The pasteboard entry must be removed from the | 75 [implementation_ suppressClipboardContent]; |
132 // omnibox list. Force entry expiration by setting copy date to 1970. | |
133 last_pasteboard_change_date_.reset( | |
134 [[NSDate alloc] initWithTimeIntervalSince1970:0]); | |
135 SaveToUserDefaults(); | |
136 } | |
137 | |
138 void ClipboardRecentContentIOS::UpdateIfNeeded() { | |
139 if (!HasPasteboardChanged()) | |
140 return; | |
141 | |
142 base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged")); | |
143 | |
144 GURL url_from_pasteboard = URLFromPasteboard(); | |
145 last_pasteboard_change_date_.reset([[NSDate date] retain]); | |
146 last_pasteboard_change_count_ = [UIPasteboard generalPasteboard].changeCount; | |
147 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; | |
148 if (!pasteboard_string) { | |
149 pasteboard_string = @""; | |
150 } | |
151 NSData* MD5 = WeakMD5FromNSString(pasteboard_string); | |
152 last_pasteboard_entry_md5_.reset([MD5 retain]); | |
153 SaveToUserDefaults(); | |
154 } | |
155 | |
156 ClipboardRecentContentIOS::ClipboardRecentContentIOS( | |
157 const std::string& application_scheme, | |
158 NSUserDefaults* group_user_defaults) | |
159 : application_scheme_(application_scheme), | |
160 shared_user_defaults_([group_user_defaults retain]) { | |
161 last_pasteboard_change_count_ = NSIntegerMax; | |
162 LoadFromUserDefaults(); | |
163 | |
164 UpdateIfNeeded(); | |
165 | |
166 // Makes sure |last_pasteboard_change_count_| was properly initialized. | |
167 DCHECK_NE(last_pasteboard_change_count_, NSIntegerMax); | |
168 notification_bridge_.reset( | |
169 [[ApplicationDidBecomeActiveNotificationListenerBridge alloc] | |
170 initWithDelegate:this]); | |
171 } | |
172 | |
173 bool ClipboardRecentContentIOS::HasPasteboardChanged() const { | |
174 // If |MD5Changed|, we know for sure there has been at least one pasteboard | |
175 // copy since last time it was checked. | |
176 // If the pasteboard content is still the same but the device was not | |
177 // rebooted, the change count can be checked to see if it changed. | |
178 // Note: due to a mismatch between the actual behavior and documentation, and | |
179 // lack of consistency on different reboot scenarios, the change count cannot | |
180 // be checked after a reboot. | |
181 // See radar://21833556 for more information. | |
182 NSInteger change_count = [UIPasteboard generalPasteboard].changeCount; | |
183 bool change_count_changed = change_count != last_pasteboard_change_count_; | |
184 | |
185 bool not_rebooted = Uptime() > GetClipboardContentAge(); | |
186 if (not_rebooted) | |
187 return change_count_changed; | |
188 | |
189 NSString* pasteboard_string = [[UIPasteboard generalPasteboard] string]; | |
190 if (!pasteboard_string) { | |
191 pasteboard_string = @""; | |
192 } | |
193 NSData* md5 = WeakMD5FromNSString(pasteboard_string); | |
194 BOOL md5_changed = ![md5 isEqualToData:last_pasteboard_entry_md5_]; | |
195 | |
196 return md5_changed; | |
197 } | |
198 | |
199 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() { | |
200 [notification_bridge_ disconnect]; | |
201 } | |
202 | |
203 GURL ClipboardRecentContentIOS::URLFromPasteboard() { | |
204 NSString* clipboard_string = [[UIPasteboard generalPasteboard] string]; | |
205 if (!clipboard_string) { | |
206 return GURL::EmptyGURL(); | |
207 } | |
208 const std::string clipboard = base::SysNSStringToUTF8(clipboard_string); | |
209 GURL gurl = GURL(clipboard); | |
210 if (gurl.is_valid()) { | |
211 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { | |
212 if (gurl.SchemeIs(kAuthorizedSchemes[i])) { | |
213 return gurl; | |
214 } | |
215 } | |
216 if (!application_scheme_.empty() && | |
217 gurl.SchemeIs(application_scheme_.c_str())) { | |
218 return gurl; | |
219 } | |
220 } | |
221 return GURL::EmptyGURL(); | |
222 } | |
223 | |
224 void ClipboardRecentContentIOS::LoadFromUserDefaults() { | |
225 last_pasteboard_change_count_ = | |
226 [shared_user_defaults_ integerForKey:kPasteboardChangeCountKey]; | |
227 last_pasteboard_change_date_.reset( | |
228 [[shared_user_defaults_ objectForKey:kPasteboardChangeDateKey] retain]); | |
229 last_pasteboard_entry_md5_.reset( | |
230 [[shared_user_defaults_ objectForKey:kPasteboardEntryMD5Key] retain]); | |
231 | |
232 DCHECK(!last_pasteboard_change_date_ || | |
233 [last_pasteboard_change_date_ isKindOfClass:[NSDate class]]); | |
234 } | 76 } |
235 | 77 |
236 void ClipboardRecentContentIOS::SaveToUserDefaults() { | 78 void ClipboardRecentContentIOS::SaveToUserDefaults() { |
237 [shared_user_defaults_ setInteger:last_pasteboard_change_count_ | 79 [implementation_ saveToUserDefaults]; |
238 forKey:kPasteboardChangeCountKey]; | |
239 [shared_user_defaults_ setObject:last_pasteboard_change_date_ | |
240 forKey:kPasteboardChangeDateKey]; | |
241 [shared_user_defaults_ setObject:last_pasteboard_entry_md5_ | |
242 forKey:kPasteboardEntryMD5Key]; | |
243 } | 80 } |
244 | 81 |
245 base::TimeDelta ClipboardRecentContentIOS::Uptime() const { | 82 void ClipboardRecentContentIOS::SetLastPasteboardChangeDate(NSDate* date) { |
246 return base::SysInfo::Uptime(); | 83 [implementation_ setLastPasteboardChangeDate:date]; |
247 } | 84 } |
85 | |
86 NSArray* ClipboardRecentContentIOS::GetAuthorizedSchemeList( | |
sdefresne
2017/04/03 09:27:16
This can be a free function, no need to export thi
lody
2017/04/04 13:42:18
Done.
| |
87 const std::string& application_scheme) { | |
88 NSMutableArray* schemes = [NSMutableArray array]; | |
89 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) { | |
90 [schemes addObject:base::SysUTF8ToNSString(kAuthorizedSchemes[i])]; | |
91 } | |
92 if (!application_scheme.empty()) { | |
93 [schemes addObject:base::SysUTF8ToNSString(application_scheme)]; | |
94 } | |
95 | |
96 return schemes; | |
97 } | |
OLD | NEW |