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;