Create SpeechRecognizer in SearchFragment only when it is available

In case Speechrecognizer is not available make the SpeechOrb non
focusable.

Test: Added focus handling related unit tests and tested the sample
manully on an aosp cuttlefish instance.

Bug: 169936953

Change-Id: I96637b234072fe2ba5a8734a899a4aaf45f68a99
diff --git a/leanback/leanback/src/androidTest/AndroidManifest.xml b/leanback/leanback/src/androidTest/AndroidManifest.xml
index 41194c1..2f345f9 100644
--- a/leanback/leanback/src/androidTest/AndroidManifest.xml
+++ b/leanback/leanback/src/androidTest/AndroidManifest.xml
@@ -16,6 +16,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="androidx.leanback.test">
 
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
     <application
         android:supportsRtl="true">
         <activity android:name="androidx.leanback.widget.GridActivity"
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
index 0565f5f..cd4bca8 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchFragmentTest.java
@@ -18,9 +18,13 @@
  */
 package androidx.leanback.app;
 
+import static org.junit.Assert.assertTrue;
+
 import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
+import android.speech.SpeechRecognizer;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -40,11 +44,14 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.testutils.AnimationTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Objects;
+
 @LargeTest
 @AnimationTest
 @RunWith(AndroidJUnit4.class)
@@ -154,4 +161,119 @@
         });
         leakDetector.assertNoLeak();
     }
+
+    @Test
+    public void testFocusWithSpeechRecognizerDisabled() {
+        SingleFragmentTestActivity activity = launchAndWaitActivity(
+                SpeechRecognizerDisabledFragment.class, 1000);
+
+        assertTrue(activity.findViewById(R.id.lb_search_text_editor).hasFocus());
+
+        sendKeys(KeyEvent.KEYCODE_A);
+        sendKeys(KeyEvent.KEYCODE_ENTER);
+
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ((SearchFragment) activity.getTestFragment())
+                        .getRowsFragment().getVerticalGridView().hasFocus();
+            }
+        });
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return activity.findViewById(R.id.lb_search_text_editor).hasFocus();
+            }
+        });
+    }
+
+    @Test
+    public void testFocusWithSpeechRecognizerEnabled() throws Exception {
+
+        // Skip the test for devices which do not have SpeechRecognizer
+        if (!SpeechRecognizer.isRecognitionAvailable(
+                InstrumentationRegistry.getInstrumentation().getContext())) {
+            return;
+        }
+
+        SingleFragmentTestActivity activity = launchAndWaitActivity(
+                SpeechRecognizerEnabledFragment.class, 1000);
+
+        assertTrue(activity.findViewById(R.id.lb_search_bar_speech_orb).hasFocus());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+
+        assertTrue(activity.findViewById(R.id.lb_search_text_editor).hasFocus());
+
+        sendKeys(KeyEvent.KEYCODE_A);
+        sendKeys(KeyEvent.KEYCODE_ENTER);
+
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ((SearchFragment) activity.getTestFragment())
+                        .getRowsFragment().getVerticalGridView().hasFocus();
+            }
+        });
+
+        Thread.sleep(1000);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        assertTrue(activity.findViewById(R.id.lb_search_bar_speech_orb).hasFocus());
+    }
+
+    static class SearchSupportTestFragment extends SearchFragment
+            implements SearchFragment.SearchResultProvider {
+        ArrayObjectAdapter mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+        String mPreviousQuery;
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setSearchResultProvider(this);
+        }
+
+        @Override
+        public ObjectAdapter getResultsAdapter() {
+            return mRowsAdapter;
+        }
+
+        @Override
+        public boolean onQueryTextChange(String newQuery) {
+            if (!Objects.equals(mPreviousQuery, newQuery)) {
+                mRowsAdapter.clear();
+                loadData(mRowsAdapter, 10, 1);
+                mPreviousQuery = newQuery;
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onQueryTextSubmit(String query) {
+            if (!Objects.equals(mPreviousQuery, query)) {
+                mRowsAdapter.clear();
+                loadData(mRowsAdapter, 10, 1);
+                mPreviousQuery = query;
+                return true;
+            }
+            return false;
+        }
+    }
+
+    public static final class SpeechRecognizerDisabledFragment extends SearchSupportTestFragment {
+        @Override
+        boolean isSpeechRecognizerAvailable() {
+            return false;
+        }
+    }
+
+    public static final class SpeechRecognizerEnabledFragment extends SearchSupportTestFragment {
+        @Override
+        boolean isSpeechRecognizerAvailable() {
+            return true;
+        }
+    }
 }
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
index 446400b..804ffa3 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/SearchSupportFragmentTest.java
@@ -15,9 +15,13 @@
  */
 package androidx.leanback.app;
 
