Added SpeechActivity to handle incoming RecognitionIntents

This commit is contained in:
Ciaran O'Reilly 2020-11-25 22:27:04 +01:00
parent 920f3468b2
commit c3755cde41
7 changed files with 368 additions and 1 deletions

View File

@ -8,7 +8,81 @@
<application
android:allowBackup="false"
android:icon="@drawable/ic_service_trigger"
android:label="@string/app_name" >
android:label="@string/app_name"
android:launchMode="standard"
android:theme="@style/AppTheme">
<activity
android:name=".SpeechActivity">
<intent-filter>
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.speech.action.WEB_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- input/output: Nothing -->
<!-- Samsung Galaxy SII launches VOICE_COMMAND when HOME is double-clicked -->
<intent-filter>
<action android:name="android.intent.action.VOICE_COMMAND" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!--
Action ASSIST (API 16)
Input: EXTRA_ASSIST_PACKAGE, EXTRA_ASSIST_CONTEXT. Output: nothing.
* "Search assistant" on CM10.2, which can be mapped to various buttons.
* Long press on the HOME button on Nexus 5X.
* Upper physical button on Huawei Watch 2.
The default ASSIST app is user-configurable on the phone but not on Wear,
i.e. on the phone the user can choose which app is started, e.g. when long-pressing
on the HOME button, and the filter priority plays no role. On Wear the choice
is based only on the priority.
We set it to lower than default to let the other guy win. This is probably
specific to Huawei Watch 2 with its weird setup,
where the upper button launches ASSIST (and this cannot be
changed) and the lower button can open any app (other than Google Assist).
-->
<intent-filter android:priority="-10">
<action android:name="android.intent.action.ASSIST" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- input/output: Nothing -->
<!-- API 3 -->
<!-- "Voice search" on CM10.2, which can be mapped to various buttons -->
<intent-filter>
<action android:name="android.intent.action.SEARCH_LONG_PRESS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- input/output: Nothing -->
<intent-filter>
<action android:name="android.speech.action.VOICE_SEARCH_HANDS_FREE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- TODO: future work
<intent-filter>
<action android:name="android.provider.MediaStore.RECORD_SOUND" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".VoskRecognitionService"

View File

