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 package org.chromium.net; | 5 package org.chromium.net; |
6 | 6 |
7 import android.content.Context; | 7 import android.content.Context; |
8 import android.os.Build; | 8 import android.os.Build; |
9 import android.os.ConditionVariable; | 9 import android.os.ConditionVariable; |
10 import android.os.Handler; | 10 import android.os.Handler; |
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
78 @UsedByReflection("CronetEngine.java") | 78 @UsedByReflection("CronetEngine.java") |
79 public CronetUrlRequestContext(final CronetEngine.Builder builder) { | 79 public CronetUrlRequestContext(final CronetEngine.Builder builder) { |
80 CronetLibraryLoader.ensureInitialized(builder.getContext(), builder); | 80 CronetLibraryLoader.ensureInitialized(builder.getContext(), builder); |
81 nativeSetMinLogLevel(getLoggingLevel()); | 81 nativeSetMinLogLevel(getLoggingLevel()); |
82 synchronized (mLock) { | 82 synchronized (mLock) { |
83 mUrlRequestContextAdapter = nativeCreateRequestContextAdapter( | 83 mUrlRequestContextAdapter = nativeCreateRequestContextAdapter( |
84 createNativeUrlRequestContextConfig(builder.getContext(), bu
ilder)); | 84 createNativeUrlRequestContextConfig(builder.getContext(), bu
ilder)); |
85 if (mUrlRequestContextAdapter == 0) { | 85 if (mUrlRequestContextAdapter == 0) { |
86 throw new NullPointerException("Context Adapter creation failed.
"); | 86 throw new NullPointerException("Context Adapter creation failed.
"); |
87 } | 87 } |
| 88 mNetworkQualityEstimatorEnabled = builder.networkQualityEstimatorEna
bled(); |
88 } | 89 } |
89 | 90 |
90 // Init native Chromium URLRequestContext on main UI thread. | 91 // Init native Chromium URLRequestContext on main UI thread. |
91 Runnable task = new Runnable() { | 92 Runnable task = new Runnable() { |
92 @Override | 93 @Override |
93 public void run() { | 94 public void run() { |
94 CronetLibraryLoader.ensureInitializedOnMainThread(builder.getCon
text()); | 95 CronetLibraryLoader.ensureInitializedOnMainThread(builder.getCon
text()); |
95 synchronized (mLock) { | 96 synchronized (mLock) { |
96 // mUrlRequestContextAdapter is guaranteed to exist until | 97 // mUrlRequestContextAdapter is guaranteed to exist until |
97 // initialization on main and network threads completes and | 98 // initialization on main and network threads completes and |
(...skipping 12 matching lines...) Expand all Loading... |
110 | 111 |
111 static long createNativeUrlRequestContextConfig( | 112 static long createNativeUrlRequestContextConfig( |
112 final Context context, CronetEngine.Builder builder) { | 113 final Context context, CronetEngine.Builder builder) { |
113 final long urlRequestContextConfig = nativeCreateRequestContextConfig( | 114 final long urlRequestContextConfig = nativeCreateRequestContextConfig( |
114 builder.getUserAgent(), builder.storagePath(), builder.quicEnabl
ed(), | 115 builder.getUserAgent(), builder.storagePath(), builder.quicEnabl
ed(), |
115 builder.getDefaultQuicUserAgentId(context), builder.http2Enabled
(), | 116 builder.getDefaultQuicUserAgentId(context), builder.http2Enabled
(), |
116 builder.sdchEnabled(), builder.dataReductionProxyKey(), | 117 builder.sdchEnabled(), builder.dataReductionProxyKey(), |
117 builder.dataReductionProxyPrimaryProxy(), builder.dataReductionP
roxyFallbackProxy(), | 118 builder.dataReductionProxyPrimaryProxy(), builder.dataReductionP
roxyFallbackProxy(), |
118 builder.dataReductionProxySecureProxyCheckUrl(), builder.cacheDi
sabled(), | 119 builder.dataReductionProxySecureProxyCheckUrl(), builder.cacheDi
sabled(), |
119 builder.httpCacheMode(), builder.httpCacheMaxSize(), builder.exp
erimentalOptions(), | 120 builder.httpCacheMode(), builder.httpCacheMaxSize(), builder.exp
erimentalOptions(), |
120 builder.mockCertVerifier()); | 121 builder.mockCertVerifier(), builder.networkQualityEstimatorEnabl
ed()); |
121 for (Builder.QuicHint quicHint : builder.quicHints()) { | 122 for (Builder.QuicHint quicHint : builder.quicHints()) { |
122 nativeAddQuicHint(urlRequestContextConfig, quicHint.mHost, quicHint.
mPort, | 123 nativeAddQuicHint(urlRequestContextConfig, quicHint.mHost, quicHint.
mPort, |
123 quicHint.mAlternatePort); | 124 quicHint.mAlternatePort); |
124 } | 125 } |
125 for (Builder.Pkp pkp : builder.publicKeyPins()) { | 126 for (Builder.Pkp pkp : builder.publicKeyPins()) { |
126 nativeAddPkp(urlRequestContextConfig, pkp.mHost, pkp.mHashes, pkp.mI
ncludeSubdomains, | 127 nativeAddPkp(urlRequestContextConfig, pkp.mHost, pkp.mHashes, pkp.mI
ncludeSubdomains, |
127 pkp.mExpirationDate.getTime()); | 128 pkp.mExpirationDate.getTime()); |
128 } | 129 } |
129 return urlRequestContextConfig; | 130 return urlRequestContextConfig; |
130 } | 131 } |
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
215 } | 216 } |
216 } | 217 } |
217 | 218 |
218 // This method is intentionally non-static to ensure Cronet native library | 219 // This method is intentionally non-static to ensure Cronet native library |
219 // is loaded by class constructor. | 220 // is loaded by class constructor. |
220 @Override | 221 @Override |
221 public byte[] getGlobalMetricsDeltas() { | 222 public byte[] getGlobalMetricsDeltas() { |
222 return nativeGetHistogramDeltas(); | 223 return nativeGetHistogramDeltas(); |
223 } | 224 } |
224 | 225 |
| 226 /** |
| 227 * TODO(tbansal): http://crbug.com/618034 Remove this API once all |
| 228 * embedders have switched to using a request finished listener that |
| 229 * provides its own executor. |
| 230 */ |
| 231 @Override |
| 232 public void setRequestFinishedListenerExecutor(Executor executor) { |
| 233 if (!mNetworkQualityEstimatorEnabled) { |
| 234 throw new IllegalStateException("Network quality estimator not enabl
ed"); |
| 235 } |
| 236 if (executor == null) { |
| 237 throw new NullPointerException("Request finished listener requires a
n executor"); |
| 238 } |
| 239 if (mNetworkQualityExecutor != null) { |
| 240 throw new NullPointerException("Request finished listener executor a
lready set"); |
| 241 } |
| 242 mNetworkQualityExecutor = executor; |
| 243 } |
| 244 |
| 245 /** |
| 246 * TODO(tbansal): http://crbug.com/618034 Remove this API once all |
| 247 * embedders have switched to using CronetEngine builder for enabling |
| 248 * network quality estimator. |
| 249 */ |
225 @Override | 250 @Override |
226 public void enableNetworkQualityEstimator(Executor executor) { | 251 public void enableNetworkQualityEstimator(Executor executor) { |
227 enableNetworkQualityEstimatorForTesting(false, false, executor); | |
228 } | |
229 | |
230 @VisibleForTesting | |
231 @Override | |
232 void enableNetworkQualityEstimatorForTesting( | |
233 boolean useLocalHostRequests, boolean useSmallerResponses, Executor
executor) { | |
234 if (mNetworkQualityEstimatorEnabled) { | 252 if (mNetworkQualityEstimatorEnabled) { |
235 throw new IllegalStateException("Network quality estimator already e
nabled"); | 253 throw new IllegalStateException("Network quality estimator already e
nabled"); |
236 } | 254 } |
237 mNetworkQualityEstimatorEnabled = true; | 255 mNetworkQualityEstimatorEnabled = true; |
238 if (executor == null) { | 256 if (executor == null) { |
239 throw new NullPointerException("Network quality estimator requires a
n executor"); | 257 throw new NullPointerException("Network quality estimator requires a
n executor"); |
240 } | 258 } |
241 mNetworkQualityExecutor = executor; | 259 mNetworkQualityExecutor = executor; |
242 synchronized (mLock) { | 260 synchronized (mLock) { |
243 checkHaveAdapter(); | 261 checkHaveAdapter(); |
244 nativeEnableNetworkQualityEstimator( | 262 nativeEnableNetworkQualityEstimator(mUrlRequestContextAdapter); |
| 263 } |
| 264 } |
| 265 |
| 266 @VisibleForTesting |
| 267 @Override |
| 268 void configureNetworkQualityEstimatorForTesting( |
| 269 boolean useLocalHostRequests, boolean useSmallerResponses) { |
| 270 if (!mNetworkQualityEstimatorEnabled) { |
| 271 throw new IllegalStateException("Network quality estimator must be e
nabled"); |
| 272 } |
| 273 synchronized (mLock) { |
| 274 checkHaveAdapter(); |
| 275 nativeConfigureNetworkQualityEstimatorForTesting( |
245 mUrlRequestContextAdapter, useLocalHostRequests, useSmallerR
esponses); | 276 mUrlRequestContextAdapter, useLocalHostRequests, useSmallerR
esponses); |
246 } | 277 } |
247 } | 278 } |
248 | 279 |
249 @Override | 280 @Override |
250 public void addRttListener(NetworkQualityRttListener listener) { | 281 public void addRttListener(NetworkQualityRttListener listener) { |
251 if (!mNetworkQualityEstimatorEnabled) { | 282 if (!mNetworkQualityEstimatorEnabled) { |
252 throw new IllegalStateException("Network quality estimator must be e
nabled"); | 283 throw new IllegalStateException("Network quality estimator must be e
nabled"); |
253 } | 284 } |
| 285 if (mNetworkQualityExecutor == null && listener.getExecutor() == null) { |
| 286 throw new IllegalStateException("Executor must not be null"); |
| 287 } |
254 synchronized (mNetworkQualityLock) { | 288 synchronized (mNetworkQualityLock) { |
255 if (mRttListenerList.isEmpty()) { | 289 if (mRttListenerList.isEmpty()) { |
256 synchronized (mLock) { | 290 synchronized (mLock) { |
257 checkHaveAdapter(); | 291 checkHaveAdapter(); |
258 nativeProvideRTTObservations(mUrlRequestContextAdapter, true
); | 292 nativeProvideRTTObservations(mUrlRequestContextAdapter, true
); |
259 } | 293 } |
260 } | 294 } |
261 mRttListenerList.addObserver(listener); | 295 mRttListenerList.addObserver(listener); |
262 } | 296 } |
263 } | 297 } |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
303 mThroughputListenerList.removeObserver(listener); | 337 mThroughputListenerList.removeObserver(listener); |
304 if (mThroughputListenerList.isEmpty()) { | 338 if (mThroughputListenerList.isEmpty()) { |
305 synchronized (mLock) { | 339 synchronized (mLock) { |
306 checkHaveAdapter(); | 340 checkHaveAdapter(); |
307 nativeProvideThroughputObservations(mUrlRequestContextAdapte
r, false); | 341 nativeProvideThroughputObservations(mUrlRequestContextAdapte
r, false); |
308 } | 342 } |
309 } | 343 } |
310 } | 344 } |
311 } | 345 } |
312 | 346 |
| 347 /** |
| 348 * TODO(tbansal): http://crbug.com/618034 Remove this API once all |
| 349 * embedders have switched to using a request finished listener that |
| 350 * provides its own executor. |
| 351 */ |
313 @Override | 352 @Override |
314 public void addRequestFinishedListener(RequestFinishedListener listener) { | 353 public void addRequestFinishedListener(RequestFinishedListener listener) { |
315 if (!mNetworkQualityEstimatorEnabled) { | 354 if (!mNetworkQualityEstimatorEnabled) { |
316 throw new IllegalStateException("Network quality estimator must be e
nabled"); | 355 throw new IllegalStateException("Network quality estimator must be e
nabled"); |
317 } | 356 } |
| 357 // RequestFinishedListener does not provide its own executor. |
| 358 if (mNetworkQualityExecutor == null) { |
| 359 throw new IllegalStateException("Executor must not be null"); |
| 360 } |
318 synchronized (mNetworkQualityLock) { | 361 synchronized (mNetworkQualityLock) { |
319 mFinishedListenerList.addObserver(listener); | 362 mFinishedListenerList.addObserver(listener); |
320 } | 363 } |
321 } | 364 } |
322 | 365 |
| 366 /** |
| 367 * TODO(tbansal): http://crbug.com/618034 Remove this API. |
| 368 */ |
323 @Override | 369 @Override |
324 public void removeRequestFinishedListener(RequestFinishedListener listener)
{ | 370 public void removeRequestFinishedListener(RequestFinishedListener listener)
{ |
325 if (!mNetworkQualityEstimatorEnabled) { | 371 if (!mNetworkQualityEstimatorEnabled) { |
326 throw new IllegalStateException("Network quality estimator must be e
nabled"); | 372 throw new IllegalStateException("Network quality estimator must be e
nabled"); |
327 } | 373 } |
328 synchronized (mNetworkQualityLock) { | 374 synchronized (mNetworkQualityLock) { |
329 mFinishedListenerList.removeObserver(listener); | 375 mFinishedListenerList.removeObserver(listener); |
330 } | 376 } |
331 } | 377 } |
332 | 378 |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
409 mNetworkThread = Thread.currentThread(); | 455 mNetworkThread = Thread.currentThread(); |
410 mInitCompleted.open(); | 456 mInitCompleted.open(); |
411 } | 457 } |
412 Thread.currentThread().setName("ChromiumNet"); | 458 Thread.currentThread().setName("ChromiumNet"); |
413 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); | 459 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); |
414 } | 460 } |
415 | 461 |
416 @SuppressWarnings("unused") | 462 @SuppressWarnings("unused") |
417 @CalledByNative | 463 @CalledByNative |
418 private void onRttObservation(final int rttMs, final long whenMs, final int
source) { | 464 private void onRttObservation(final int rttMs, final long whenMs, final int
source) { |
419 Runnable task = new Runnable() { | 465 synchronized (mNetworkQualityLock) { |
420 @Override | 466 for (final NetworkQualityRttListener listener : mRttListenerList) { |
421 public void run() { | 467 Runnable task = new Runnable() { |
422 synchronized (mNetworkQualityLock) { | 468 @Override |
423 for (NetworkQualityRttListener listener : mRttListenerList)
{ | 469 public void run() { |
424 listener.onRttObservation(rttMs, whenMs, source); | 470 listener.onRttObservation(rttMs, whenMs, source); |
425 } | 471 } |
426 } | 472 }; |
| 473 // Use the executor provided by the listener. If not available, |
| 474 // use the network quality executor. |
| 475 // TODO(tbansal): Change this to always use the executor |
| 476 // provided by the listener, once all embedders start providing |
| 477 // a non-null executor. |
| 478 Executor executor = listener.getExecutor() != null ? listener.ge
tExecutor() |
| 479 : mNetworkQua
lityExecutor; |
| 480 postObservationTaskToExecutor(executor, task); |
427 } | 481 } |
428 }; | 482 } |
429 postObservationTaskToNetworkQualityExecutor(task); | |
430 } | 483 } |
431 | 484 |
432 @SuppressWarnings("unused") | 485 @SuppressWarnings("unused") |
433 @CalledByNative | 486 @CalledByNative |
434 private void onThroughputObservation( | 487 private void onThroughputObservation( |
435 final int throughputKbps, final long whenMs, final int source) { | 488 final int throughputKbps, final long whenMs, final int source) { |
| 489 synchronized (mNetworkQualityLock) { |
| 490 for (final NetworkQualityThroughputListener listener : mThroughputLi
stenerList) { |
| 491 Runnable task = new Runnable() { |
| 492 @Override |
| 493 public void run() { |
| 494 listener.onThroughputObservation(throughputKbps, whenMs,
source); |
| 495 } |
| 496 }; |
| 497 postObservationTaskToExecutor(listener.getExecutor(), task); |
| 498 } |
| 499 } |
| 500 } |
| 501 |
| 502 void reportFinished(final CronetUrlRequest request) { |
| 503 if (!mNetworkQualityEstimatorEnabled) { |
| 504 return; |
| 505 } |
| 506 // If no request finished listener has been added, then mNetworkQualityE
xecutor may be |
| 507 // null. Exit early to avoid posting to a null executor. |
| 508 synchronized (mNetworkQualityLock) { |
| 509 if (mFinishedListenerList.isEmpty()) { |
| 510 return; |
| 511 } |
| 512 } |
436 Runnable task = new Runnable() { | 513 Runnable task = new Runnable() { |
437 @Override | 514 @Override |
438 public void run() { | 515 public void run() { |
439 synchronized (mNetworkQualityLock) { | 516 synchronized (mNetworkQualityLock) { |
440 for (NetworkQualityThroughputListener listener : mThroughput
ListenerList) { | 517 UrlRequestInfo requestInfo = request.getRequestInfo(); |
441 listener.onThroughputObservation(throughputKbps, whenMs,
source); | 518 for (RequestFinishedListener listener : mFinishedListenerLis
t) { |
| 519 listener.onRequestFinished(requestInfo); |
442 } | 520 } |
443 } | 521 } |
444 } | 522 } |
445 }; | 523 }; |
446 postObservationTaskToNetworkQualityExecutor(task); | 524 // Use {@link mNetworkQualityExecutor} since |
| 525 // RequestFInishedListeners do not provide an executor. |
| 526 postObservationTaskToExecutor(mNetworkQualityExecutor, task); |
447 } | 527 } |
448 | 528 |
449 void reportFinished(final CronetUrlRequest request) { | 529 private static void postObservationTaskToExecutor(Executor executor, Runnabl
e task) { |
450 if (mNetworkQualityEstimatorEnabled) { | |
451 Runnable task = new Runnable() { | |
452 @Override | |
453 public void run() { | |
454 synchronized (mNetworkQualityLock) { | |
455 UrlRequestInfo requestInfo = request.getRequestInfo(); | |
456 for (RequestFinishedListener listener : mFinishedListene
rList) { | |
457 listener.onRequestFinished(requestInfo); | |
458 } | |
459 } | |
460 } | |
461 }; | |
462 postObservationTaskToNetworkQualityExecutor(task); | |
463 } | |
464 } | |
465 | |
466 void postObservationTaskToNetworkQualityExecutor(Runnable task) { | |
467 try { | 530 try { |
468 mNetworkQualityExecutor.execute(task); | 531 executor.execute(task); |
469 } catch (RejectedExecutionException failException) { | 532 } catch (RejectedExecutionException failException) { |
470 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to ex
ecutor", | 533 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to ex
ecutor", |
471 failException); | 534 failException); |
472 } | 535 } |
473 } | 536 } |
474 | 537 |
475 // Native methods are implemented in cronet_url_request_context_adapter.cc. | 538 // Native methods are implemented in cronet_url_request_context_adapter.cc. |
476 private static native long nativeCreateRequestContextConfig(String userAgent
, | 539 private static native long nativeCreateRequestContextConfig(String userAgent
, |
477 String storagePath, boolean quicEnabled, String quicUserAgentId, boo
lean http2Enabled, | 540 String storagePath, boolean quicEnabled, String quicUserAgentId, boo
lean http2Enabled, |
478 boolean sdchEnabled, String dataReductionProxyKey, | 541 boolean sdchEnabled, String dataReductionProxyKey, |
479 String dataReductionProxyPrimaryProxy, String dataReductionProxyFall
backProxy, | 542 String dataReductionProxyPrimaryProxy, String dataReductionProxyFall
backProxy, |
480 String dataReductionProxySecureProxyCheckUrl, boolean disableCache,
int httpCacheMode, | 543 String dataReductionProxySecureProxyCheckUrl, boolean disableCache,
int httpCacheMode, |
481 long httpCacheMaxSize, String experimentalOptions, long mockCertVeri
fier); | 544 long httpCacheMaxSize, String experimentalOptions, long mockCertVeri
fier, |
| 545 boolean enableNetworkQualityEstimator); |
482 | 546 |
483 private static native void nativeAddQuicHint( | 547 private static native void nativeAddQuicHint( |
484 long urlRequestContextConfig, String host, int port, int alternatePo
rt); | 548 long urlRequestContextConfig, String host, int port, int alternatePo
rt); |
485 | 549 |
486 private static native void nativeAddPkp(long urlRequestContextConfig, String
host, | 550 private static native void nativeAddPkp(long urlRequestContextConfig, String
host, |
487 byte[][] hashes, boolean includeSubdomains, long expirationTime); | 551 byte[][] hashes, boolean includeSubdomains, long expirationTime); |
488 | 552 |
489 private static native long nativeCreateRequestContextAdapter(long urlRequest
ContextConfig); | 553 private static native long nativeCreateRequestContextAdapter(long urlRequest
ContextConfig); |
490 | 554 |
491 private static native int nativeSetMinLogLevel(int loggingLevel); | 555 private static native int nativeSetMinLogLevel(int loggingLevel); |
492 | 556 |
493 private static native byte[] nativeGetHistogramDeltas(); | 557 private static native byte[] nativeGetHistogramDeltas(); |
494 | 558 |
495 @NativeClassQualifiedName("CronetURLRequestContextAdapter") | 559 @NativeClassQualifiedName("CronetURLRequestContextAdapter") |
496 private native void nativeDestroy(long nativePtr); | 560 private native void nativeDestroy(long nativePtr); |
497 | 561 |
498 @NativeClassQualifiedName("CronetURLRequestContextAdapter") | 562 @NativeClassQualifiedName("CronetURLRequestContextAdapter") |
499 private native void nativeStartNetLogToFile(long nativePtr, | 563 private native void nativeStartNetLogToFile(long nativePtr, |
500 String fileName, boolean logAll); | 564 String fileName, boolean logAll); |
501 | 565 |
502 @NativeClassQualifiedName("CronetURLRequestContextAdapter") | 566 @NativeClassQualifiedName("CronetURLRequestContextAdapter") |
503 private native void nativeStopNetLog(long nativePtr); | 567 private native void nativeStopNetLog(long nativePtr); |
504 | 568 |
505 @NativeClassQualifiedName("CronetURLRequestContextAdapter") | 569 @NativeClassQualifiedName("CronetURLRequestContextAdapter") |
506 private native void nativeInitRequestContextOnMainThread(long nativePtr); | 570 private native void nativeInitRequestContextOnMainThread(long nativePtr); |
507 | 571 |
508 @NativeClassQualifiedName("CronetURLRequestContextAdapter") | 572 @NativeClassQualifiedName("CronetURLRequestContextAdapter") |
509 private native void nativeEnableNetworkQualityEstimator( | 573 private native void nativeConfigureNetworkQualityEstimatorForTesting( |
510 long nativePtr, boolean useLocalHostRequests, boolean useSmallerResp
onses); | 574 long nativePtr, boolean useLocalHostRequests, boolean useSmallerResp
onses); |
511 | 575 |
512 @NativeClassQualifiedName("CronetURLRequestContextAdapter") | 576 @NativeClassQualifiedName("CronetURLRequestContextAdapter") |
| 577 private native void nativeEnableNetworkQualityEstimator(long nativePtr); |
| 578 |
| 579 @NativeClassQualifiedName("CronetURLRequestContextAdapter") |
513 private native void nativeProvideRTTObservations(long nativePtr, boolean sho
uld); | 580 private native void nativeProvideRTTObservations(long nativePtr, boolean sho
uld); |
514 | 581 |
515 @NativeClassQualifiedName("CronetURLRequestContextAdapter") | 582 @NativeClassQualifiedName("CronetURLRequestContextAdapter") |
516 private native void nativeProvideThroughputObservations(long nativePtr, bool
ean should); | 583 private native void nativeProvideThroughputObservations(long nativePtr, bool
ean should); |
517 } | 584 } |
OLD | NEW |