+import static org.junit.Assert.assertTrue;
+
 import android.content.Context;
 import android.os.Build;
 import android.os.Bundle;
+import android.speech.SpeechRecognizer;
+import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -37,11 +41,14 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.testutils.AnimationTest;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Objects;
+
 @LargeTest
 @AnimationTest
 @RunWith(AndroidJUnit4.class)
@@ -151,4 +158,119 @@
         });
         leakDetector.assertNoLeak();
     }
+
+    @Test
+    public void testFocusWithSpeechRecognizerDisabled() {
+        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                SpeechRecognizerDisabledFragment.class, 1000);
+
+        assertTrue(activity.findViewById(R.id.lb_search_text_editor).hasFocus());
+
+        sendKeys(KeyEvent.KEYCODE_A);
+        sendKeys(KeyEvent.KEYCODE_ENTER);
+
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ((SearchSupportFragment) activity.getTestFragment())
+                        .getRowsSupportFragment().getVerticalGridView().hasFocus();
+            }
+        });
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return activity.findViewById(R.id.lb_search_text_editor).hasFocus();
+            }
+        });
+    }
+
+    @Test
+    public void testFocusWithSpeechRecognizerEnabled() throws Exception {
+
+        // Skip the test for devices which do not have SpeechRecognizer
+        if (!SpeechRecognizer.isRecognitionAvailable(
+                InstrumentationRegistry.getInstrumentation().getContext())) {
+            return;
+        }
+
+        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                SpeechRecognizerEnabledFragment.class, 1000);
+
+        assertTrue(activity.findViewById(R.id.lb_search_bar_speech_orb).hasFocus());
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_RIGHT);
+
+        assertTrue(activity.findViewById(R.id.lb_search_text_editor).hasFocus());
+
+        sendKeys(KeyEvent.KEYCODE_A);
+        sendKeys(KeyEvent.KEYCODE_ENTER);
+
+        PollingCheck.waitFor(new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return ((SearchSupportFragment) activity.getTestFragment())
+                        .getRowsSupportFragment().getVerticalGridView().hasFocus();
+            }
+        });
+
+        Thread.sleep(1000);
+
+        sendKeys(KeyEvent.KEYCODE_DPAD_UP);
+        assertTrue(activity.findViewById(R.id.lb_search_bar_speech_orb).hasFocus());
+    }
+
+    static class SearchSupportTestFragment extends SearchSupportFragment
+            implements SearchSupportFragment.SearchResultProvider {
+        ArrayObjectAdapter mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
+        String mPreviousQuery;
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            setSearchResultProvider(this);
+        }
+
+        @Override
+        public ObjectAdapter getResultsAdapter() {
+            return mRowsAdapter;
+        }
+
+        @Override
+        public boolean onQueryTextChange(String newQuery) {
+            if (!Objects.equals(mPreviousQuery, newQuery)) {
+                mRowsAdapter.clear();
+                loadData(mRowsAdapter, 10, 1);
+                mPreviousQuery = newQuery;
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onQueryTextSubmit(String query) {
+            if (!Objects.equals(mPreviousQuery, query)) {
+                mRowsAdapter.clear();
+                loadData(mRowsAdapter, 10, 1);
+                mPreviousQuery = query;
+                return true;
+            }
+            return false;
+        }
+    }
+
+    public static final class SpeechRecognizerDisabledFragment extends SearchSupportTestFragment {
+        @Override
+        boolean isSpeechRecognizerAvailable() {
+            return false;
+        }
+    }
+
+    public static final class SpeechRecognizerEnabledFragment extends SearchSupportTestFragment {
+        @Override
+        boolean isSpeechRecognizerAvailable() {
+            return true;
+        }
+    }
 }
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
index 644405a..38bcc52 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/SearchFragment.java
@@ -240,6 +240,8 @@
         }
     };
 