@ -0,0 +1,248 @@
package cat.oreilly.localstt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.Intent;
import android.app.PendingIntent;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.os.Message;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Locale;
import java.util.List;
public class SpeechActivity extends AppCompatActivity {
protected static final String TAG = SpeechActivity.class.getSimpleName();
private static final String MSG = "MSG";
private static final int MSG_TOAST = 1;
private static final int MSG_RESULT_ERROR = 2;
public static final Integer RecordAudioRequestCode = 1;
private SpeechRecognizer speechRecognizer;
private EditText editText;
protected static class SimpleMessageHandler extends Handler {
private final WeakReference<SpeechActivity> mRef;
private SimpleMessageHandler(SpeechActivity c) {
mRef = new WeakReference<>(c);
}
public void handleMessage(Message msg) {
SpeechActivity outerClass = mRef.get();
if (outerClass != null) {
Bundle b = msg.getData();
String msgAsString = b.getString(MSG);
switch (msg.what) {
case MSG_TOAST:
outerClass.toast(msgAsString);
break;
case MSG_RESULT_ERROR:
outerClass.showError(msgAsString);
break;
default:
break;
}
}
}
}
protected static Message createMessage(int type, String str) {
Bundle b = new Bundle();
b.putString(MSG, str);
Message msg = Message.obtain();
msg.what = type;
msg.setData(b);
return msg;
}
protected void toast(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
void showError(String msg) {
editText.setText(msg);
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.speech_activity);
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
checkPermission();
}
editText = findViewById(R.id.text);
speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this);
speechRecognizer.setRecognitionListener(new RecognitionListener() {
@Override
public void onReadyForSpeech(Bundle bundle) {
}
@Override
public void onBeginningOfSpeech() {
editText.setText("");
editText.setHint("Listening...");
}
@Override
public void onRmsChanged(float v) {
}
@Override
public void onBufferReceived(byte[] bytes) {
}
@Override
public void onEndOfSpeech() {
speechRecognizer.stopListening();
}
@Override
public void onError(int i) {
}
@Override
public void onResults(Bundle bundle) {
Log.i(TAG, "onResults");
ArrayList<String> results = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
Log.i(TAG, results.get(0));
editText.setText(results.get(0));
returnResults(results);
}
@Override
public void onPartialResults(Bundle bundle) {
Log.i(TAG, "onPartialResults");
ArrayList<String> data = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
Log.i(TAG, data.get(0));
editText.setText(data.get(0));
}
@Override
public void onEvent(int i, Bundle bundle) {
Log.d(TAG, bundle.toString());
}
});
}
@Override
public void onStart() {
super.onStart();
Log.i(TAG, "onStart");
final Intent speechRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault());
speechRecognizer.startListening(speechRecognizerIntent);
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.i(TAG, "onDestroy");
speechRecognizer.destroy();
}
private void checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.RECORD_AUDIO },
RecordAudioRequestCode);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == RecordAudioRequestCode && grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
Toast.makeText(this, "Permission Granted", Toast.LENGTH_SHORT).show();
}
}
private void returnResults(ArrayList<String> results) {
Handler handler = new SimpleMessageHandler(this);
Intent incomingIntent = getIntent();
Log.d(TAG, incomingIntent.toString());
Bundle extras = incomingIntent.getExtras();
if (extras == null) {
return;
}
Log.d(TAG, extras.toString());
PendingIntent pendingIntent = getPendingIntent(extras);
if (pendingIntent == null) {
Log.d(TAG, "No pending intent, setting result intent.");
setResultIntent(handler, results);
} else {
Log.d(TAG, pendingIntent.toString());
Bundle bundle = extras.getBundle(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE);
if (bundle == null) {
bundle = new Bundle();
}
Intent intent = new Intent();
intent.putExtras(bundle);
// This is for Google Maps, YouTube, ...
// intent.putExtra(SearchManager.QUERY, result);
// Display a toast with the transcription.
handler.sendMessage(
createMessage(MSG_TOAST, String.format(getString(R.string.toastForwardedMatches), results.get(0))));
try {
Log.d(TAG, "Sending result via pendingIntent");
pendingIntent.send(this, AppCompatActivity.RESULT_OK, intent);
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, e.getMessage());
handler.sendMessage(createMessage(MSG_TOAST, e.getMessage()));
}
}
finish();
}
private void setResultIntent(final Handler handler, List<String> matches) {
Intent intent = new Intent();
intent.putStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS, new ArrayList<>(matches));
setResult(Activity.RESULT_OK, intent);
}
private PendingIntent getPendingIntent(Bundle extras) {
Parcelable extraResultsPendingIntentAsParceable = extras
.getParcelable(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT);
if (extraResultsPendingIntentAsParceable != null) {
// PendingIntent.readPendingIntentOrNullFromParcel(mExtraResultsPendingIntent);
if (extraResultsPendingIntentAsParceable instanceof PendingIntent) {
return (PendingIntent) extraResultsPendingIntentAsParceable;
}
}
return null;
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SpeechActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_centerInParent="true"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_height="wrap_content">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="15dp"
android:padding="10dp"
android:hint="Loading..."
android:id="@+id/text"
android:layout_centerInParent="true"
/>
</RelativeLayout>
</RelativeLayout>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
</resources>

View File

@ -4,5 +4,6 @@
<string name="app_name">LocalSTT</string>
<string name="vosk_recognition_service">Kaldi/Vosk Recognizer</string>
<string name="deepspeech_recognition_service">Deepspeech Recognizer</string>
<string name="toastForwardedMatches">Recognized: %1$s</string>
</resources>

View File

@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

3
gradle.properties Normal file
View File

@ -0,0 +1,3 @@
android.enableD8=true
android.enableJetifier=true
android.useAndroidX=true