Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / ios / chrome / browser / installation_notifier.mm
blobe403685fd6714fb7ece5f95fcd59e4617955d1cd
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"
16 #include "url/gurl.h"
18 namespace {
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.
28 }  // namespace
30 @interface DefaultDispatcher : NSObject<DispatcherProtocol>
31 @end
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);
38 @end
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;
53 @end
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
59 // application.
60 - (void)setSharedApplication:(UIApplication*)sharedApplication;
61 @end
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];
78   return instance;
81 - (instancetype)init {
82   self = [super init];
83   if (self) {
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]));
90   }
91   return self;
94 - (void)registerForInstallationNotifications:(id)observer
95                                 withSelector:(SEL)notificationSelector
96                                    forScheme:(NSString*)scheme {
97   [self registerForInstallationNotifications:observer
98                                 withSelector:notificationSelector
99                                    forScheme:scheme
100                                 startPolling:YES];
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])
109     return;
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];
117   if (!observers)
118     observers = [[[NSMutableSet alloc] init] autorelease];
119   if ([observers containsObject:weakReferenceToObserver])
120     return;
121   [observers addObject:weakReferenceToObserver];
122   [_installedAppObservers setObject:observers forKey:scheme];
123   [_notificationCenter addObserver:observer
124                           selector:notificationSelector
125                               name:scheme
126                             object:self];
127   _backoffEntry->Reset();
128   if (poll)
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);
145       }
146     }
147   }
150 - (void)checkNow {
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
164                    withBlock:^{
165                      DCHECK_CURRENTLY_ON_WEB_THREAD(web::WebThread::UI);
166                      base::scoped_nsobject<InstallationNotifier> strongSelf(
167                          [weakSelf retain]);
168                      if (blockId == [strongSelf lastCreatedBlockId]) {
169                        [strongSelf pollForTheInstallationOfApps];
170                      }
171                    }];
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,
179                                                               id observers,
180                                                               BOOL* stop) {
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];
191       }
192       if (![keysToDelete containsObject:scheme]) {
193         [keysToDelete addObject:scheme];
194         UMA_HISTOGRAM_BOOLEAN("NativeAppLauncher.InstallationDetected", YES);
195       }
196     } else {
197       keepPolling = YES;
198     }
199   }];
200   [_installedAppObservers removeObjectsForKeys:[keysToDelete allObjects]];
201   if (keepPolling)
202     [self dispatchInstallationNotifierBlock];
205 - (net::BackoffEntry::Policy const*)backOffPolicy {
206   return &kPollingBackoffPolicy;
209 #pragma mark -
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;
223 @end