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 #include "components/open_from_clipboard/clipboard_recent_content_ios.h"
7 #import <UIKit/UIKit.h>
9 #include "base/ios/ios_util.h"
10 #include "base/logging.h"
11 #include "base/macros.h"
12 #include "base/memory/singleton.h"
13 #include "base/metrics/user_metrics.h"
14 #include "base/strings/sys_string_conversions.h"
15 #include "base/sys_info.h"
17 #include "url/url_constants.h"
19 ClipboardRecentContent* ClipboardRecentContent::GetInstance() {
20 return ClipboardRecentContentIOS::GetInstance();
23 // Bridge that forwards pasteboard change notifications to its delegate.
24 @interface PasteboardNotificationListenerBridge : NSObject {
25 ClipboardRecentContentIOS* _delegate;
29 @implementation PasteboardNotificationListenerBridge
31 - (id)initWithDelegate:(ClipboardRecentContentIOS*)delegate {
36 [[NSNotificationCenter defaultCenter]
38 selector:@selector(pasteboardChangedNotification:)
39 name:UIPasteboardChangedNotification
40 object:[UIPasteboard generalPasteboard]];
41 [[NSNotificationCenter defaultCenter]
43 selector:@selector(pasteboardChangedNotification:)
44 name:UIApplicationDidBecomeActiveNotification
51 [[NSNotificationCenter defaultCenter] removeObserver:self];
55 - (void)pasteboardChangedNotification:(NSNotification*)notification {
57 _delegate->PasteboardChanged();
68 // Key used to store the pasteboard's current change count. If when resuming
69 // chrome the pasteboard's change count is different from the stored one, then
70 // it means that the pasteboard's content has changed.
71 NSString* kPasteboardChangeCountKey = @"PasteboardChangeCount";
72 // Key used to store the last date at which it was detected that the pasteboard
73 // changed. It is used to evaluate the age of the pasteboard's content.
74 NSString* kPasteboardChangeDateKey = @"PasteboardChangeDate";
75 // Key used to store the
76 NSString* kSuppressedPasteboardEntryCountKey = @"PasteboardSupressedEntryCount";
77 base::TimeDelta kMaximumAgeOfClipboard = base::TimeDelta::FromHours(3);
78 // Schemes accepted by the ClipboardRecentContentIOS.
79 const char* kAuthorizedSchemes[] = {
87 ClipboardRecentContentIOS* ClipboardRecentContentIOS::GetInstance() {
88 return Singleton<ClipboardRecentContentIOS>::get();
91 bool ClipboardRecentContentIOS::GetRecentURLFromClipboard(GURL* url) const {
93 if (GetClipboardContentAge() > kMaximumAgeOfClipboard ||
94 [UIPasteboard generalPasteboard].changeCount ==
95 suppressedPasteboardEntryCount_) {
99 if (urlFromPasteboardCache_.is_valid()) {
100 *url = urlFromPasteboardCache_;
106 base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const {
107 return base::TimeDelta::FromSeconds(
108 static_cast<int64>(-[lastPasteboardChangeDate_ timeIntervalSinceNow]));
111 void ClipboardRecentContentIOS::SuppressClipboardContent() {
112 suppressedPasteboardEntryCount_ =
113 [UIPasteboard generalPasteboard].changeCount;
114 SaveToUserDefaults();
117 void ClipboardRecentContentIOS::PasteboardChanged() {
118 urlFromPasteboardCache_ = URLFromPasteboard();
119 if (!urlFromPasteboardCache_.is_empty()) {
121 base::UserMetricsAction("MobileOmniboxClipboardChanged"));
123 lastPasteboardChangeDate_.reset([[NSDate date] retain]);
124 lastPasteboardChangeCount_ = [UIPasteboard generalPasteboard].changeCount;
125 if (lastPasteboardChangeCount_ != suppressedPasteboardEntryCount_) {
126 suppressedPasteboardEntryCount_ = NSIntegerMax;
130 ClipboardRecentContentIOS::ClipboardRecentContentIOS() {
131 Init(base::TimeDelta::FromMilliseconds(base::SysInfo::Uptime()));
134 ClipboardRecentContentIOS::ClipboardRecentContentIOS(base::TimeDelta uptime) {
138 void ClipboardRecentContentIOS::Init(base::TimeDelta uptime) {
139 lastPasteboardChangeCount_ = NSIntegerMax;
140 suppressedPasteboardEntryCount_ = NSIntegerMax;
141 urlFromPasteboardCache_ = URLFromPasteboard();
142 LoadFromUserDefaults();
144 // On iOS 7 (unlike on iOS 8, despite what the documentation says), the change
145 // count is reset when the device is rebooted.
146 if (uptime < GetClipboardContentAge() &&
147 !base::ios::IsRunningOnIOS8OrLater()) {
148 if ([UIPasteboard generalPasteboard].changeCount == 0) {
149 // The user hasn't pasted anything in the clipboard since the device's
150 // reboot. |PasteboardChanged| isn't called because it would update
151 // |lastPasteboardChangeData_|, and record metrics.
152 lastPasteboardChangeCount_ = 0;
153 if (suppressedPasteboardEntryCount_ != NSIntegerMax) {
154 // If the last time Chrome was running the pasteboard was suppressed,
155 // and the user has not copied anything since the device launched, then
156 // supress this entry.
157 suppressedPasteboardEntryCount_ = 0;
159 SaveToUserDefaults();
161 // The user pasted something in the clipboard since the device's reboot.
165 NSInteger changeCount = [UIPasteboard generalPasteboard].changeCount;
166 if (changeCount != lastPasteboardChangeCount_) {
170 // Makes sure |lastPasteboardChangeCount_| was properly initialized.
171 DCHECK_NE(lastPasteboardChangeCount_, NSIntegerMax);
172 notificationBridge_.reset(
173 [[PasteboardNotificationListenerBridge alloc] initWithDelegate:this]);
176 ClipboardRecentContentIOS::~ClipboardRecentContentIOS() {
177 [notificationBridge_ disconnect];
180 GURL ClipboardRecentContentIOS::URLFromPasteboard() {
181 const std::string clipboard =
182 base::SysNSStringToUTF8([[UIPasteboard generalPasteboard] string]);
183 GURL gurl = GURL(clipboard);
184 if (gurl.is_valid()) {
185 for (size_t i = 0; i < arraysize(kAuthorizedSchemes); ++i) {
186 if (gurl.SchemeIs(kAuthorizedSchemes[i])) {
190 if (!application_scheme_.empty() &&
191 gurl.SchemeIs(application_scheme_.c_str())) {
195 return GURL::EmptyGURL();
198 void ClipboardRecentContentIOS::LoadFromUserDefaults() {
199 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
201 lastPasteboardChangeCount_ =
202 [defaults integerForKey:kPasteboardChangeCountKey];
203 lastPasteboardChangeDate_.reset(
204 [[defaults objectForKey:kPasteboardChangeDateKey] retain]);
206 if ([[[defaults dictionaryRepresentation] allKeys]
207 containsObject:kSuppressedPasteboardEntryCountKey]) {
208 suppressedPasteboardEntryCount_ =
209 [defaults integerForKey:kSuppressedPasteboardEntryCountKey];
211 suppressedPasteboardEntryCount_ = NSIntegerMax;
214 DCHECK(!lastPasteboardChangeDate_ ||
215 [lastPasteboardChangeDate_ isKindOfClass:[NSDate class]]);
218 void ClipboardRecentContentIOS::SaveToUserDefaults() {
219 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
220 [defaults setInteger:lastPasteboardChangeCount_
221 forKey:kPasteboardChangeCountKey];
222 [defaults setObject:lastPasteboardChangeDate_
223 forKey:kPasteboardChangeDateKey];
224 [defaults setInteger:suppressedPasteboardEntryCount_
225 forKey:kSuppressedPasteboardEntryCountKey];