OLD | NEW |
---|---|
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "components/google/core/browser/google_url_tracker.h" | 5 #include "components/google/core/browser/google_url_tracker.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/command_line.h" | 8 #include "base/command_line.h" |
9 #include "base/prefs/pref_service.h" | 9 #include "base/prefs/pref_service.h" |
10 #include "base/strings/string_util.h" | 10 #include "base/strings/string_util.h" |
11 #include "components/google/core/browser/google_pref_names.h" | 11 #include "components/google/core/browser/google_pref_names.h" |
12 #include "components/google/core/browser/google_switches.h" | 12 #include "components/google/core/browser/google_switches.h" |
13 #include "components/google/core/browser/google_url_tracker_infobar_delegate.h" | |
14 #include "components/google/core/browser/google_url_tracker_navigation_helper.h" | |
15 #include "components/google/core/browser/google_util.h" | 13 #include "components/google/core/browser/google_util.h" |
16 #include "components/infobars/core/infobar.h" | |
17 #include "components/infobars/core/infobar_manager.h" | |
18 #include "net/base/load_flags.h" | 14 #include "net/base/load_flags.h" |
19 #include "net/base/net_util.h" | |
20 #include "net/url_request/url_fetcher.h" | 15 #include "net/url_request/url_fetcher.h" |
21 #include "net/url_request/url_request_status.h" | 16 #include "net/url_request/url_request_status.h" |
22 | 17 |
23 | 18 |
24 const char GoogleURLTracker::kDefaultGoogleHomepage[] = | 19 const char GoogleURLTracker::kDefaultGoogleHomepage[] = |
25 "http://www.google.com/"; | 20 "http://www.google.com/"; |
26 const char GoogleURLTracker::kSearchDomainCheckURL[] = | 21 const char GoogleURLTracker::kSearchDomainCheckURL[] = |
27 "https://www.google.com/searchdomaincheck?format=url&type=chrome"; | 22 "https://www.google.com/searchdomaincheck?format=url&type=chrome"; |
28 | 23 |
29 GoogleURLTracker::GoogleURLTracker(scoped_ptr<GoogleURLTrackerClient> client, | 24 GoogleURLTracker::GoogleURLTracker(scoped_ptr<GoogleURLTrackerClient> client, |
30 Mode mode) | 25 Mode mode) |
31 : client_(client.Pass()), | 26 : client_(client.Pass()), |
32 google_url_(mode == UNIT_TEST_MODE ? | 27 google_url_(mode == UNIT_TEST_MODE ? |
33 kDefaultGoogleHomepage : | 28 kDefaultGoogleHomepage : |
34 client_->GetPrefs()->GetString(prefs::kLastKnownGoogleURL)), | 29 client_->GetPrefs()->GetString(prefs::kLastKnownGoogleURL)), |
35 fetcher_id_(0), | 30 fetcher_id_(0), |
36 in_startup_sleep_(true), | 31 in_startup_sleep_(true), |
37 already_fetched_(false), | 32 already_fetched_(false), |
38 need_to_fetch_(false), | 33 need_to_fetch_(false), |
39 need_to_prompt_(false), | |
40 search_committed_(false), | |
41 weak_ptr_factory_(this) { | 34 weak_ptr_factory_(this) { |
42 net::NetworkChangeNotifier::AddNetworkChangeObserver(this); | 35 net::NetworkChangeNotifier::AddNetworkChangeObserver(this); |
43 client_->set_google_url_tracker(this); | 36 client_->set_google_url_tracker(this); |
44 | 37 |
45 // Because this function can be called during startup, when kicking off a URL | 38 // Because this function can be called during startup, when kicking off a URL |
46 // fetch can eat up 20 ms of time, we delay five seconds, which is hopefully | 39 // fetch can eat up 20 ms of time, we delay five seconds, which is hopefully |
47 // long enough to be after startup, but still get results back quickly. | 40 // long enough to be after startup, but still get results back quickly. |
48 // Ideally, instead of this timer, we'd do something like "check if the | 41 // Ideally, instead of this timer, we'd do something like "check if the |
49 // browser is starting up, and if so, come back later", but there is currently | 42 // browser is starting up, and if so, come back later", but there is currently |
50 // no function to do this. | 43 // no function to do this. |
51 // | 44 // |
52 // In UNIT_TEST_MODE, where we want to explicitly control when the tracker | 45 // In UNIT_TEST_MODE, where we want to explicitly control when the tracker |
53 // "wakes up", we do nothing at all. | 46 // "wakes up", we do nothing at all. |
54 if (mode == NORMAL_MODE) { | 47 if (mode == NORMAL_MODE) { |
55 static const int kStartFetchDelayMS = 5000; | 48 static const int kStartFetchDelayMS = 5000; |
56 base::MessageLoop::current()->PostDelayedTask(FROM_HERE, | 49 base::MessageLoop::current()->PostDelayedTask(FROM_HERE, |
57 base::Bind(&GoogleURLTracker::FinishSleep, | 50 base::Bind(&GoogleURLTracker::FinishSleep, |
58 weak_ptr_factory_.GetWeakPtr()), | 51 weak_ptr_factory_.GetWeakPtr()), |
59 base::TimeDelta::FromMilliseconds(kStartFetchDelayMS)); | 52 base::TimeDelta::FromMilliseconds(kStartFetchDelayMS)); |
60 } | 53 } |
61 } | 54 } |
62 | 55 |
63 GoogleURLTracker::~GoogleURLTracker() { | 56 GoogleURLTracker::~GoogleURLTracker() { |
64 // We should only reach here after any tabs and their infobars have been torn | |
65 // down. | |
66 DCHECK(entry_map_.empty()); | |
67 } | 57 } |
68 | 58 |
69 void GoogleURLTracker::RequestServerCheck(bool force) { | 59 void GoogleURLTracker::RequestServerCheck(bool force) { |
70 // If this instance already has a fetcher, SetNeedToFetch() is unnecessary, | 60 // If this instance already has a fetcher, SetNeedToFetch() is unnecessary, |
71 // and changing |already_fetched_| is wrong. | 61 // and changing |already_fetched_| is wrong. |
72 if (!fetcher_) { | 62 if (!fetcher_) { |
73 if (force) | 63 if (force) |
74 already_fetched_ = false; | 64 already_fetched_ = false; |
75 SetNeedToFetch(); | 65 SetNeedToFetch(); |
76 } | 66 } |
77 } | 67 } |
78 | 68 |
79 void GoogleURLTracker::SearchCommitted() { | 69 scoped_ptr<GoogleURLTracker::Subscription> GoogleURLTracker::RegisterCallback( |
80 if (need_to_prompt_) { | 70 const OnGoogleURLUpdatedCallback& cb) { |
81 search_committed_ = true; | 71 return callback_list_.Add(cb); |
Peter Kasting
2014/12/20 02:18:07
Same code as before, just moved up here to match t
| |
82 // These notifications will fire a bit later in the same call chain we're | |
83 // currently in. | |
84 if (!client_->IsListeningForNavigationStart()) | |
85 client_->SetListeningForNavigationStart(true); | |
86 } | |
87 } | |
88 | |
89 void GoogleURLTracker::AcceptGoogleURL(bool redo_searches) { | |
90 GURL old_google_url = google_url_; | |
91 google_url_ = fetched_google_url_; | |
92 PrefService* prefs = client_->GetPrefs(); | |
93 prefs->SetString(prefs::kLastKnownGoogleURL, google_url_.spec()); | |
94 prefs->SetString(prefs::kLastPromptedGoogleURL, google_url_.spec()); | |
95 NotifyGoogleURLUpdated(); | |
96 | |
97 need_to_prompt_ = false; | |
98 CloseAllEntries(redo_searches); | |
99 } | |
100 | |
101 void GoogleURLTracker::CancelGoogleURL() { | |
102 client_->GetPrefs()->SetString(prefs::kLastPromptedGoogleURL, | |
103 fetched_google_url_.spec()); | |
104 need_to_prompt_ = false; | |
105 CloseAllEntries(false); | |
106 } | 72 } |
107 | 73 |
108 void GoogleURLTracker::OnURLFetchComplete(const net::URLFetcher* source) { | 74 void GoogleURLTracker::OnURLFetchComplete(const net::URLFetcher* source) { |
109 // Delete the fetcher on this function's exit. | 75 // Delete the fetcher on this function's exit. |
110 scoped_ptr<net::URLFetcher> clean_up_fetcher(fetcher_.release()); | 76 scoped_ptr<net::URLFetcher> clean_up_fetcher(fetcher_.release()); |
111 | 77 |
112 // Don't update the URL if the request didn't succeed. | 78 // Don't update the URL if the request didn't succeed. |
113 if (!source->GetStatus().is_success() || (source->GetResponseCode() != 200)) { | 79 if (!source->GetStatus().is_success() || (source->GetResponseCode() != 200)) { |
114 already_fetched_ = false; | 80 already_fetched_ = false; |
115 return; | 81 return; |
116 } | 82 } |
117 | 83 |
118 // See if the response data was valid. It should be | 84 // See if the response data was valid. It should be |
119 // "<scheme>://[www.]google.<TLD>/". | 85 // "<scheme>://[www.]google.<TLD>/". |
120 std::string url_str; | 86 std::string url_str; |
121 source->GetResponseAsString(&url_str); | 87 source->GetResponseAsString(&url_str); |
122 base::TrimWhitespace(url_str, base::TRIM_ALL, &url_str); | 88 base::TrimWhitespace(url_str, base::TRIM_ALL, &url_str); |
123 GURL url(url_str); | 89 GURL url(url_str); |
124 if (!url.is_valid() || (url.path().length() > 1) || url.has_query() || | 90 if (!url.is_valid() || (url.path().length() > 1) || url.has_query() || |
125 url.has_ref() || | 91 url.has_ref() || |
126 !google_util::IsGoogleDomainUrl(url, | 92 !google_util::IsGoogleDomainUrl(url, |
127 google_util::DISALLOW_SUBDOMAIN, | 93 google_util::DISALLOW_SUBDOMAIN, |
128 google_util::DISALLOW_NON_STANDARD_PORTS)) | 94 google_util::DISALLOW_NON_STANDARD_PORTS)) |
129 return; | 95 return; |
130 | 96 |
131 std::swap(url, fetched_google_url_); | 97 if (url != google_url_) { |
132 GURL last_prompted_url( | 98 google_url_ = url; |
133 client_->GetPrefs()->GetString(prefs::kLastPromptedGoogleURL)); | 99 client_->GetPrefs()->SetString(prefs::kLastKnownGoogleURL, |
134 | 100 google_url_.spec()); |
135 if (last_prompted_url.is_empty()) { | 101 callback_list_.Notify(); |
136 // On the very first run of Chrome, when we've never looked up the URL at | |
137 // all, we should just silently switch over to whatever we get immediately. | |
138 AcceptGoogleURL(true); // Arg is irrelevant. | |
139 return; | |
140 } | |
141 | |
142 base::string16 fetched_host(net::StripWWWFromHost(fetched_google_url_)); | |
143 if (fetched_google_url_ == google_url_) { | |
144 // Either the user has continually been on this URL, or we prompted for a | |
145 // different URL but have now changed back before they responded to any of | |
146 // the prompts. In this latter case we want to close any infobars and stop | |
147 // prompting. | |
148 CancelGoogleURL(); | |
149 } else if (fetched_host == net::StripWWWFromHost(google_url_)) { | |
150 // Similar to the above case, but this time the new URL differs from the | |
151 // existing one, probably due to switching between HTTP and HTTPS searching. | |
152 // Like before we want to close any infobars and stop prompting; we also | |
153 // want to silently accept the change in scheme. We don't redo open | |
154 // searches so as to avoid suddenly changing a page the user might be | |
155 // interacting with; it's enough to simply get future searches right. | |
156 AcceptGoogleURL(false); | |
157 } else if (fetched_host == net::StripWWWFromHost(last_prompted_url)) { | |
158 // We've re-fetched a TLD the user previously turned down. Although the new | |
159 // URL might have a different scheme than the old, we want to preserve the | |
160 // user's decision. Note that it's possible that, like in the above two | |
161 // cases, we fetched yet another different URL in the meantime, which we | |
162 // have infobars prompting about; in this case, as in those above, we want | |
163 // to go ahead and close the infobars and stop prompting, since we've | |
164 // switched back away from that URL. | |
165 CancelGoogleURL(); | |
166 } else { | |
167 // We've fetched a URL with a different TLD than the user is currently using | |
168 // or was previously prompted about. This means we need to prompt again. | |
169 need_to_prompt_ = true; | |
170 | |
171 // As in all the above cases, there could be infobars prompting about some | |
172 // URL. If these URLs have the same TLD (e.g. for scheme changes), we can | |
173 // simply leave the existing infobars open as their messages will still be | |
174 // accurate. Otherwise we go ahead and close them because we need to | |
175 // display a new message. | |
176 // Note: |url| is the previous |fetched_google_url_|. | |
177 if (url.is_valid() && (fetched_host != net::StripWWWFromHost(url))) | |
178 CloseAllEntries(false); | |
179 } | 102 } |
180 } | 103 } |
181 | 104 |
182 void GoogleURLTracker::OnNetworkChanged( | 105 void GoogleURLTracker::OnNetworkChanged( |
183 net::NetworkChangeNotifier::ConnectionType type) { | 106 net::NetworkChangeNotifier::ConnectionType type) { |
184 // Ignore destructive signals. | 107 // Ignore destructive signals. |
185 if (type == net::NetworkChangeNotifier::CONNECTION_NONE) | 108 if (type == net::NetworkChangeNotifier::CONNECTION_NONE) |
186 return; | 109 return; |
187 already_fetched_ = false; | 110 already_fetched_ = false; |
188 StartFetchIfDesirable(); | 111 StartFetchIfDesirable(); |
189 } | 112 } |
190 | 113 |
191 void GoogleURLTracker::Shutdown() { | 114 void GoogleURLTracker::Shutdown() { |
192 client_.reset(); | 115 client_.reset(); |
193 fetcher_.reset(); | 116 fetcher_.reset(); |
194 weak_ptr_factory_.InvalidateWeakPtrs(); | 117 weak_ptr_factory_.InvalidateWeakPtrs(); |
195 net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this); | 118 net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this); |
196 } | 119 } |
197 | 120 |
198 void GoogleURLTracker::DeleteMapEntryForManager( | |
199 const infobars::InfoBarManager* infobar_manager) { | |
200 // WARNING: |infobar_manager| may point to a deleted object. Do not | |
201 // dereference it! See OnTabClosed(). | |
202 EntryMap::iterator i(entry_map_.find(infobar_manager)); | |
203 DCHECK(i != entry_map_.end()); | |
204 GoogleURLTrackerMapEntry* map_entry = i->second; | |
205 | |
206 UnregisterForEntrySpecificNotifications(map_entry, false); | |
207 entry_map_.erase(i); | |
208 delete map_entry; | |
209 } | |
210 | |
211 void GoogleURLTracker::SetNeedToFetch() { | 121 void GoogleURLTracker::SetNeedToFetch() { |
212 need_to_fetch_ = true; | 122 need_to_fetch_ = true; |
213 StartFetchIfDesirable(); | 123 StartFetchIfDesirable(); |
214 } | 124 } |
215 | 125 |
216 void GoogleURLTracker::FinishSleep() { | 126 void GoogleURLTracker::FinishSleep() { |
217 in_startup_sleep_ = false; | 127 in_startup_sleep_ = false; |
218 StartFetchIfDesirable(); | 128 StartFetchIfDesirable(); |
219 } | 129 } |
220 | 130 |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
254 // Also retry kMaxRetries times on network change errors. A network change can | 164 // Also retry kMaxRetries times on network change errors. A network change can |
255 // propagate through Chrome in various stages, so it's possible for this code | 165 // propagate through Chrome in various stages, so it's possible for this code |
256 // to be reached via OnNetworkChanged(), and then have the fetch we kick off | 166 // to be reached via OnNetworkChanged(), and then have the fetch we kick off |
257 // be canceled due to e.g. the DNS server changing at a later time. In general | 167 // be canceled due to e.g. the DNS server changing at a later time. In general |
258 // it's not possible to ensure that by the time we reach here any requests we | 168 // it's not possible to ensure that by the time we reach here any requests we |
259 // start won't be canceled in this fashion, so retrying is the best we can do. | 169 // start won't be canceled in this fashion, so retrying is the best we can do. |
260 fetcher_->SetAutomaticallyRetryOnNetworkChanges(kMaxRetries); | 170 fetcher_->SetAutomaticallyRetryOnNetworkChanges(kMaxRetries); |
261 | 171 |
262 fetcher_->Start(); | 172 fetcher_->Start(); |
263 } | 173 } |
264 | |
265 void GoogleURLTracker::OnNavigationPending( | |
266 scoped_ptr<GoogleURLTrackerNavigationHelper> nav_helper, | |
267 infobars::InfoBarManager* infobar_manager, | |
268 int pending_id) { | |
269 GoogleURLTrackerMapEntry* map_entry = NULL; | |
270 | |
271 EntryMap::iterator i(entry_map_.find(infobar_manager)); | |
272 if (i != entry_map_.end()) | |
273 map_entry = i->second; | |
274 | |
275 if (search_committed_) { | |
276 search_committed_ = false; | |
277 if (!map_entry) { | |
278 // This is a search on a tab that doesn't have one of our infobars, so | |
279 // prepare to add one. Note that we only listen for the tab's destruction | |
280 // on this path; if there was already a map entry, then either it doesn't | |
281 // yet have an infobar and we're already registered for this, or it has an | |
282 // infobar and the infobar's owner will handle tearing it down when the | |
283 // tab is destroyed. | |
284 map_entry = new GoogleURLTrackerMapEntry( | |
285 this, infobar_manager, nav_helper.Pass()); | |
286 map_entry->navigation_helper()->SetListeningForTabDestruction(true); | |
287 entry_map_.insert(std::make_pair(infobar_manager, map_entry)); | |
288 } else if (map_entry->infobar_delegate()) { | |
289 // This is a new search on a tab where we already have an infobar. | |
290 map_entry->infobar_delegate()->set_pending_id(pending_id); | |
291 } | |
292 | |
293 // Whether there's an existing infobar or not, we need to listen for the | |
294 // load to commit, so we can show and/or update the infobar when it does. | |
295 // (We may already be registered for this if there is an existing infobar | |
296 // that had a previous pending search that hasn't yet committed.) | |
297 if (!map_entry->navigation_helper()->IsListeningForNavigationCommit()) | |
298 map_entry->navigation_helper()->SetListeningForNavigationCommit(true); | |
299 } else if (map_entry) { | |
300 if (map_entry->has_infobar_delegate()) { | |
301 // This is a non-search navigation on a tab with an infobar. If there was | |
302 // a previous pending search on this tab, this means it won't commit, so | |
303 // undo anything we did in response to seeing that. Note that if there | |
304 // was no pending search on this tab, these statements are effectively a | |
305 // no-op. | |
306 // | |
307 // If this navigation actually commits, that will trigger the infobar's | |
308 // owner to expire the infobar if need be. If it doesn't commit, then | |
309 // simply leaving the infobar as-is will have been the right thing. | |
310 UnregisterForEntrySpecificNotifications(map_entry, false); | |
311 map_entry->infobar_delegate()->set_pending_id(0); | |
312 } else { | |
313 // Non-search navigation on a tab with an entry that has not yet created | |
314 // an infobar. This means the original search won't commit, so delete the | |
315 // entry. | |
316 map_entry->Close(false); | |
317 } | |
318 } else { | |
319 // Non-search navigation on a tab without an infobars. This is irrelevant | |
320 // to us. | |
321 } | |
322 } | |
323 | |
324 void GoogleURLTracker::OnNavigationCommitted( | |
325 infobars::InfoBarManager* infobar_manager, | |
326 const GURL& search_url) { | |
327 EntryMap::iterator i(entry_map_.find(infobar_manager)); | |
328 DCHECK(i != entry_map_.end()); | |
329 GoogleURLTrackerMapEntry* map_entry = i->second; | |
330 DCHECK(search_url.is_valid()); | |
331 | |
332 UnregisterForEntrySpecificNotifications(map_entry, true); | |
333 if (map_entry->has_infobar_delegate()) { | |
334 map_entry->infobar_delegate()->Update(search_url); | |
335 } else { | |
336 infobars::InfoBar* infobar = GoogleURLTrackerInfoBarDelegate::Create( | |
337 infobar_manager, this, search_url); | |
338 if (infobar) { | |
339 map_entry->SetInfoBarDelegate( | |
340 static_cast<GoogleURLTrackerInfoBarDelegate*>(infobar->delegate())); | |
341 } else { | |
342 map_entry->Close(false); | |
343 } | |
344 } | |
345 } | |
346 | |
347 void GoogleURLTracker::OnTabClosed( | |
348 GoogleURLTrackerNavigationHelper* nav_helper) { | |
349 // Because InfoBarManager tears itself down on tab destruction, it's possible | |
350 // to get a non-NULL InfoBarManager pointer here, depending on which order | |
351 // notifications fired in. Likewise, the pointer in |entry_map_| (and in its | |
352 // associated MapEntry) may point to deleted memory. Therefore, if we were | |
353 // to access the InfoBarManager* we have for this tab, we'd need to ensure we | |
354 // just looked at the raw pointer value, and never dereferenced it. This | |
355 // function doesn't need to do even that, but others in the call chain from | |
356 // here might (and have comments pointing back here). | |
357 for (EntryMap::iterator i(entry_map_.begin()); i != entry_map_.end(); ++i) { | |
358 if (i->second->navigation_helper() == nav_helper) { | |
359 i->second->Close(false); | |
360 return; | |
361 } | |
362 } | |
363 NOTREACHED(); | |
364 } | |
365 | |
366 scoped_ptr<GoogleURLTracker::Subscription> GoogleURLTracker::RegisterCallback( | |
367 const OnGoogleURLUpdatedCallback& cb) { | |
368 return callback_list_.Add(cb); | |
369 } | |
370 | |
371 void GoogleURLTracker::CloseAllEntries(bool redo_searches) { | |
372 // Delete all entries, whether they have infobars or not. | |
373 while (!entry_map_.empty()) | |
374 entry_map_.begin()->second->Close(redo_searches); | |
375 } | |
376 | |
377 void GoogleURLTracker::UnregisterForEntrySpecificNotifications( | |
378 GoogleURLTrackerMapEntry* map_entry, | |
379 bool must_be_listening_for_commit) { | |
380 // For tabs with map entries but no infobars, we should always be listening | |
381 // for both these notifications. For tabs with infobars, we may be listening | |
382 // for navigation commits if the user has performed a new search on this tab. | |
383 if (map_entry->navigation_helper()->IsListeningForNavigationCommit()) { | |
384 map_entry->navigation_helper()->SetListeningForNavigationCommit(false); | |
385 } else { | |
386 DCHECK(!must_be_listening_for_commit); | |
387 DCHECK(map_entry->has_infobar_delegate()); | |
388 } | |
389 const bool registered_for_tab_destruction = | |
390 map_entry->navigation_helper()->IsListeningForTabDestruction(); | |
391 DCHECK_NE(registered_for_tab_destruction, map_entry->has_infobar_delegate()); | |
392 if (registered_for_tab_destruction) { | |
393 map_entry->navigation_helper()->SetListeningForTabDestruction(false); | |
394 } | |
395 | |
396 // Our global listeners for these other notifications should be in place iff | |
397 // we have any tabs still listening for commits. These tabs either have no | |
398 // infobars or have received new pending searches atop existing infobars; in | |
399 // either case we want to catch subsequent pending non-search navigations. | |
400 // See the various cases inside OnNavigationPending(). | |
401 for (EntryMap::const_iterator i(entry_map_.begin()); i != entry_map_.end(); | |
402 ++i) { | |
403 if (i->second->navigation_helper()->IsListeningForNavigationCommit()) { | |
404 DCHECK(client_->IsListeningForNavigationStart()); | |
405 return; | |
406 } | |
407 } | |
408 if (client_->IsListeningForNavigationStart()) { | |
409 DCHECK(!search_committed_); | |
410 client_->SetListeningForNavigationStart(false); | |
411 } | |
412 } | |
413 | |
414 void GoogleURLTracker::NotifyGoogleURLUpdated() { | |
415 callback_list_.Notify(); | |
416 } | |
OLD | NEW |