1 // Copyright 2013 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 #import "ios/chrome/browser/installation_notifier.h"
7 #import <UIKit/UIKit.h>
9 #include "base/ios/weak_nsobject.h"
10 #include "base/logging.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/metrics/histogram.h"
14 #include "ios/web/public/web_thread.h"
15 #include "net/base/backoff_entry.h"
19 const net::BackoffEntry::Policy kPollingBackoffPolicy = {
20 0, // Number of errors to ignore.
21 1 * 1000, // Initial delay in milliseconds.
22 1.5, // Multiply factor.
23 0.1, // Jitter factor.
24 60 * 1000, // Maximum backoff in milliseconds.
25 -1, // Entry lifetime.
26 false // Always use initial delay.
30 @interface DefaultDispatcher : NSObject<DispatcherProtocol>
33 @implementation DefaultDispatcher
34 - (void)dispatchAfter:(int64_t)delayInNSec withBlock:(dispatch_block_t)block {
35 dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, delayInNSec);
36 dispatch_after(dispatchTime, dispatch_get_main_queue(), block);
40 @interface InstallationNotifier ()
41 // Registers for a notification and gives the option to not immediately start
42 // polling. |scheme| must not be nil nor an empty string.
43 - (void)registerForInstallationNotifications:(id)observer
44 withSelector:(SEL)notificationSelector
45 forScheme:(NSString*)scheme
46 startPolling:(BOOL)poll;
47 // Dispatches a block with an exponentially increasing delay.
48 - (void)dispatchInstallationNotifierBlock;
49 // Dispatched blocks cannot be cancelled. Instead, each block has a |blockId|.
50 // If |blockId| is different from |lastCreatedBlockId_|, then the block does
51 // not execute anything.
52 @property(nonatomic, readonly) int lastCreatedBlockId;
55 @interface InstallationNotifier (Testing)
56 // Sets the dispatcher.
57 - (void)setDispatcher:(id<DispatcherProtocol>)dispatcher;
58 // Sets the UIApplication used to determine if a scheme can be opened by an
60 - (void)setSharedApplication:(UIApplication*)sharedApplication;
63 @implementation InstallationNotifier {
64 scoped_ptr<net::BackoffEntry> _backoffEntry;
65 base::scoped_nsprotocol<id<DispatcherProtocol>> _dispatcher;
66 // Dictionary mapping URL schemes to mutable sets of observers.
67 base::scoped_nsobject<NSMutableDictionary> _installedAppObservers;
68 NSNotificationCenter* _notificationCenter; // Weak.
70 // This object can be a fake application in unittests.
71 UIApplication* sharedApplication_; // Weak.
74 @synthesize lastCreatedBlockId = lastCreatedBlockId_;
76 + (InstallationNotifier*)sharedInstance {
77 static InstallationNotifier* instance = [[InstallationNotifier alloc] init];
81 - (instancetype)init {
84 lastCreatedBlockId_ = 0;
85 _dispatcher.reset([[DefaultDispatcher alloc] init]);
86 _installedAppObservers.reset([[NSMutableDictionary alloc] init]);
87 _notificationCenter = [NSNotificationCenter defaultCenter];
88 sharedApplication_ = [UIApplication sharedApplication];
89 _backoffEntry.reset(new net::BackoffEntry([self backOffPolicy]));
94 - (void)registerForInstallationNotifications:(id)observer
95 withSelector:(SEL)notificationSelector
96 forScheme:(NSString*)scheme {
97 [self registerForInstallationNotifications:observer
98 withSelector:notificationSelector
103 - (void)registerForInstallationNotifications:(id)observer
104 withSelector:(SEL)notificationSelector
105 forScheme:(NSString*)scheme
106 startPolling:(BOOL)poll {
107 // Workaround a crash caused by calls to this function with a nil |scheme|.
108 if (![scheme length])
110 DCHECK([observer respondsToSelector:notificationSelector]);
111 DCHECK([scheme rangeOfString:@":"].location == NSNotFound);
112 // A strong reference would prevent the observer from unregistering itself
113 // from its dealloc method, because the dealloc itself would never be called.
114 NSValue* weakReferenceToObserver =
115 [NSValue valueWithNonretainedObject:observer];
116 NSMutableSet* observers = [_installedAppObservers objectForKey:scheme];
118 observers = [[[NSMutableSet alloc] init] autorelease];
119 if ([observers containsObject:weakReferenceToObserver])
121 [observers addObject:weakReferenceToObserver];
122 [_installedAppObservers setObject:observers forKey:scheme];
123 [_notificationCenter addObserver:observer
124 selector:notificationSelector
127 _backoffEntry->Reset();
129 [self dispatchInstallationNotifierBlock];
132 - (void)unregisterForNotifications:(id)observer {
133 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
134 NSValue* weakReferenceToObserver =
135 [NSValue valueWithNonretainedObject:observer];
136 [_notificationCenter removeObserver:observer];
137 for (NSString* scheme in [_installedAppObservers allKeys]) {
138 DCHECK([scheme isKindOfClass:[NSString class]]);
139 NSMutableSet* observers = [_installedAppObservers objectForKey:scheme];
140 if ([observers containsObject:weakReferenceToObserver]) {
141 [observers removeObject:weakReferenceToObserver];
142 if ([observers count] == 0) {
143 [_installedAppObservers removeObjectForKey:scheme];
144 UMA_HISTOGRAM_BOOLEAN("NativeAppLauncher.InstallationDetected", NO);
151 // Reset the back off polling.
152 _backoffEntry->Reset();
153 [self pollForTheInstallationOfApps];
156 - (void)dispatchInstallationNotifierBlock {
157 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
158 int blockId = ++lastCreatedBlockId_;
159 _backoffEntry->InformOfRequest(false);
160 int64_t delayInNSec =
161 _backoffEntry->GetTimeUntilRelease().InMicroseconds() * NSEC_PER_USEC;
162 base::WeakNSObject<InstallationNotifier> weakSelf(self);
163 [_dispatcher dispatchAfter:delayInNSec
165 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
166 base::scoped_nsobject<InstallationNotifier> strongSelf(
168 if (blockId == [strongSelf lastCreatedBlockId]) {
169 [strongSelf pollForTheInstallationOfApps];
174 - (void)pollForTheInstallationOfApps {
175 DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
176 __block BOOL keepPolling = NO;
177 NSMutableSet* keysToDelete = [NSMutableSet set];
178 [_installedAppObservers enumerateKeysAndObjectsUsingBlock:^(id scheme,
181 DCHECK([scheme isKindOfClass:[NSString class]]);
182 DCHECK([observers isKindOfClass:[NSMutableSet class]]);
183 DCHECK([observers count] > 0);
184 NSURL* testSchemeURL =
185 [NSURL URLWithString:[NSString stringWithFormat:@"%@:", scheme]];
186 if ([sharedApplication_ canOpenURL:testSchemeURL]) {
187 [_notificationCenter postNotificationName:scheme object:self];
188 for (id weakReferenceToObserver in observers) {
189 id observer = [weakReferenceToObserver nonretainedObjectValue];
190 [_notificationCenter removeObserver:observer name:scheme object:self];
192 if (![keysToDelete containsObject:scheme]) {
193 [keysToDelete addObject:scheme];
194 UMA_HISTOGRAM_BOOLEAN("NativeAppLauncher.InstallationDetected", YES);
200 [_installedAppObservers removeObjectsForKeys:[keysToDelete allObjects]];
202 [self dispatchInstallationNotifierBlock];
205 - (net::BackoffEntry::Policy const*)backOffPolicy {
206 return &kPollingBackoffPolicy;
210 #pragma mark Testing setters
212 - (void)setDispatcher:(id<DispatcherProtocol>)dispatcher {
213 _dispatcher.reset(dispatcher);
216 - (void)setSharedApplication:(id)sharedApplication {
217 // Verify that the test application object responds to all the selectors that
218 // will be called on it.
219 CHECK([sharedApplication respondsToSelector:@selector(canOpenURL:)]);
220 sharedApplication_ = (UIApplication*)sharedApplication;