Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2012 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. | |
| 4 | |
| 5 #import "ios/chrome/browser/upgrade/upgrade_center.h" | |
| 6 | |
| 7 #include <memory> | |
| 8 #include <set> | |
| 9 #include <utility> | |
| 10 | |
| 11 #include "base/mac/bundle_locations.h" | |
| 12 #include "base/mac/scoped_nsobject.h" | |
| 13 #include "base/scoped_observer.h" | |
| 14 #include "base/strings/sys_string_conversions.h" | |
| 15 #include "base/version.h" | |
| 16 #include "components/infobars/core/confirm_infobar_delegate.h" | |
| 17 #include "components/infobars/core/infobar.h" | |
| 18 #include "components/infobars/core/infobar_manager.h" | |
| 19 #include "components/version_info/version_info.h" | |
| 20 #import "ios/chrome/browser/open_url_util.h" | |
| 21 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" | |
| 22 #import "ios/chrome/browser/ui/commands/open_url_command.h" | |
| 23 #include "ios/chrome/grit/ios_chromium_strings.h" | |
| 24 #include "ios/chrome/grit/ios_strings.h" | |
| 25 #import "ios/web/public/url_scheme_util.h" | |
| 26 #import "net/base/mac/url_conversions.h" | |
| 27 #include "ui/base/l10n/l10n_util.h" | |
| 28 #include "ui/gfx/image/image.h" | |
| 29 #include "url/gurl.h" | |
| 30 | |
| 31 @interface UpgradeCenter () | |
| 32 // Create infobars on all tabs. | |
|
sdefresne
2016/12/14 11:50:56
nit: Creates?
rohitrao (ping after 24h)
2016/12/14 13:26:33
Done.
| |
| 33 - (void)showUpgradeInfoBars; | |
| 34 // Remove all the infobars. | |
|
sdefresne
2016/12/14 11:50:56
nit: Removes?
rohitrao (ping after 24h)
2016/12/14 13:26:33
Done.
| |
| 35 - (void)hideUpgradeInfoBars; | |
| 36 // Callback when an infobar is closed, for any reason. Perform upgrade is set to | |
| 37 // YES if the user choose to upgrade. | |
| 38 - (void)dismissedInfoBar:(NSString*)tabId performUpgrade:(BOOL)shouldUpgrade; | |
| 39 // Return YES if the infobar should be shown. | |
|
sdefresne
2016/12/14 11:50:56
nit: Returns?
rohitrao (ping after 24h)
2016/12/14 13:26:33
Done.
| |
| 40 - (BOOL)shouldShowInfoBar; | |
| 41 // Return YES if the last version signaled by a server side service is more | |
|
sdefresne
2016/12/14 11:50:56
nit: Returns?
rohitrao (ping after 24h)
2016/12/14 13:26:33
Done.
| |
| 42 // recent than the current version. | |
| 43 - (BOOL)isCurrentVersionObsolete; | |
| 44 // Returns YES if the infobar has already been shown within the allowed display | |
| 45 // interval. | |
| 46 - (BOOL)infoBarShownRecently; | |
| 47 // Called when the application become active again. | |
| 48 - (void)applicationWillEnterForeground:(NSNotification*)note; | |
| 49 @end | |
| 50 | |
| 51 namespace { | |
| 52 | |
| 53 // The user defaults key for the upgrade version. | |
| 54 NSString* const kNextVersionKey = @"UpdateInfobarNextVersion"; | |
| 55 // The user defaults key for the upgrade URL. | |
| 56 NSString* const kUpgradeURLKey = @"UpdateInfobarUpgradeURL"; | |
| 57 // The user defaults key for the last time the update infobar was shown. | |
| 58 NSString* const kLastInfobarDisplayTimeKey = @"UpdateInfobarLastDisplayTime"; | |
| 59 // The amount of time that must elapse before showing the infobar again. | |
| 60 const NSTimeInterval kInfobarDisplayInterval = 24 * 60 * 60; // One day. | |
| 61 | |
| 62 // The class controlling the look of the infobar displayed when an upgrade is | |
| 63 // available. | |
| 64 class UpgradeInfoBarDelegate : public ConfirmInfoBarDelegate { | |
| 65 public: | |
| 66 UpgradeInfoBarDelegate() : trigger_upgrade_(false) {} | |
| 67 | |
| 68 ~UpgradeInfoBarDelegate() override {} | |
| 69 | |
| 70 // Returns true is the infobar was closed by pressing the accept button. | |
| 71 bool AcceptPressed() { return trigger_upgrade_; } | |
| 72 | |
| 73 void RemoveSelf() { | |
| 74 infobars::InfoBar* infobar = this->infobar(); | |
| 75 if (infobar) | |
| 76 infobar->RemoveSelf(); | |
| 77 } | |
| 78 | |
| 79 private: | |
| 80 InfoBarIdentifier GetIdentifier() const override { | |
| 81 return UPGRADE_INFOBAR_DELEGATE_IOS; | |
| 82 } | |
| 83 | |
| 84 bool ShouldExpire(const NavigationDetails& details) const override { | |
| 85 return false; | |
| 86 } | |
| 87 | |
| 88 gfx::Image GetIcon() const override { | |
| 89 if (icon_.IsEmpty()) { | |
| 90 icon_ = gfx::Image([UIImage imageNamed:@"infobar_update"], | |
| 91 base::scoped_policy::RETAIN); | |
| 92 } | |
| 93 return icon_; | |
| 94 } | |
| 95 | |
| 96 InfoBarDelegate::Type GetInfoBarType() const override { | |
| 97 return PAGE_ACTION_TYPE; | |
| 98 } | |
| 99 | |
| 100 base::string16 GetMessageText() const override { | |
| 101 return l10n_util::GetStringUTF16(IDS_IOS_UPGRADE_AVAILABLE); | |
| 102 } | |
| 103 | |
| 104 bool Accept() override { | |
| 105 trigger_upgrade_ = true; | |
| 106 return true; | |
| 107 } | |
| 108 | |
| 109 int GetButtons() const override { return BUTTON_OK; } | |
| 110 | |
| 111 base::string16 GetButtonLabel(InfoBarButton button) const override { | |
| 112 DCHECK(button == BUTTON_OK); | |
| 113 return l10n_util::GetStringUTF16(IDS_IOS_UPGRADE_AVAILABLE_BUTTON); | |
| 114 } | |
| 115 | |
| 116 mutable gfx::Image icon_; | |
| 117 bool trigger_upgrade_; | |
| 118 | |
| 119 DISALLOW_COPY_AND_ASSIGN(UpgradeInfoBarDelegate); | |
| 120 }; | |
| 121 | |
| 122 // The InfoBarDelegate unfortunately is not called at all when an infoBar is | |
| 123 // simply dismissed. In order to catch that case this object listens to the | |
| 124 // infobars::InfoBarManager::Observer::OnInfoBarRemoved() which is invoked when | |
| 125 // an infobar is closed, for any reason. | |
| 126 class UpgradeInfoBarDismissObserver | |
| 127 : public infobars::InfoBarManager::Observer { | |
| 128 public: | |
| 129 UpgradeInfoBarDismissObserver() | |
| 130 : infobar_delegate_(nullptr), | |
| 131 dismiss_delegate_(nil), | |
| 132 scoped_observer_(this) {} | |
| 133 | |
| 134 ~UpgradeInfoBarDismissObserver() override {} | |
| 135 | |
| 136 void RegisterObserver(infobars::InfoBarManager* infobar_manager, | |
| 137 UpgradeInfoBarDelegate* infobar_delegate, | |
| 138 NSString* tab_id, | |
| 139 UpgradeCenter* dismiss_delegate) { | |
| 140 scoped_observer_.Add(infobar_manager); | |
| 141 infobar_delegate_ = infobar_delegate; | |
| 142 dismiss_delegate_ = dismiss_delegate; | |
| 143 tab_id_.reset([tab_id copy]); | |
| 144 } | |
| 145 | |
| 146 UpgradeInfoBarDelegate* infobar_delegate() { return infobar_delegate_; } | |
| 147 | |
| 148 private: | |
| 149 // infobars::InfoBarManager::Observer implementation. | |
| 150 void OnInfoBarRemoved(infobars::InfoBar* infobar, bool animate) override { | |
| 151 if (infobar->delegate() == infobar_delegate_) { | |
| 152 [dismiss_delegate_ dismissedInfoBar:tab_id_.get() | |
| 153 performUpgrade:infobar_delegate_->AcceptPressed()]; | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 void OnManagerShuttingDown( | |
| 158 infobars::InfoBarManager* infobar_manager) override { | |
| 159 scoped_observer_.Remove(infobar_manager); | |
| 160 } | |
| 161 | |
| 162 UpgradeInfoBarDelegate* infobar_delegate_; | |
| 163 UpgradeCenter* dismiss_delegate_; | |
| 164 base::scoped_nsobject<NSString> tab_id_; | |
| 165 ScopedObserver<infobars::InfoBarManager, infobars::InfoBarManager::Observer> | |
| 166 scoped_observer_; | |
| 167 | |
| 168 DISALLOW_COPY_AND_ASSIGN(UpgradeInfoBarDismissObserver); | |
| 169 }; | |
| 170 | |
| 171 } // namespace | |
| 172 | |
| 173 // The delegateHolder is a simple wrapper to be able to store all the | |
| 174 // infoBarDelegate related information in an object that can be put in | |
| 175 // an ObjectiveC container. | |
| 176 @interface DelegateHolder : NSObject { | |
| 177 UpgradeInfoBarDismissObserver observer_; | |
| 178 } | |
| 179 | |
| 180 - (instancetype)initWithInfoBarManager:(infobars::InfoBarManager*)infoBarManager | |
| 181 infoBarDelegate:(UpgradeInfoBarDelegate*)infoBarDelegate | |
| 182 upgradeCenter:(UpgradeCenter*)upgradeCenter | |
| 183 tabId:(NSString*)tabId; | |
| 184 | |
| 185 @property(nonatomic, readonly) UpgradeInfoBarDelegate* infoBarDelegate; | |
| 186 @end | |
| 187 | |
| 188 @implementation DelegateHolder | |
| 189 | |
| 190 - (instancetype)initWithInfoBarManager:(infobars::InfoBarManager*)infoBarManager | |
| 191 infoBarDelegate:(UpgradeInfoBarDelegate*)infoBarDelegate | |
| 192 upgradeCenter:(UpgradeCenter*)upgradeCenter | |
| 193 tabId:(NSString*)tabId { | |
| 194 self = [super init]; | |
| 195 if (self) { | |
| 196 observer_.RegisterObserver(infoBarManager, infoBarDelegate, tabId, | |
| 197 upgradeCenter); | |
| 198 } | |
| 199 return self; | |
| 200 } | |
| 201 | |
| 202 - (UpgradeInfoBarDelegate*)infoBarDelegate { | |
| 203 return observer_.infobar_delegate(); | |
| 204 } | |
| 205 | |
| 206 @end | |
| 207 | |
| 208 @implementation UpgradeCenter { | |
| 209 // YES if the infobars are currently visible. | |
| 210 BOOL upgradeInfoBarIsVisible_; | |
| 211 // Used to store the visible upgrade infobars, indexed by tabId. | |
| 212 base::scoped_nsobject<NSMutableDictionary> upgradeInfoBarDelegates_; | |
| 213 // Stores the clients of the upgrade center. These objectiveC objects are not | |
| 214 // retained. | |
| 215 std::set<id<UpgradeCenterClientProtocol>> clients_; | |
| 216 #ifndef NDEBUG | |
|
sdefresne
2016/12/14 11:50:56
nit: I think all "#ifndef NDEBUG" should be change
rohitrao (ping after 24h)
2016/12/14 13:26:33
Noted. Not changing in this CL.
| |
| 217 bool inCallback_; | |
| 218 #endif | |
| 219 } | |
| 220 | |
| 221 + (UpgradeCenter*)sharedInstance { | |
| 222 static UpgradeCenter* obj; | |
| 223 static dispatch_once_t onceToken; | |
| 224 dispatch_once(&onceToken, ^{ | |
| 225 obj = [[self alloc] init]; | |
| 226 }); | |
| 227 return obj; | |
| 228 } | |
| 229 | |
| 230 - (instancetype)init { | |
| 231 self = [super init]; | |
| 232 if (self) { | |
| 233 upgradeInfoBarDelegates_.reset([[NSMutableDictionary alloc] init]); | |
| 234 | |
| 235 // There is no dealloc and no unregister as this class is a never | |
| 236 // deallocated singleton. | |
| 237 [[NSNotificationCenter defaultCenter] | |
| 238 addObserver:self | |
| 239 selector:@selector(applicationWillEnterForeground:) | |
| 240 name:UIApplicationWillEnterForegroundNotification | |
| 241 object:nil]; | |
| 242 | |
| 243 upgradeInfoBarIsVisible_ = [self shouldShowInfoBar]; | |
| 244 } | |
| 245 return self; | |
| 246 } | |
| 247 | |
| 248 // Return YES if the infobar should be shown. | |
|
sdefresne
2016/12/14 11:50:56
Comment is already at the top of the file where th
rohitrao (ping after 24h)
2016/12/14 13:26:33
Done.
| |
| 249 - (BOOL)shouldShowInfoBar { | |
| 250 return [self isCurrentVersionObsolete] && ![self infoBarShownRecently]; | |
| 251 } | |
| 252 | |
| 253 - (BOOL)isCurrentVersionObsolete { | |
| 254 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; | |
| 255 NSString* nextVersion = [defaults stringForKey:kNextVersionKey]; | |
| 256 if (nextVersion) { | |
| 257 base::Version current_version(version_info::GetVersionNumber()); | |
| 258 const std::string upgrade = base::SysNSStringToUTF8(nextVersion); | |
| 259 return current_version < base::Version(upgrade); | |
| 260 } | |
| 261 return NO; | |
| 262 } | |
| 263 | |
| 264 - (BOOL)infoBarShownRecently { | |
| 265 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; | |
| 266 NSDate* lastDisplay = [defaults objectForKey:kLastInfobarDisplayTimeKey]; | |
| 267 // Absolute value is to ensure the infobar won't be supressed forever if the | |
| 268 // clock temporarily jumps to the distant future. | |
| 269 if (lastDisplay && | |
| 270 fabs([lastDisplay timeIntervalSinceNow]) < kInfobarDisplayInterval) { | |
| 271 return YES; | |
| 272 } | |
| 273 return NO; | |
| 274 } | |
| 275 | |
| 276 - (void)applicationWillEnterForeground:(NSNotification*)note { | |
| 277 if (upgradeInfoBarIsVisible_) | |
| 278 return; | |
| 279 | |
| 280 // When returning to active if the upgrade notification has been dismissed, | |
| 281 // bring it back. | |
| 282 if ([self shouldShowInfoBar]) | |
| 283 [self showUpgradeInfoBars]; | |
| 284 } | |
| 285 | |
| 286 - (void)registerClient:(id<UpgradeCenterClientProtocol>)client { | |
| 287 clients_.insert(client); | |
| 288 if (upgradeInfoBarIsVisible_) | |
| 289 [client showUpgrade:self]; | |
| 290 } | |
| 291 | |
| 292 - (void)unregisterClient:(id<UpgradeCenterClientProtocol>)client { | |
| 293 #ifndef NDEBUG | |
| 294 DCHECK(!inCallback_); | |
| 295 #endif | |
| 296 clients_.erase(client); | |
| 297 } | |
| 298 | |
| 299 // For the client to call when -showUpgrade: is called or when a new tab is | |
|
sdefresne
2016/12/14 11:50:56
The comment is already in the header file, please
rohitrao (ping after 24h)
2016/12/14 13:26:33
Done.
| |
| 300 // created. The infobar will not be created if is already there or if there is | |
| 301 // no need to do so. | |
| 302 - (void)addInfoBarToManager:(infobars::InfoBarManager*)infoBarManager | |
| 303 forTabId:(NSString*)tabId { | |
| 304 DCHECK(tabId); | |
| 305 DCHECK(infoBarManager); | |
| 306 | |
| 307 // Nothing to do if the infobar are not visible at this point in time. | |
| 308 if (!upgradeInfoBarIsVisible_) | |
| 309 return; | |
| 310 | |
| 311 // Nothing to do if the infobar is already there. | |
| 312 if ([upgradeInfoBarDelegates_ objectForKey:tabId]) | |
| 313 return; | |
| 314 | |
| 315 std::unique_ptr<UpgradeInfoBarDelegate> infobarDelegate( | |
|
sdefresne
2016/12/14 11:50:56
nit: use base::MakeUnique
auto infobarDelegate
rohitrao (ping after 24h)
2016/12/14 13:26:33
Done.
| |
| 316 new UpgradeInfoBarDelegate); | |
| 317 base::scoped_nsobject<DelegateHolder> delegateHolder([[DelegateHolder alloc] | |
| 318 initWithInfoBarManager:infoBarManager | |
| 319 infoBarDelegate:infobarDelegate.get() | |
| 320 upgradeCenter:self | |
| 321 tabId:tabId]); | |
| 322 | |
| 323 [upgradeInfoBarDelegates_ setObject:delegateHolder forKey:tabId]; | |
| 324 infoBarManager->AddInfoBar( | |
| 325 infoBarManager->CreateConfirmInfoBar(std::move(infobarDelegate))); | |
| 326 } | |
| 327 | |
| 328 - (void)tabWillClose:(NSString*)tabId { | |
| 329 [upgradeInfoBarDelegates_ removeObjectForKey:tabId]; | |
| 330 } | |
| 331 | |
| 332 // Callback when an infobar is closed, for any reason. Perform upgrade is set to | |
|
sdefresne
2016/12/14 11:50:56
Comment is already at the top of the file when the
rohitrao (ping after 24h)
2016/12/14 13:26:33
Done.
| |
| 333 // YES if the user choose to upgrade. | |
| 334 - (void)dismissedInfoBar:(NSString*)tabId performUpgrade:(BOOL)shouldUpgrade { | |
| 335 // If the tabId is not in the upgradeInfoBarDelegates_ just ignore the | |
| 336 // notification. In all likelyhood it was trigerred by calling | |
| 337 // -hideUpgradeInfoBars. Or because a tab was closed without dismissing the | |
| 338 // infobar. | |
| 339 base::scoped_nsobject<DelegateHolder> delegateHolder( | |
| 340 [[upgradeInfoBarDelegates_ objectForKey:tabId] retain]); | |
| 341 if (!delegateHolder.get()) | |
| 342 return; | |
| 343 | |
| 344 // Forget about this dismissed infobar. | |
| 345 [upgradeInfoBarDelegates_ removeObjectForKey:tabId]; | |
| 346 | |
| 347 // Get rid of all the infobars on the other tabs. | |
| 348 [self hideUpgradeInfoBars]; | |
| 349 | |
| 350 if (shouldUpgrade) { | |
| 351 NSString* urlString = | |
| 352 [[NSUserDefaults standardUserDefaults] valueForKey:kUpgradeURLKey]; | |
| 353 if (!urlString) | |
| 354 return; // Missing URL, no upgrade possible. | |
| 355 | |
| 356 GURL url = GURL(base::SysNSStringToUTF8(urlString)); | |
| 357 if (!url.is_valid()) | |
| 358 return; | |
| 359 | |
| 360 if (web::UrlHasWebScheme(url)) { | |
| 361 // This URL can be opened in the application, just open in a new tab. | |
| 362 base::scoped_nsobject<OpenUrlCommand> command( | |
| 363 [[OpenUrlCommand alloc] initWithURLFromChrome:url]); | |
| 364 UIWindow* main_window = [[UIApplication sharedApplication] keyWindow]; | |
| 365 DCHECK(main_window); | |
| 366 [main_window chromeExecuteCommand:command]; | |
| 367 } else { | |
| 368 // This URL scheme is not understood, ask the system to open it. | |
| 369 NSURL* nsurl = [NSURL URLWithString:urlString]; | |
| 370 if (nsurl) { | |
| 371 OpenUrlWithCompletionHandler(nsurl, nil); | |
| 372 } | |
| 373 } | |
| 374 } | |
| 375 } | |
| 376 | |
| 377 - (void)showUpgradeInfoBars { | |
| 378 // Add an infobar on all the open tabs. | |
| 379 #ifndef NDEBUG | |
| 380 inCallback_ = YES; | |
| 381 #endif | |
| 382 upgradeInfoBarIsVisible_ = YES; | |
| 383 std::set<id<UpgradeCenterClientProtocol>>::iterator it; | |
| 384 for (it = clients_.begin(); it != clients_.end(); ++it) | |
| 385 [*it showUpgrade:self]; | |
| 386 #ifndef NDEBUG | |
| 387 inCallback_ = NO; | |
| 388 #endif | |
| 389 | |
| 390 [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] | |
| 391 forKey:kLastInfobarDisplayTimeKey]; | |
| 392 } | |
| 393 | |
| 394 - (void)hideUpgradeInfoBars { | |
| 395 upgradeInfoBarIsVisible_ = NO; | |
| 396 // It is important to call -allKeys here and not using a fast iteration on the | |
| 397 // dictionary directly: the dictionary is modified as we go... | |
| 398 for (NSString* tabId in [upgradeInfoBarDelegates_ allKeys]) { | |
| 399 // It is important to retain the delegateHolder as otherwise it is | |
| 400 // deallocated as soon as it is removed from the dictionary. | |
| 401 base::scoped_nsobject<DelegateHolder> delegateHolder( | |
| 402 [[upgradeInfoBarDelegates_ objectForKey:tabId] retain]); | |
| 403 if (delegateHolder.get()) { | |
| 404 [upgradeInfoBarDelegates_ removeObjectForKey:tabId]; | |
| 405 UpgradeInfoBarDelegate* delegate = [delegateHolder infoBarDelegate]; | |
| 406 DCHECK(delegate); | |
| 407 delegate->RemoveSelf(); | |
| 408 } | |
| 409 } | |
| 410 } | |
| 411 | |
| 412 - (void)upgradeNotificationDidOccur:(const UpgradeRecommendedDetails&)details { | |
| 413 const GURL& upgradeUrl = details.upgrade_url; | |
| 414 | |
| 415 if (!upgradeUrl.is_valid()) { | |
| 416 // The application may crash if the URL is invalid. As the URL is defined | |
| 417 // externally to the application it needs to bail right away and ignore the | |
| 418 // upgrade notification. | |
| 419 NOTREACHED(); | |
| 420 return; | |
| 421 } | |
| 422 | |
| 423 if (!details.next_version.size() || | |
| 424 !base::Version(details.next_version).IsValid()) { | |
| 425 // If the upgrade version is not known or is invalid just ignore the | |
| 426 // upgrade notification. | |
| 427 NOTREACHED(); | |
| 428 return; | |
| 429 } | |
| 430 | |
| 431 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; | |
| 432 | |
| 433 // Reset the display clock when the version changes. | |
| 434 NSString* newVersionString = base::SysUTF8ToNSString(details.next_version); | |
| 435 NSString* previousVersionString = [defaults stringForKey:kNextVersionKey]; | |
| 436 if (!previousVersionString || | |
| 437 ![previousVersionString isEqualToString:newVersionString]) { | |
| 438 [defaults removeObjectForKey:kLastInfobarDisplayTimeKey]; | |
| 439 } | |
| 440 | |
| 441 [defaults setValue:base::SysUTF8ToNSString(upgradeUrl.spec()) | |
| 442 forKey:kUpgradeURLKey]; | |
| 443 [defaults setValue:newVersionString forKey:kNextVersionKey]; | |
| 444 | |
| 445 if ([self shouldShowInfoBar]) | |
| 446 [self showUpgradeInfoBars]; | |
| 447 } | |
| 448 | |
| 449 - (void)resetForTests { | |
| 450 [[UpgradeCenter sharedInstance] hideUpgradeInfoBars]; | |
| 451 NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; | |
| 452 [defaults removeObjectForKey:kNextVersionKey]; | |
| 453 [defaults removeObjectForKey:kUpgradeURLKey]; | |
| 454 [defaults removeObjectForKey:kLastInfobarDisplayTimeKey]; | |
| 455 clients_.clear(); | |
| 456 } | |
| 457 | |
| 458 - (void)setLastDisplayToPast { | |
| 459 NSDate* pastDate = | |
| 460 [NSDate dateWithTimeIntervalSinceNow:-(kInfobarDisplayInterval + 1)]; | |
| 461 [[NSUserDefaults standardUserDefaults] setObject:pastDate | |
| 462 forKey:kLastInfobarDisplayTimeKey]; | |
| 463 } | |
| 464 | |
| 465 @end | |
| OLD | NEW |