[Omnibox] [Switch To Tab] Enable Native to search through Java tabs

This CL is the first step to implement Switch To Tab. Implement native
side can search tabs on java side, and return if hasTabMatch to java.


Bug:1085220

Change-Id: I595488e19ae4955bdbb3032bb213200b00faa414
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2200252
Reviewed-by: Ted Choc <[email protected]>
Reviewed-by: Ender <[email protected]>
Commit-Queue: Gang Wu <[email protected]>
Cr-Commit-Position: refs/heads/master@{#778072}
diff --git a/chrome/android/chrome_test_java_sources.gni b/chrome/android/chrome_test_java_sources.gni
index b09cc04..fd3ac4f 100644
--- a/chrome/android/chrome_test_java_sources.gni
+++ b/chrome/android/chrome_test_java_sources.gni
@@ -307,6 +307,7 @@
   "javatests/src/org/chromium/chrome/browser/omnibox/geo/GeolocationHeaderTest.java",
   "javatests/src/org/chromium/chrome/browser/omnibox/status/StatusViewRenderTest.java",
   "javatests/src/org/chromium/chrome/browser/omnibox/status/StatusViewTest.java",
+  "javatests/src/org/chromium/chrome/browser/omnibox/suggestions/SwitchToTabTest.java",
   "javatests/src/org/chromium/chrome/browser/omnibox/suggestions/VoiceSuggestionProviderTest.java",
   "javatests/src/org/chromium/chrome/browser/omnibox/voice/VoiceRecognitionHandlerTest.java",
   "javatests/src/org/chromium/chrome/browser/page_info/ConnectionInfoPopupTest.java",
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteController.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteController.java
index 7ef08d5b..5dab444 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteController.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/AutocompleteController.java
@@ -329,7 +329,7 @@
             SuggestionAnswer answer, String fillIntoEdit, GURL url, GURL imageUrl,
             String imageDominantColor, boolean isStarred, boolean isDeletable,
             String postContentType, byte[] postData, int groupId, List<QueryTile> tiles,
-            byte[] clipboardImageData) {
+            byte[] clipboardImageData, boolean hasTabMatch) {
         assert contentClassificationOffsets.length == contentClassificationStyles.length;
         List<MatchClassification> contentClassifications = new ArrayList<>();
         for (int i = 0; i < contentClassificationOffsets.length; i++) {
@@ -347,7 +347,7 @@
         return new OmniboxSuggestion(nativeType, isSearchType, relevance, transition, contents,
                 contentClassifications, description, descriptionClassifications, answer,
                 fillIntoEdit, url, imageUrl, imageDominantColor, isStarred, isDeletable,
-                postContentType, postData, groupId, tiles, clipboardImageData);
+                postContentType, postData, groupId, tiles, clipboardImageData, hasTabMatch);
     }
 
     /**
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/CachedZeroSuggestionsManager.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/CachedZeroSuggestionsManager.java
index 91bc66f..5a0d214 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/CachedZeroSuggestionsManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/CachedZeroSuggestionsManager.java
@@ -164,7 +164,7 @@
             OmniboxSuggestion suggestion = new OmniboxSuggestion(nativeType, isSearchType, 0, 0,
                     displayText, classifications, description, classifications, null, null, url,
                     GURL.emptyGURL(), null, isStarred, isDeletable, postContentType, postData,
-                    groupId, null, null);
+                    groupId, null, null, false);
             suggestions.add(suggestion);
         }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestion.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestion.java
index 332d425..9e1b37fb 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestion.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestion.java
@@ -76,6 +76,7 @@
     private final int mGroupId;
     private final List<QueryTile> mQueryTiles;
     private final byte[] mClipboardImageData;
+    private final boolean mHasTabMatch;
 
     public OmniboxSuggestion(int nativeType, boolean isSearchType, int relevance, int transition,
             String displayText, List<MatchClassification> displayTextClassifications,
@@ -83,7 +84,7 @@
             SuggestionAnswer answer, String fillIntoEdit, GURL url, GURL imageUrl,
             String imageDominantColor, boolean isStarred, boolean isDeletable,
             String postContentType, byte[] postData, int groupId, List<QueryTile> queryTiles,
-            byte[] clipboardImageData) {
+            byte[] clipboardImageData, boolean hasTabMatch) {
         mType = nativeType;
         mIsSearchType = isSearchType;
         mRelevance = relevance;
@@ -106,6 +107,7 @@
         mGroupId = groupId;
         mQueryTiles = queryTiles;
         mClipboardImageData = clipboardImageData;
+        mHasTabMatch = hasTabMatch;
     }
 
     public int getType() {
@@ -187,6 +189,10 @@
         return mPostData;
     }
 
+    public boolean hasTabMatch() {
+        return mHasTabMatch;
+    }
+
     /**
      * @return The image data for the image clipbaord suggestion. This data has already been
      *         validated in C++ and is safe to use in the browser process.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/VoiceSuggestionProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/VoiceSuggestionProvider.java
index 3662ad13..795a72bf 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/VoiceSuggestionProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/suggestions/VoiceSuggestionProvider.java
@@ -124,7 +124,7 @@
         suggestions.add(new OmniboxSuggestion(OmniboxSuggestionType.VOICE_SUGGEST, true, 0, 1,
                 result.getMatch(), classifications, null, classifications, null, null, voiceUrl,
                 GURL.emptyGURL(), null, false, false, null, null, OmniboxSuggestion.INVALID_GROUP,
-                null, null));
+                null, null, false));
     }
 
     private boolean doesVoiceResultHaveMatch(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
index c28887e..0d9e4470 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
@@ -677,6 +677,7 @@
         for (TabObserver observer : mObservers) observer.onClosingStateChanged(this, closing);
     }
 
+    @CalledByNative
     @Override
     public boolean isHidden() {
         return mIsHidden;
@@ -1457,6 +1458,7 @@
         return restored;
     }
 
+    @CalledByNative
     private boolean isCustomTab() {
         ChromeActivity activity = getActivity();
         return activity != null && activity.isCustomTab();
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/suggestions/SwitchToTabTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/suggestions/SwitchToTabTest.java
new file mode 100644
index 0000000..a60c159
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/omnibox/suggestions/SwitchToTabTest.java
@@ -0,0 +1,126 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.omnibox.suggestions;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.ChromeActivity;
+import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.browser.omnibox.LocationBarLayout;
+import org.chromium.chrome.test.ChromeActivityTestRule;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.chrome.test.util.OmniboxTestUtils;
+import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
+import org.chromium.content_public.browser.test.util.Criteria;
+import org.chromium.content_public.browser.test.util.CriteriaHelper;
+import org.chromium.net.test.EmbeddedTestServer;
+import org.chromium.net.test.ServerCertificate;
+
+/**
+ * Tests of the Switch To Tab feature.
+ */
+@RunWith(ChromeJUnit4ClassRunner.class)
[email protected]({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
+public class SwitchToTabTest {
+    @Rule
+    public ChromeActivityTestRule<ChromeActivity> mActivityTestRule =
+            new ChromeActivityTestRule<>(ChromeActivity.class);
+
+    @Before
+    public void setUp() throws InterruptedException {
+        mActivityTestRule.startMainActivityOnBlankPage();
+    }
+
+    private void typeAndWaitForHasTabMatchSuggestions(
+            LocationBarLayout locationBarLayout, String input) throws InterruptedException {
+        mActivityTestRule.typeInOmnibox(input, false);
+
+        OmniboxTestUtils.waitForOmniboxSuggestions(locationBarLayout);
+        // waitForOmniboxSuggestions only wait until one suggestion shows up, we need to wait util
+        // autocomplete return more suggestions.
+        CriteriaHelper.pollUiThread(new Criteria() {
+            @Override
+            public boolean isSatisfied() {
+                return findFirstHasTabMatchOmniboxSuggestion(locationBarLayout) != null;
+            }
+        });
+    }
+
+    OmniboxSuggestion findFirstHasTabMatchOmniboxSuggestion(LocationBarLayout locationBarLayout) {
+        OmniboxSuggestionsDropdown suggestionsDropdown =
+                AutocompleteCoordinatorTestUtils.getSuggestionsDropdown(
+                        locationBarLayout.getAutocompleteCoordinator());
+        // Find the index of the first matching suggestion.
+        for (int i = 0; i < suggestionsDropdown.getItemCount(); ++i) {
+            OmniboxSuggestion suggestion = AutocompleteCoordinatorTestUtils.getOmniboxSuggestionAt(
+                    locationBarLayout.getAutocompleteCoordinator(), i);
+            if (suggestion != null && suggestion.hasTabMatch()) {
+                return suggestion;
+            }
+        }
+        return null;
+    }
+
+    @Test
+    @MediumTest
+    @EnableFeatures("OmniboxTabSwitchSuggestions")
+    public void testSwitchToTabSuggestion() throws InterruptedException {
+        EmbeddedTestServer testServer = EmbeddedTestServer.createAndStartHTTPSServer(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                ServerCertificate.CERT_OK);
+        final String testHttpsUrl1 = testServer.getURL("/chrome/test/data/android/about.html");
+        final String testHttpsUrl2 = testServer.getURL("/chrome/test/data/android/ok.txt");
+        final String testHttpsUrl3 = testServer.getURL("/chrome/test/data/android/test.html");
+        mActivityTestRule.loadUrlInNewTab(testHttpsUrl1);
+        mActivityTestRule.loadUrlInNewTab(testHttpsUrl2);
+        mActivityTestRule.loadUrlInNewTab(testHttpsUrl3);
+
+        LocationBarLayout locationBarLayout =
+                (LocationBarLayout) mActivityTestRule.getActivity().findViewById(R.id.location_bar);
+        typeAndWaitForHasTabMatchSuggestions(locationBarLayout, "about");
+
+        OmniboxSuggestion matchSuggestion =
+                findFirstHasTabMatchOmniboxSuggestion(locationBarLayout);
+
+        Assert.assertNotNull("No Switch to Tab suggestion returned.", matchSuggestion);
+        Assert.assertEquals(matchSuggestion.getUrl().getSpec(), testHttpsUrl1);
+    }
+
+    @Test
+    @MediumTest
+    @EnableFeatures("OmniboxTabSwitchSuggestions")
+    public void testNoSwitchToIncognitoTabFromNormalModel() throws InterruptedException {
+        EmbeddedTestServer testServer = EmbeddedTestServer.createAndStartHTTPSServer(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                ServerCertificate.CERT_OK);
+        final String testHttpsUrl1 = testServer.getURL("/chrome/test/data/android/about.html");
+        final String testHttpsUrl2 = testServer.getURL("/chrome/test/data/android/ok.txt");
+        final String testHttpsUrl3 = testServer.getURL("/chrome/test/data/android/test.html");
+        // Open the url trying to match in incognito mode.
+        mActivityTestRule.loadUrlInNewTab(testHttpsUrl1, true);
+        mActivityTestRule.loadUrlInNewTab(testHttpsUrl2);
+        mActivityTestRule.loadUrlInNewTab(testHttpsUrl3);
+
+        LocationBarLayout locationBarLayout =
+                (LocationBarLayout) mActivityTestRule.getActivity().findViewById(R.id.location_bar);
+        // trying to match incognito tab.
+        mActivityTestRule.typeInOmnibox("about", false);
+        OmniboxTestUtils.waitForOmniboxSuggestions(locationBarLayout);
+
+        OmniboxSuggestion matchSuggestion =
+                findFirstHasTabMatchOmniboxSuggestion(locationBarLayout);
+
+        Assert.assertNull("Should no Switch to Incognito Tab from normal tab.", matchSuggestion);
+    }
+}
diff --git a/chrome/android/native_java_unittests/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionBuilderForTest.java b/chrome/android/native_java_unittests/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionBuilderForTest.java
index 31b5aea..089f6aa 100644
--- a/chrome/android/native_java_unittests/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionBuilderForTest.java
+++ b/chrome/android/native_java_unittests/src/org/chromium/chrome/browser/omnibox/suggestions/OmniboxSuggestionBuilderForTest.java
@@ -39,6 +39,7 @@
     private int mGroupId;
     private List<QueryTile> mQueryTiles;
     private byte[] mClipboardImageData;
+    private boolean mHasTabMatch;
 
     /**
      * Create a suggestion builder for a search suggestion.
@@ -81,7 +82,8 @@
         return new OmniboxSuggestion(mType, mIsSearchType, mRelevance, mTransition, mDisplayText,
                 mDisplayTextClassifications, mDescription, mDescriptionClassifications, mAnswer,
                 mFillIntoEdit, mUrl, mImageUrl, mImageDominantColor, mIsStarred, mIsDeletable,
-                mPostContentType, mPostData, mGroupId, mQueryTiles, mClipboardImageData);
+                mPostContentType, mPostData, mGroupId, mQueryTiles, mClipboardImageData,
+                mHasTabMatch);
     }
 
     /**
diff --git a/chrome/browser/android/omnibox/autocomplete_controller_android.cc b/chrome/browser/android/omnibox/autocomplete_controller_android.cc
index fe94a768..67ae5fd 100644
--- a/chrome/browser/android/omnibox/autocomplete_controller_android.cc
+++ b/chrome/browser/android/omnibox/autocomplete_controller_android.cc
@@ -596,7 +596,8 @@
       ToJavaByteArray(env, post_content),
       match.suggestion_group_id.value_or(
           SearchSuggestionParser::kNoSuggestionGroupId),
-      j_query_tiles, ToJavaByteArray(env, clipboard_image_data));
+      j_query_tiles, ToJavaByteArray(env, clipboard_image_data),
+      match.has_tab_match);
 }
 
 void AutocompleteControllerAndroid::PopulateOmniboxGroupHeaders(
diff --git a/chrome/browser/android/tab_android.cc b/chrome/browser/android/tab_android.cc
index 6034d1c..fc5c8b63 100644
--- a/chrome/browser/android/tab_android.cc
+++ b/chrome/browser/android/tab_android.cc
@@ -221,6 +221,16 @@
   return base::WrapUnique(old_contents);
 }
 
+bool TabAndroid::IsCustomTab() {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  return Java_TabImpl_isCustomTab(env, weak_java_tab_.get(env));
+}
+
+bool TabAndroid::IsHidden() {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  return Java_TabImpl_isHidden(env, weak_java_tab_.get(env));
+}
+
 void TabAndroid::Destroy(JNIEnv* env, const JavaParamRef<jobject>& obj) {
   delete this;
 }
diff --git a/chrome/browser/android/tab_android.h b/chrome/browser/android/tab_android.h
index bfb398e..546166dd 100644
--- a/chrome/browser/android/tab_android.h
+++ b/chrome/browser/android/tab_android.h
@@ -103,6 +103,9 @@
       bool did_start_load,
       bool did_finish_load);
 
+  bool IsCustomTab();
+  bool IsHidden();
+
   // Methods called from Java via JNI -----------------------------------------
 
   void Destroy(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj);
diff --git a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
index 9ebff6a..603488f 100644
--- a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
+++ b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc
@@ -6,6 +6,8 @@
 
 #include <stddef.h>
 
+#include <algorithm>
+
 #include "base/bind.h"
 #include "base/bind_helpers.h"
 #include "base/stl_util.h"
@@ -54,7 +56,11 @@
 #include "chrome/browser/autocomplete/keyword_extensions_delegate_impl.h"
 #endif
 
-#if !defined(OS_ANDROID)
+#if defined(OS_ANDROID)
+#include "chrome/browser/android/tab_android.h"
+#include "chrome/browser/ui/android/tab_model/tab_model.h"
+#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
+#else
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_list.h"
 #include "chrome/browser/ui/tabs/tab_strip_model.h"
@@ -336,16 +342,44 @@
 bool ChromeAutocompleteProviderClient::IsTabOpenWithURL(
     const GURL& url,
     const AutocompleteInput* input) {
-#if !defined(OS_ANDROID)
-  Browser* active_browser = BrowserList::GetInstance()->GetLastActive();
-  content::WebContents* active_tab = nullptr;
-  if (active_browser)
-    active_tab = active_browser->tab_strip_model()->GetActiveWebContents();
   const AutocompleteInput empty_input;
   if (!input)
     input = &empty_input;
   const GURL stripped_url = AutocompleteMatch::GURLToStrippedGURL(
       url, *input, GetTemplateURLService(), base::string16());
+#if defined(OS_ANDROID)
+  for (auto it = TabModelList::begin(); it != TabModelList::end(); ++it) {
+    TabModel* model = *it;
+    if (profile_ == model->GetProfile()) {
+      for (int i = 0; i < model->GetTabCount(); ++i) {
+        TabAndroid* tab = model->GetTabAt(i);
+        if (!tab->IsHidden() || tab->IsCustomTab())
+          continue;
+        content::WebContents* web_contents = tab->web_contents();
+        if (web_contents != nullptr) {
+          if (IsStrippedURLEqualToWebContentsURL(stripped_url, web_contents)) {
+            return true;
+          }
+        } else {
+          // Browser did not load the tab yet after Chrome started. To avoid
+          // fully load WebContents, we just compare tab's url and suggestion's
+          // url.
+          // TODO(1094056) : Let's TabAndroid to support base::SupportsUserData,
+          // so we can avoid create new url over and over again.
+          const GURL tab_stripped_url = AutocompleteMatch::GURLToStrippedGURL(
+              tab->GetURL(), AutocompleteInput(), GetTemplateURLService(),
+              base::string16());
+          if (tab_stripped_url == stripped_url)
+            return true;
+        }
+      }
+    }
+  }
+#else
+  Browser* active_browser = BrowserList::GetInstance()->GetLastActive();
+  content::WebContents* active_tab = nullptr;
+  if (active_browser)
+    active_tab = active_browser->tab_strip_model()->GetActiveWebContents();
   for (auto* browser : *BrowserList::GetInstance()) {
     // Only look at same profile (and anonymity level).
     if (profile_ == browser->profile()) {
@@ -358,7 +392,7 @@
       }
     }
   }
-#endif  // !defined(OS_ANDROID)
+#endif  // defined(OS_ANDROID)
   return false;
 }
 
diff --git a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h
index fa07577..7700046 100644
--- a/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h
+++ b/chrome/browser/autocomplete/chrome_autocomplete_provider_client.h
@@ -5,6 +5,10 @@
 #ifndef CHROME_BROWSER_AUTOCOMPLETE_CHROME_AUTOCOMPLETE_PROVIDER_CLIENT_H_
 #define CHROME_BROWSER_AUTOCOMPLETE_CHROME_AUTOCOMPLETE_PROVIDER_CLIENT_H_
 
+#include <memory>
+#include <string>
+#include <vector>
+
 #include "base/macros.h"
 #include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
 #include "components/omnibox/browser/autocomplete_provider_client.h"