1 // Copyright 2017 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. 4 5 package org.chromium.chrome.browser.contextualsearch; 6 7 import android.text.TextUtils; 8 9 import androidx.annotation.NonNull; 10 import androidx.annotation.Nullable; 11 import androidx.annotation.VisibleForTesting; 12 13 import org.chromium.base.annotations.CalledByNative; 14 import org.chromium.base.annotations.NativeMethods; 15 16 /** 17 * Provides a context in which to search, and links to the native ContextualSearchContext. 18 * Includes the selection, selection offsets, surrounding page content, etc. 19 * Requires an override of #onSelectionChanged to call when a non-empty selection is established 20 * or changed. 21 */ 22 public abstract class ContextualSearchContext { 23 private static final String TAG = "TTS Context"; 24 static final int INVALID_OFFSET = -1; 25 26 // Non-visible word-break marker. 27 private static final int SOFT_HYPHEN_CHAR = '\u00AD'; 28 29 // Pointer to the native instance of this class. 30 private long mNativePointer; 31 32 // Whether this context has had the required properties set so it can Resolve a Search Term. 33 private boolean mHasSetResolveProperties; 34 35 // A shortened version of the actual text content surrounding the selection, or null if not yet 36 // established. 37 private String mSurroundingText; 38 39 // The start and end offsets of the selection within the text content. 40 private int mSelectionStartOffset = INVALID_OFFSET; 41 private int mSelectionEndOffset = INVALID_OFFSET; 42 43 // The home country, or an empty string if not set. 44 @NonNull 45 private String mHomeCountry = ""; 46 47 // The detected language of the context, or {@code null} if not yet detected, and empty if 48 // it cannot be reliably determined. 49 private String mDetectedLanguage; 50 51 // The offset of an initial Tap gesture within the text content. 52 private int mTapOffset = INVALID_OFFSET; 53 54 // The initial word selected by a Tap, or null. 55 private String mInitialSelectedWord; 56 57 // The original encoding of the base page. 58 private String mEncoding; 59 60 // The word that was tapped, as analyzed internally before selection takes place, 61 // or {@code null} if no analysis has been done yet. 62 private String mWordTapped; 63 64 // The offset of the tapped word within the surrounding text or {@code INVALID_OFFSET} if not 65 // yet analyzed. 66 private int mWordTappedStartOffset = INVALID_OFFSET; 67 68 // The offset of the tap within the tapped word, or {@code INVALID_OFFSET} if not yet analyzed. 69 private int mTapWithinWordOffset = INVALID_OFFSET; 70 71 // The words before and after the tapped word, and their offsets. 72 private String mWordPreviousToTap; 73 private int mWordPreviousToTapOffset = INVALID_OFFSET; 74 private String mWordFollowingTap; 75 private int mWordFollowingTapOffset = INVALID_OFFSET; 76 77 // Data about the previous user interactions and the event-ID from the server that will log it. 78 private long mPreviousEventId; 79 private int mPreviousUserInteractions; 80 81 // Translation members. 82 @NonNull 83 private String mTargetLanguage = ""; 84 @NonNull 85 private String mFluentLanguages = ""; 86 87 // Whether the Related Searches functionality should also be activated. 88 private boolean mDoRelatedSearches; 89 90 /** A {@link ContextualSearchContext} that ignores changes to the selection. */ 91 static class ChangeIgnoringContext extends ContextualSearchContext { 92 @Override onSelectionChanged()93 void onSelectionChanged() {} 94 } 95 96 /** 97 * Constructs a context that tracks the selection and some amount of page content. 98 */ ContextualSearchContext()99 ContextualSearchContext() { 100 mNativePointer = ContextualSearchContextJni.get().init(this); 101 mHasSetResolveProperties = false; 102 } 103 104 /** 105 * Updates a context to be able to resolve a search term and have a large amount of 106 * page content. 107 * @param homeCountry The country where the user usually resides, or an empty string if not 108 * known. 109 * @param doSendBasePageUrl Whether the base-page URL should be sent to the server. 110 * @param previousEventId An EventID from the server to send along with the resolve request. 111 * @param previousUserInteractions Persisted interaction outcomes to send along with the resolve 112 * request. 113 * @param targetLanguage The language to translate into, in case translation might be needed. 114 * @param fluentLanguages An ordered comma-separated list of ISO 639 language codes that 115 * the user can read fluently, or an empty string. 116 */ setResolveProperties(@onNull String homeCountry, boolean doSendBasePageUrl, long previousEventId, int previousUserInteractions, @NonNull String targetLanguage, @NonNull String fluentLanguages)117 void setResolveProperties(@NonNull String homeCountry, boolean doSendBasePageUrl, 118 long previousEventId, int previousUserInteractions, @NonNull String targetLanguage, 119 @NonNull String fluentLanguages) { 120 // TODO(donnd): consider making this a constructor variation. 121 mHasSetResolveProperties = true; 122 mHomeCountry = homeCountry; 123 mPreviousEventId = previousEventId; 124 mPreviousUserInteractions = previousUserInteractions; 125 ContextualSearchContextJni.get().setResolveProperties(getNativePointer(), this, homeCountry, 126 doSendBasePageUrl, previousEventId, previousUserInteractions); 127 mTargetLanguage = targetLanguage; 128 mFluentLanguages = fluentLanguages; 129 } 130 131 /** 132 * This method should be called to clean up storage when an instance of this class is 133 * no longer in use. The ContextualSearchContextJni.get().destroy will call the destructor on 134 * the native instance. 135 */ destroy()136 void destroy() { 137 assert mNativePointer != 0; 138 ContextualSearchContextJni.get().destroy(mNativePointer, this); 139 mNativePointer = 0; 140 141 // Also zero out private data that may be sizable. 142 mSurroundingText = null; 143 } 144 145 /** 146 * Sets the surrounding text and selection offsets. 147 * @param encoding The original encoding of the base page. 148 * @param surroundingText The text from the base page surrounding the selection. 149 * @param startOffset The offset of start the selection. 150 * @param endOffset The offset of the end of the selection 151 */ setSurroundingText( String encoding, String surroundingText, int startOffset, int endOffset)152 void setSurroundingText( 153 String encoding, String surroundingText, int startOffset, int endOffset) { 154 setSurroundingText(encoding, surroundingText, startOffset, endOffset, false); 155 } 156 157 /** 158 * Sets the surrounding text and selection offsets assuming UTF-8 and no insertion-point 159 * support. 160 * @param surroundingText The text from the base page surrounding the selection. 161 * @param startOffset The offset of start the selection. 162 * @param endOffset The offset of the end of the selection 163 */ 164 @VisibleForTesting setSurroundingText(String surroundingText, int startOffset, int endOffset)165 void setSurroundingText(String surroundingText, int startOffset, int endOffset) { 166 setSurroundingText("UTF-8", surroundingText, startOffset, endOffset, false); 167 } 168 169 /** 170 * Sets the surrounding text and selection offsets. 171 * @param encoding The original encoding of the base page. 172 * @param surroundingText The text from the base page surrounding the selection. 173 * @param startOffset The offset of start the selection. 174 * @param endOffset The offset of the end of the selection. 175 * @param setNative Whether to set the native context too by passing it through JNI. 176 */ 177 @VisibleForTesting setSurroundingText(String encoding, String surroundingText, int startOffset, int endOffset, boolean setNative)178 void setSurroundingText(String encoding, String surroundingText, int startOffset, int endOffset, 179 boolean setNative) { 180 assert startOffset <= endOffset; 181 mEncoding = encoding; 182 mSurroundingText = surroundingText; 183 mSelectionStartOffset = startOffset; 184 mSelectionEndOffset = endOffset; 185 if (startOffset == endOffset && startOffset <= surroundingText.length() 186 && !hasAnalyzedTap()) { 187 analyzeTap(startOffset); 188 } 189 // Notify of an initial selection if it's not empty. 190 if (endOffset > startOffset) { 191 updateInitialSelectedWord(); 192 onSelectionChanged(); 193 } 194 if (setNative) { 195 ContextualSearchContextJni.get().setContent(getNativePointer(), this, mSurroundingText, 196 mSelectionStartOffset, mSelectionEndOffset); 197 } 198 // Detect the language of the surroundings or the selection. 199 setTranslationLanguages(getDetectedLanguage(), mTargetLanguage, mFluentLanguages); 200 } 201 202 /** 203 * @return The text that surrounds the selection, or {@code null} if none yet known. 204 */ 205 @Nullable getSurroundingText()206 String getSurroundingText() { 207 return mSurroundingText; 208 } 209 210 /** 211 * @return The offset into the surrounding text of the start of the selection, or 212 * {@link #INVALID_OFFSET} if not yet established. 213 */ getSelectionStartOffset()214 int getSelectionStartOffset() { 215 return mSelectionStartOffset; 216 } 217 218 /** 219 * @return The offset into the surrounding text of the end of the selection, or 220 * {@link #INVALID_OFFSET} if not yet established. 221 */ getSelectionEndOffset()222 int getSelectionEndOffset() { 223 return mSelectionEndOffset; 224 } 225 226 /** 227 * @return The original encoding of the base page. 228 */ getEncoding()229 String getEncoding() { 230 return mEncoding; 231 } 232 233 /** 234 * @return The home country, or an empty string if none set. 235 */ getHomeCountry()236 String getHomeCountry() { 237 return mHomeCountry; 238 } 239 240 /** 241 * @return The initial word selected by a Tap. 242 */ getInitialSelectedWord()243 String getInitialSelectedWord() { 244 return mInitialSelectedWord; 245 } 246 247 /** 248 * @param word The initial word selected. 249 */ setInitialSelectedWord(String word)250 private void setInitialSelectedWord(String word) { 251 mInitialSelectedWord = word; 252 } 253 254 /** 255 * @return The text content that follows the selection (one side of the surrounding text). 256 */ getTextContentFollowingSelection()257 String getTextContentFollowingSelection() { 258 if (mSurroundingText != null && mSelectionEndOffset > 0 259 && mSelectionEndOffset <= mSurroundingText.length()) { 260 return mSurroundingText.substring(mSelectionEndOffset); 261 } else { 262 return ""; 263 } 264 } 265 266 /** 267 * @return Whether this context can Resolve the Search Term. 268 */ canResolve()269 boolean canResolve() { 270 return mHasSetResolveProperties && hasValidSelection(); 271 } 272 273 /** 274 * Prepares the Context to be used in a Resolve request by supplying last minute parameters. 275 * If this call is not made before a Resolve then defaults are used (not exact and not a 276 * Related Search). 277 * @param isExactSearch Specifies whether this search must be exact -- meaning the resolve must 278 * return a non-expanding result that matches the selection exactly. 279 * @param relatedSearchesStamp Information to be attached to the Resolve request that is needed 280 * for Related Searches. If this string is empty then no Related Searches results will 281 * be requested. 282 */ prepareToResolve(boolean isExactSearch, String relatedSearchesStamp)283 void prepareToResolve(boolean isExactSearch, String relatedSearchesStamp) { 284 ContextualSearchContextJni.get().prepareToResolve( 285 mNativePointer, this, isExactSearch, relatedSearchesStamp); 286 } 287 288 /** 289 * Notifies of an adjustment that has been applied to the start and end of the selection. 290 * @param startAdjust A signed value indicating the direction of the adjustment to the start of 291 * the selection (typically a negative value when the selection expands). 292 * @param endAdjust A signed value indicating the direction of the adjustment to the end of 293 * the selection (typically a positive value when the selection expands). 294 */ onSelectionAdjusted(int startAdjust, int endAdjust)295 void onSelectionAdjusted(int startAdjust, int endAdjust) { 296 // Fully track the selection as it changes. 297 mSelectionStartOffset += startAdjust; 298 mSelectionEndOffset += endAdjust; 299 updateInitialSelectedWord(); 300 ContextualSearchContextJni.get().adjustSelection( 301 getNativePointer(), this, startAdjust, endAdjust); 302 // Notify of changes. 303 onSelectionChanged(); 304 } 305 306 /** Updates the initial selected word if it has not yet been set. */ updateInitialSelectedWord()307 private void updateInitialSelectedWord() { 308 if (TextUtils.isEmpty(mInitialSelectedWord) && !TextUtils.isEmpty(mSurroundingText)) { 309 // TODO(donnd): investigate the root cause of crbug.com/725027 that requires this 310 // additional validation to prevent this substring call from crashing! 311 if (mSelectionEndOffset < mSelectionStartOffset || mSelectionStartOffset < 0 312 || mSelectionEndOffset > mSurroundingText.length()) { 313 return; 314 } 315 mInitialSelectedWord = 316 mSurroundingText.substring(mSelectionStartOffset, mSelectionEndOffset); 317 } 318 } 319 320 /** @return the current selection, or an empty string if data is invalid or nothing selected. */ getSelection()321 String getSelection() { 322 if (TextUtils.isEmpty(mSurroundingText) || mSelectionEndOffset < mSelectionStartOffset 323 || mSelectionStartOffset < 0 || mSelectionEndOffset > mSurroundingText.length()) { 324 return ""; 325 } 326 return mSurroundingText.substring(mSelectionStartOffset, mSelectionEndOffset); 327 } 328 329 /** 330 * Notifies this instance that the selection has been changed. 331 */ onSelectionChanged()332 abstract void onSelectionChanged(); 333 334 /** 335 * Gets the language of the current context's content by calling the native CLD3 detector if 336 * needed. 337 * @return An ISO 639 language code string, or an empty string if the language cannot be 338 * reliably determined. 339 */ 340 @NonNull getDetectedLanguage()341 String getDetectedLanguage() { 342 assert mSurroundingText != null; 343 if (mDetectedLanguage == null) { 344 mDetectedLanguage = 345 ContextualSearchContextJni.get().detectLanguage(mNativePointer, this); 346 } 347 return mDetectedLanguage; 348 } 349 350 /** 351 * Pushes the given language down to the native ContextualSearchContext. 352 * @param detectedLanguage An ISO 639 language code string for the language to translate from. 353 * @param targetLanguage An ISO 639 language code string to translation into. 354 * @param fluentLanguages An ordered comma-separated list of ISO 639 language codes that 355 * the user can read fluently, or an empty string. 356 */ 357 @VisibleForTesting setTranslationLanguages(@onNull String detectedLanguage, @NonNull String targetLanguage, @NonNull String fluentLanguages)358 void setTranslationLanguages(@NonNull String detectedLanguage, @NonNull String targetLanguage, 359 @NonNull String fluentLanguages) { 360 // Set redundant languages to empty strings. 361 fluentLanguages = targetLanguage.equals(fluentLanguages) ? "" : fluentLanguages; 362 if (targetLanguage.equals(detectedLanguage)) { 363 detectedLanguage = ""; 364 targetLanguage = ""; 365 } 366 ContextualSearchContextJni.get().setTranslationLanguages( 367 mNativePointer, this, detectedLanguage, targetLanguage, fluentLanguages); 368 } 369 370 // ============================================================================================ 371 // Content Analysis. 372 // ============================================================================================ 373 374 /** 375 * @return Whether this context has valid Surrounding text and initial Tap offset. 376 */ 377 @VisibleForTesting hasValidTappedText()378 boolean hasValidTappedText() { 379 return !TextUtils.isEmpty(mSurroundingText) && mTapOffset >= 0 380 && mTapOffset <= mSurroundingText.length(); 381 } 382 383 /** 384 * @return Whether this context has a valid selection, which may be an insertion point. 385 */ 386 @VisibleForTesting hasValidSelection()387 boolean hasValidSelection() { 388 return !TextUtils.isEmpty(mSurroundingText) && mSelectionStartOffset != INVALID_OFFSET 389 && mSelectionEndOffset != INVALID_OFFSET 390 && mSelectionStartOffset < mSelectionEndOffset 391 && mSelectionEndOffset < mSurroundingText.length(); 392 } 393 394 /** 395 * @return Whether a Tap gesture has occurred and been analyzed. 396 */ 397 @VisibleForTesting hasAnalyzedTap()398 boolean hasAnalyzedTap() { 399 return mTapOffset >= 0; 400 } 401 402 /** 403 * @return The word tapped, or {@code null} if the word that was tapped cannot be identified by 404 * the current limited parsing capability. 405 * @see #analyzeTap(int) 406 */ getWordTapped()407 String getWordTapped() { 408 return mWordTapped; 409 } 410 411 /** 412 * @return The offset of the start of the tapped word, or {@code INVALID_OFFSET} if the tapped 413 * word cannot be identified by the current parsing capability. 414 * @see #analyzeTap(int) 415 */ getWordTappedOffset()416 int getWordTappedOffset() { 417 return mWordTappedStartOffset; 418 } 419 420 /** 421 * @return The offset of the tap within the tapped word, or {@code INVALID_OFFSET} if the tapped 422 * word cannot be identified by the current parsing capability. 423 * @see #analyzeTap(int) 424 */ getTapOffsetWithinTappedWord()425 int getTapOffsetWithinTappedWord() { 426 return mTapWithinWordOffset; 427 } 428 429 /** 430 * @return The word previous to the word that was tapped, or {@code null} if not available. 431 */ getWordPreviousToTap()432 String getWordPreviousToTap() { 433 return mWordPreviousToTap; 434 } 435 436 /** 437 * @return The offset of the first character of the word previous to the word that was tapped, 438 * or {@code INVALID_OFFSET} if not available. 439 */ getWordPreviousToTapOffset()440 int getWordPreviousToTapOffset() { 441 return mWordPreviousToTapOffset; 442 } 443 444 /** 445 * @return The word following the word that was tapped, or {@code null} if not available. 446 */ getWordFollowingTap()447 String getWordFollowingTap() { 448 return mWordFollowingTap; 449 } 450 451 /** 452 * @return The offset of the first character of the word following the word that was tapped, 453 * or {@code INVALID_OFFSET} if not available. 454 */ getWordFollowingTapOffset()455 int getWordFollowingTapOffset() { 456 return mWordFollowingTapOffset; 457 } 458 459 /** 460 * Finds the words around the initial Tap offset by expanding and looking for word-breaks. 461 * This mimics the Blink word-segmentation invoked by SelectWordAroundCaret and similar 462 * selection logic, but is only appropriate for limited use. Does not work on ideographic 463 * languages and possibly many other cases. Should only be used only for ML signal evaluation. 464 * @param tapOffset The offset of the Tap within the surrounding text. 465 */ analyzeTap(int tapOffset)466 private void analyzeTap(int tapOffset) { 467 mTapOffset = tapOffset; 468 mWordTapped = null; 469 mTapWithinWordOffset = INVALID_OFFSET; 470 471 assert hasValidTappedText(); 472 473 int wordStartOffset = findWordStartOffset(mTapOffset); 474 int wordEndOffset = findWordEndOffset(mTapOffset); 475 if (wordStartOffset == INVALID_OFFSET || wordEndOffset == INVALID_OFFSET) return; 476 477 mWordTappedStartOffset = wordStartOffset; 478 mWordTapped = mSurroundingText.substring(wordStartOffset, wordEndOffset); 479 mTapWithinWordOffset = mTapOffset - wordStartOffset; 480 481 findPreviousWord(); 482 findFollowingWord(); 483 } 484 485 /** 486 * Finds the word previous to the word tapped. 487 */ findPreviousWord()488 private void findPreviousWord() { 489 // Scan past word-break characters preceding the tapped word. 490 int previousWordEndOffset = mWordTappedStartOffset; 491 while (previousWordEndOffset >= 1 && isWordBreakAtIndex(previousWordEndOffset - 1)) { 492 --previousWordEndOffset; 493 } 494 if (previousWordEndOffset == 0) return; 495 496 mWordPreviousToTapOffset = findWordStartOffset(previousWordEndOffset); 497 if (mWordPreviousToTapOffset == INVALID_OFFSET) return; 498 499 mWordPreviousToTap = 500 mSurroundingText.substring(mWordPreviousToTapOffset, previousWordEndOffset); 501 } 502 503 /** 504 * Finds the word following the word tapped. 505 */ findFollowingWord()506 private void findFollowingWord() { 507 int tappedWordOffset = getWordTappedOffset(); 508 int followingWordStartOffset = tappedWordOffset + mWordTapped.length() + 1; 509 while (followingWordStartOffset < mSurroundingText.length() 510 && isWordBreakAtIndex(followingWordStartOffset)) { 511 ++followingWordStartOffset; 512 } 513 if (followingWordStartOffset == mSurroundingText.length()) return; 514 515 int wordFollowingTapEndOffset = findWordEndOffset(followingWordStartOffset); 516 if (wordFollowingTapEndOffset == INVALID_OFFSET) return; 517 518 mWordFollowingTapOffset = followingWordStartOffset; 519 mWordFollowingTap = 520 mSurroundingText.substring(mWordFollowingTapOffset, wordFollowingTapEndOffset); 521 } 522 523 /** 524 * @return The start of the word that contains the given initial offset, within the surrounding 525 * text, or {@code INVALID_OFFSET} if not found. 526 */ findWordStartOffset(int initial)527 private int findWordStartOffset(int initial) { 528 // Scan before, aborting if we hit any ideographic letter. 529 for (int offset = initial - 1; offset >= 0; offset--) { 530 if (isWordBreakAtIndex(offset)) { 531 // The start of the word is after this word break. 532 return offset + 1; 533 } 534 } 535 536 return INVALID_OFFSET; 537 } 538 539 /** 540 * Finds the offset of the end of the word that includes the given initial offset. 541 * NOTE: this is the index of the character just past the last character of the word, 542 * so a 3 character word "who" has start index 0 and end index 3. 543 * The character at the initial offset is examined and each one after that too until a non-word 544 * character is encountered, and that offset will be returned. 545 * @param initial The initial offset to scan from. 546 * @return The end of the word that contains the given initial offset, within the surrounding 547 * text. 548 */ findWordEndOffset(int initial)549 private int findWordEndOffset(int initial) { 550 // Scan after, aborting if we hit any CJKV letter. 551 for (int offset = initial; offset < mSurroundingText.length(); offset++) { 552 if (isWordBreakAtIndex(offset)) { 553 // The end of the word is the offset of this word break. 554 return offset; 555 } 556 } 557 return INVALID_OFFSET; 558 } 559 560 /** 561 * @return Whether the character at the given index is a word-break. 562 */ isWordBreakAtIndex(int index)563 private boolean isWordBreakAtIndex(int index) { 564 return !Character.isLetterOrDigit(mSurroundingText.charAt(index)) 565 && mSurroundingText.charAt(index) != SOFT_HYPHEN_CHAR; 566 } 567 568 // ============================================================================================ 569 // Test support. 570 // ============================================================================================ 571 572 @VisibleForTesting getPreviousUserInteractions()573 int getPreviousUserInteractions() { 574 return mPreviousUserInteractions; 575 } 576 577 @VisibleForTesting getPreviousEventId()578 long getPreviousEventId() { 579 return mPreviousEventId; 580 } 581 582 // ============================================================================================ 583 // Native callback support. 584 // ============================================================================================ 585 586 @CalledByNative getNativePointer()587 private long getNativePointer() { 588 assert mNativePointer != 0; 589 return mNativePointer; 590 } 591 592 @NativeMethods 593 interface Natives { init(ContextualSearchContext caller)594 long init(ContextualSearchContext caller); destroy(long nativeContextualSearchContext, ContextualSearchContext caller)595 void destroy(long nativeContextualSearchContext, ContextualSearchContext caller); setResolveProperties(long nativeContextualSearchContext, ContextualSearchContext caller, String homeCountry, boolean doSendBasePageUrl, long previousEventId, int previousEventResults)596 void setResolveProperties(long nativeContextualSearchContext, 597 ContextualSearchContext caller, String homeCountry, boolean doSendBasePageUrl, 598 long previousEventId, int previousEventResults); adjustSelection(long nativeContextualSearchContext, ContextualSearchContext caller, int startAdjust, int endAdjust)599 void adjustSelection(long nativeContextualSearchContext, ContextualSearchContext caller, 600 int startAdjust, int endAdjust); setContent(long nativeContextualSearchContext, ContextualSearchContext caller, String content, int selectionStart, int selectionEnd)601 void setContent(long nativeContextualSearchContext, ContextualSearchContext caller, 602 String content, int selectionStart, int selectionEnd); detectLanguage(long nativeContextualSearchContext, ContextualSearchContext caller)603 String detectLanguage(long nativeContextualSearchContext, ContextualSearchContext caller); setTranslationLanguages(long nativeContextualSearchContext, ContextualSearchContext caller, String detectedLanguage, String targetLanguage, String fluentLanguages)604 void setTranslationLanguages(long nativeContextualSearchContext, 605 ContextualSearchContext caller, String detectedLanguage, String targetLanguage, 606 String fluentLanguages); prepareToResolve(long nativeContextualSearchContext, ContextualSearchContext caller, boolean isExactSearch, String relatedSearchesStamp)607 void prepareToResolve(long nativeContextualSearchContext, ContextualSearchContext caller, 608 boolean isExactSearch, String relatedSearchesStamp); 609 } 610 } 611