+    boolean mSpeechRecognizerEnabled;
+
     @Override
     public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                            int[] grantResults) {
@@ -370,7 +372,11 @@
                 if (mRowsFragment != null && mRowsFragment.getView() != null
                         && mRowsFragment.getView().hasFocus()) {
                     if (direction == View.FOCUS_UP) {
-                        return mSearchBar.findViewById(R.id.lb_search_bar_speech_orb);
+                        if (mSpeechRecognizerEnabled) {
+                            return mSearchBar.findViewById(R.id.lb_search_bar_speech_orb);
+                        } else {
+                            return mSearchBar;
+                        }
                     }
                 } else if (mSearchBar.hasFocus() && direction == View.FOCUS_DOWN) {
                     if (mRowsFragment.getView() != null
@@ -381,6 +387,15 @@
                 return null;
             }
         });
+
+        if (!isSpeechRecognizerAvailable()) {
+            if(mSearchBar.hasFocus()) {
+                mSearchBar.findViewById(R.id.lb_search_text_editor).requestFocus();
+            }
+            mSearchBar.findViewById(R.id.lb_search_bar_speech_orb).setFocusable(false);
+        } else {
+            mSpeechRecognizerEnabled = true;
+        }
         return root;
     }
 
@@ -402,7 +417,8 @@
     public void onResume() {
         super.onResume();
         mIsPaused = false;
-        if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) {
+        if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer
+                && mSpeechRecognizerEnabled) {
             mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(
                     FragmentUtil.getContext(SearchFragment.this));
             mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
@@ -766,6 +782,11 @@
         mSearchBar.setSearchQuery(query);
     }
 
+    boolean isSpeechRecognizerAvailable() {
+        return SpeechRecognizer.isRecognitionAvailable(
+                FragmentUtil.getContext(SearchFragment.this));
+    }
+
     static class ExternalQuery {
         String mQuery;
         boolean mSubmit;
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
index 124da0b..3c3a515 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/SearchSupportFragment.java
@@ -235,6 +235,8 @@
         }
     };
 
+    boolean mSpeechRecognizerEnabled;
+
     @Override
     public void onRequestPermissionsResult(int requestCode, String[] permissions,
                                            int[] grantResults) {
@@ -365,7 +367,11 @@
                 if (mRowsSupportFragment != null && mRowsSupportFragment.getView() != null
                         && mRowsSupportFragment.getView().hasFocus()) {
                     if (direction == View.FOCUS_UP) {
-                        return mSearchBar.findViewById(R.id.lb_search_bar_speech_orb);
+                        if (mSpeechRecognizerEnabled) {
+                            return mSearchBar.findViewById(R.id.lb_search_bar_speech_orb);
+                        } else {
+                            return mSearchBar;
+                        }
                     }
                 } else if (mSearchBar.hasFocus() && direction == View.FOCUS_DOWN) {
                     if (mRowsSupportFragment.getView() != null
@@ -376,6 +382,15 @@
                 return null;
             }
         });
+
+        if (!isSpeechRecognizerAvailable()) {
+            if (mSearchBar.hasFocus()) {
+                mSearchBar.findViewById(R.id.lb_search_text_editor).requestFocus();
+            }
+            mSearchBar.findViewById(R.id.lb_search_bar_speech_orb).setFocusable(false);
+        } else {
+            mSpeechRecognizerEnabled = true;
+        }
         return root;
     }
 
@@ -397,9 +412,9 @@
     public void onResume() {
         super.onResume();
         mIsPaused = false;
-        if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer) {
-            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(
-                    getContext());
+        if (mSpeechRecognitionCallback == null && null == mSpeechRecognizer
+                && mSpeechRecognizerEnabled) {
+            mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(getContext());
             mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
         }
         if (mPendingStartRecognitionWhenPaused) {
@@ -761,6 +776,10 @@
         mSearchBar.setSearchQuery(query);
     }
 
+    boolean isSpeechRecognizerAvailable() {
+        return SpeechRecognizer.isRecognitionAvailable(getContext());
+    }
+
     static class ExternalQuery {
         String mQuery;
         boolean mSubmit;