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