blob: e403685fd6714fb7ece5f95fcd59e4617955d1cd [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/installation_notifier.h"
#import <UIKit/UIKit.h>
#include "base/ios/weak_nsobject.h"
#include "base/logging.h"
#include "base/mac/scoped_nsobject.h"
#include "base/memory/scoped_ptr.h"
#include "base/metrics/histogram.h"
#include "ios/web/public/web_thread.h"
#include "net/base/backoff_entry.h"
#include "url/gurl.h"
namespace {
const net::BackoffEntry::Policy kPollingBackoffPolicy = {
0, // Number of errors to ignore.
1 * 1000, // Initial delay in milliseconds.
1.5, // Multiply factor.
0.1, // Jitter factor.
60 * 1000, // Maximum backoff in milliseconds.
-1, // Entry lifetime.
false // Always use initial delay.
};
} // namespace
@interface DefaultDispatcher : NSObject<DispatcherProtocol>
@end
@implementation DefaultDispatcher
- (void)dispatchAfter:(int64_t)delayInNSec withBlock:(dispatch_block_t)block {
dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, delayInNSec);
dispatch_after(dispatchTime, dispatch_get_main_queue(), block);
}
@end
@interface InstallationNotifier ()
// Registers for a notification and gives the option to not immediately start
// polling. |scheme| must not be nil nor an empty string.
- (void)registerForInstallationNotifications:(id)observer
withSelector:(SEL)notificationSelector
forScheme:(NSString*)scheme
startPolling:(BOOL)poll;
// Dispatches a block with an exponentially increasing delay.
- (void)dispatchInstallationNotifierBlock;
// Dispatched blocks cannot be cancelled. Instead, each block has a |blockId|.
// If |blockId| is different from |lastCreatedBlockId_|, then the block does
// not execute anything.
@property(nonatomic, readonly) int lastCreatedBlockId;
@end
@interface InstallationNotifier (Testing)
// Sets the dispatcher.
- (void)setDispatcher:(id<DispatcherProtocol>)dispatcher;
// Sets the UIApplication used to determine if a scheme can be opened by an
// application.
- (void)setSharedApplication:(UIApplication*)sharedApplication;
@end
@implementation InstallationNotifier {
scoped_ptr<net::BackoffEntry> _backoffEntry;
base::scoped_nsprotocol<id<DispatcherProtocol>> _dispatcher;
// Dictionary mapping URL schemes to mutable sets of observers.
base::scoped_nsobject<NSMutableDictionary> _installedAppObservers;
NSNotificationCenter* _notificationCenter; // Weak.
// This object can be a fake application in unittests.
UIApplication* sharedApplication_; // Weak.
}
@synthesize lastCreatedBlockId = lastCreatedBlockId_;
+ (InstallationNotifier*)sharedInstance {
static InstallationNotifier* instance = [[InstallationNotifier alloc] init];
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
lastCreatedBlockId_ = 0;
_dispatcher.reset([[DefaultDispatcher alloc] init]);
_installedAppObservers.reset([[NSMutableDictionary alloc] init]);
_notificationCenter = [NSNotificationCenter defaultCenter];
sharedApplication_ = [UIApplication sharedApplication];
_backoffEntry.reset(new net::BackoffEntry([self backOffPolicy]));
}
return self;
}
- (void)registerForInstallationNotifications:(id)observer
withSelector:(SEL)notificationSelector
forScheme:(NSString*)scheme {
[self registerForInstallationNotifications:observer
withSelector:notificationSelector
forScheme:scheme
startPolling:YES];
}
- (void)registerForInstallationNotifications:(id)observer
withSelector:(SEL)notificationSelector
forScheme:(NSString*)scheme
startPolling:(BOOL)poll {
// Workaround a crash caused by calls to this function with a nil |scheme|.
if (![scheme length])
return;
DCHECK([observer respondsToSelector:notificationSelector]);
DCHECK([scheme rangeOfString:@":"].location == NSNotFound);
// A strong reference would prevent the observer from unregistering itself
// from its dealloc method, because the dealloc itself would never be called.
NSValue* weakReferenceToObserver =
[NSValue valueWithNonretainedObject:observer];
NSMutableSet* observers = [_installedAppObservers objectForKey:scheme];
if (!observers)
observers = [[[NSMutableSet alloc] init] autorelease];
if ([observers containsObject:weakReferenceToObserver])
return;
[observers addObject:weakReferenceToObserver];
[_installedAppObservers setObject:observers forKey:scheme];
[_notificationCenter addObserver:observer
selector:notificationSelector
name:scheme
object:self];
_backoffEntry->Reset();
if (poll)
[self dispatchInstallationNotifierBlock];
}
- (void)unregisterForNotifications:(id)observer {
DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
NSValue* weakReferenceToObserver =
[NSValue valueWithNonretainedObject:observer];
[_notificationCenter removeObserver:observer];
for (NSString* scheme in [_installedAppObservers allKeys]) {
DCHECK([scheme isKindOfClass:[NSString class]]);
NSMutableSet* observers = [_installedAppObservers objectForKey:scheme];
if ([observers containsObject:weakReferenceToObserver]) {
[observers removeObject:weakReferenceToObserver];
if ([observers count] == 0) {
[_installedAppObservers removeObjectForKey:scheme];
UMA_HISTOGRAM_BOOLEAN("NativeAppLauncher.InstallationDetected", NO);
}
}
}
}
- (void)checkNow {
// Reset the back off polling.
_backoffEntry->Reset();
[self pollForTheInstallationOfApps];
}
- (void)dispatchInstallationNotifierBlock {
DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
int blockId = ++lastCreatedBlockId_;
_backoffEntry->InformOfRequest(false);
int64_t delayInNSec =
_backoffEntry->GetTimeUntilRelease().InMicroseconds() * NSEC_PER_USEC;
base::WeakNSObject<InstallationNotifier> weakSelf(self);
[_dispatcher dispatchAfter:delayInNSec
withBlock:^{
DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
base::scoped_nsobject<InstallationNotifier> strongSelf(
[weakSelf retain]);
if (blockId == [strongSelf lastCreatedBlockId]) {
[strongSelf pollForTheInstallationOfApps];
}
}];
}
- (void)pollForTheInstallationOfApps {
DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
__block BOOL keepPolling = NO;
NSMutableSet* keysToDelete = [NSMutableSet set];
[_installedAppObservers enumerateKeysAndObjectsUsingBlock:^(id scheme,
id observers,
BOOL* stop) {
DCHECK([scheme isKindOfClass:[NSString class]]);
DCHECK([observers isKindOfClass:[NSMutableSet class]]);
DCHECK([observers count] > 0);
NSURL* testSchemeURL =
[NSURL URLWithString:[NSString stringWithFormat:@"%@:", scheme]];
if ([sharedApplication_ canOpenURL:testSchemeURL]) {
[_notificationCenter postNotificationName:scheme object:self];
for (id weakReferenceToObserver in observers) {
id observer = [weakReferenceToObserver nonretainedObjectValue];
[_notificationCenter removeObserver:observer name:scheme object:self];
}
if (![keysToDelete containsObject:scheme]) {
[keysToDelete addObject:scheme];
UMA_HISTOGRAM_BOOLEAN("NativeAppLauncher.InstallationDetected", YES);
}
} else {
keepPolling = YES;
}
}];
[_installedAppObservers removeObjectsForKeys:[keysToDelete allObjects]];
if (keepPolling)
[self dispatchInstallationNotifierBlock];
}
- (net::BackoffEntry::Policy const*)backOffPolicy {
return &kPollingBackoffPolicy;
}
#pragma mark -
#pragma mark Testing setters
- (void)setDispatcher:(id<DispatcherProtocol>)dispatcher {
_dispatcher.reset(dispatcher);
}
- (void)setSharedApplication:(id)sharedApplication {
// Verify that the test application object responds to all the selectors that
// will be called on it.
CHECK([sharedApplication respondsToSelector:@selector(canOpenURL:)]);
sharedApplication_ = (UIApplication*)sharedApplication;
}
@end