├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── android ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── de │ └── siteof │ └── rn │ └── androidspeechrecognizer │ ├── ArgumentsConverter.java │ ├── Constants.java │ ├── ListenerEvents.java │ ├── ListenerMapRecognitionListener.java │ ├── RNAndroidSpeechRecognizerModule.java │ └── RNAndroidSpeechRecognizerPackage.java ├── package.json └── src ├── __tests__ ├── index.spec.js └── lib │ └── react-native.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "env": { 4 | "test": { 5 | "plugins": [ 6 | ["module-alias", [ 7 | { "expose": "react-native", "src": "./src/__tests__/lib/react-native.js"} 8 | ]] 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /ios 3 | /windows 4 | /android/build 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Daniel Ecer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # react-native-android-speech-recognizer 3 | 4 | The purpose of this module is to provide access to 5 | [Android's SpeechRecognizer API](https://developer.android.com/reference/android/speech/SpeechRecognizer.html) 6 | for React Native apps. 7 | 8 | This module isn't meant to abstract the API. Higher level modules could be written to do that. 9 | 10 | The SpeechRecognizer can be used to integrate voice recognition into your app rather than using the default UI. 11 | 12 | ## Getting started 13 | 14 | `$ npm install react-native-android-speech-recognizer --save` 15 | 16 | ### Mostly automatic installation 17 | 18 | `$ react-native link react-native-android-speech-recognizer` 19 | 20 | ### Manual installation 21 | 22 | #### Android 23 | 24 | 1. Open up `android/app/src/main/java/[...]/MainActivity.java` 25 | - Add `import de.siteof.rn.androidspeechrecognizer.RNAndroidSpeechRecognizerPackage;` to the imports at the top of the file 26 | - Add `new RNAndroidSpeechRecognizerPackage()` to the list returned by the `getPackages()` method 27 | 2. Append the following lines to `android/settings.gradle`: 28 | ``` 29 | include ':react-native-android-speech-recognizer' 30 | project(':react-native-android-speech-recognizer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-speech-recognizer/android') 31 | ``` 32 | 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: 33 | ``` 34 | compile project(':react-native-android-speech-recognizer') 35 | ``` 36 | 37 | ## Permissions 38 | 39 | To use this library you will need the RECORD_AUDIO permission. 40 | 41 | Insert the following in `android/app/src/AndroidManifest.xml`: 42 | ```xml 43 | 44 | ``` 45 | 46 | ## Usage 47 | 48 | The API follows Android's 49 | [SpeechRecognizer](https://developer.android.com/reference/android/speech/SpeechRecognizer.html) 50 | and 51 | [RecognizerIntent](https://developer.android.com/reference/android/speech/RecognizerIntent.html) 52 | 53 | Some changes are necessary due to the way React Native works: 54 | 55 | * Methods can only return promises (or nothing) due to being asynchronous 56 | * Types are restricted by the what React Native supports 57 | * Callbacks can't be called multiple times (events can) 58 | 59 | ```javascript 60 | import { 61 | SpeechRecognizer, 62 | RecognizerIntent, 63 | RecognitionListener 64 | } from 'react-native-android-speech-recognizer'; 65 | 66 | const recognise = options => new Promise(async (resolve, reject) => { 67 | const available = await SpeechRecognizer.isRecognitionAvailable(); 68 | if (!available) { 69 | reject("not available"); 70 | } 71 | const recognizer = await SpeechRecognizer.createSpeechRecognizer(); 72 | recognizer.setRecognitionListener({ 73 | onError: event => reject("Failed with error code: " + event.error), 74 | onResults: event => { 75 | const recognition = event.results[SpeechRecognizer.RESULTS_RECOGNITION]; 76 | const bestRecognition = recognition[0]; 77 | resolve(bestRecognition); 78 | } 79 | }); 80 | recognizer.startListening(RecognizerIntent.ACTION_RECOGNIZE_SPEECH, {}); 81 | }); 82 | 83 | recognise().then(bestRecognition => { 84 | console.log("recognised:", resultTextToEvent(bestRecognition)); 85 | }).catch(error => { 86 | console.log("error:", error); 87 | }); 88 | ``` 89 | 90 | You could also request partial results like so: 91 | 92 | ```javascript 93 | recognizer.setRecognitionListener({ 94 | // ... 95 | onPartialResults: event => { 96 | const recognition = event.partialResults[SpeechRecognizer.RESULTS_RECOGNITION]; 97 | const bestRecognition = recognition[0]; 98 | console.log("best recognition so far:", bestRecognition); 99 | } 100 | }); 101 | 102 | recognizer.startListening( 103 | RecognizerIntent.ACTION_RECOGNIZE_SPEECH, { 104 | [RecognizerIntent.EXTRA_PARTIAL_RESULTS]: true 105 | } 106 | ); 107 | ``` 108 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | apply plugin: 'com.android.library' 3 | 4 | android { 5 | compileSdkVersion 23 6 | buildToolsVersion "23.0.1" 7 | 8 | defaultConfig { 9 | minSdkVersion 16 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | ndk { 14 | abiFilters "armeabi-v7a", "x86" 15 | } 16 | } 17 | lintOptions { 18 | warning 'InvalidPackage' 19 | } 20 | } 21 | 22 | dependencies { 23 | compile 'com.facebook.react:react-native:0.20.+' 24 | } 25 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/java/de/siteof/rn/androidspeechrecognizer/ArgumentsConverter.java: -------------------------------------------------------------------------------- 1 | package de.siteof.rn.androidspeechrecognizer; 2 | 3 | import android.os.Bundle; 4 | import android.util.Log; 5 | 6 | import com.facebook.react.bridge.Arguments; 7 | import com.facebook.react.bridge.ReadableArray; 8 | import com.facebook.react.bridge.WritableArray; 9 | import com.facebook.react.bridge.WritableMap; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | 16 | 17 | /** 18 | * Amended com.facebook.react.bridge.Arguments to handle: 19 | * 25 | */ 26 | public class ArgumentsConverter { 27 | 28 | private static final String TAG = ArgumentsConverter.class.getName(); 29 | 30 | private ArgumentsConverter() { 31 | // prevent instantiation 32 | } 33 | 34 | public static void pushObject(WritableArray a, Object v) { 35 | if (v == null) { 36 | a.pushNull(); 37 | } else if (v instanceof String) { 38 | a.pushString((String) v); 39 | } else if (v instanceof Bundle) { 40 | a.pushMap(fromBundle((Bundle) v)); 41 | } else if (v instanceof Byte) { 42 | a.pushInt(((Byte) v) & 0xff); 43 | } else if (v instanceof Integer) { 44 | a.pushInt((Integer) v); 45 | } else if (v instanceof Float) { 46 | a.pushDouble((Float) v); 47 | } else if (v instanceof Double) { 48 | a.pushDouble((Double) v); 49 | } else if (v instanceof Boolean) { 50 | a.pushBoolean((Boolean) v); 51 | } else { 52 | throw new IllegalArgumentException("Unknown type " + v.getClass()); 53 | } 54 | } 55 | 56 | public static WritableArray fromArray(Object array) { 57 | WritableArray catalystArray = Arguments.createArray(); 58 | if (array instanceof String[]) { 59 | for (String v: (String[]) array) { 60 | catalystArray.pushString(v); 61 | } 62 | } else if (array instanceof Bundle[]) { 63 | for (Bundle v: (Bundle[]) array) { 64 | catalystArray.pushMap(fromBundle(v)); 65 | } 66 | } else if (array instanceof byte[]) { 67 | for (byte v: (byte[]) array) { 68 | catalystArray.pushInt(v & 0xff); 69 | } 70 | } else if (array instanceof int[]) { 71 | for (int v: (int[]) array) { 72 | catalystArray.pushInt(v); 73 | } 74 | } else if (array instanceof float[]) { 75 | for (float v: (float[]) array) { 76 | catalystArray.pushDouble(v); 77 | } 78 | } else if (array instanceof double[]) { 79 | for (double v: (double[]) array) { 80 | catalystArray.pushDouble(v); 81 | } 82 | } else if (array instanceof boolean[]) { 83 | for (boolean v: (boolean[]) array) { 84 | catalystArray.pushBoolean(v); 85 | } 86 | } else if (array instanceof Object[]) { 87 | for (Object v: (Object[]) array) { 88 | pushObject(catalystArray, v); 89 | } 90 | } else { 91 | throw new IllegalArgumentException("Unknown array type " + array.getClass()); 92 | } 93 | return catalystArray; 94 | } 95 | 96 | public static WritableMap fromBundle(Bundle bundle) { 97 | WritableMap map = Arguments.createMap(); 98 | for (String key: bundle.keySet()) { 99 | Object value = bundle.get(key); 100 | if (value == null) { 101 | map.putNull(key); 102 | } else if (value.getClass().isArray()) { 103 | map.putArray(key, Arguments.fromArray(value)); 104 | } else if (value instanceof String) { 105 | map.putString(key, (String) value); 106 | } else if (value instanceof Number) { 107 | if (value instanceof Integer) { 108 | map.putInt(key, (Integer) value); 109 | } else { 110 | map.putDouble(key, ((Number) value).doubleValue()); 111 | } 112 | } else if (value instanceof Boolean) { 113 | map.putBoolean(key, (Boolean) value); 114 | } else if (value instanceof Bundle) { 115 | map.putMap(key, fromBundle((Bundle) value)); 116 | } else if (value instanceof List) { 117 | map.putArray(key, fromArray(((List) value).toArray())); 118 | } else { 119 | throw new IllegalArgumentException("Could not convert " + value.getClass()); 120 | } 121 | } 122 | return map; 123 | } 124 | 125 | public static List toStringList(ReadableArray a) { 126 | int size = a.size(); 127 | List list = new ArrayList(size); 128 | for (int i = 0; i < size; i++) { 129 | list.add(a.getString(i)); 130 | } 131 | return list; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /android/src/main/java/de/siteof/rn/androidspeechrecognizer/Constants.java: -------------------------------------------------------------------------------- 1 | package de.siteof.rn.androidspeechrecognizer; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class Constants { 9 | private static String TAG = Constants.class.getSimpleName(); 10 | 11 | public static Map getConstants() { 12 | final Map constants = new HashMap<>(); 13 | 14 | constants.put( 15 | "SpeechRecognizer", 16 | extractConstants(android.speech.SpeechRecognizer.class) 17 | ); 18 | constants.put("RecognizerIntent", 19 | extractConstants(android.speech.RecognizerIntent.class) 20 | ); 21 | constants.put("RecognitionListener", 22 | getListenerEventsMap() 23 | ); 24 | 25 | return constants; 26 | } 27 | 28 | private static Map getListenerEventsMap() { 29 | Map m = new HashMap<>(); 30 | for (ListenerEvents listenerEvent : ListenerEvents.values()) { 31 | m.put(listenerEvent.name(), listenerEvent.value()); 32 | } 33 | return m; 34 | } 35 | 36 | private static Map extractConstants( 37 | Class c 38 | ) { 39 | Map m = new HashMap<>(); 40 | for (java.lang.reflect.Field field: c.getDeclaredFields()) { 41 | String name = field.getName(); 42 | Class type = field.getType(); 43 | try { 44 | if ( 45 | name.toUpperCase().equals(name) && 46 | ( 47 | type.isPrimitive() || 48 | Number.class.isAssignableFrom(type) || 49 | type.equals(String.class) 50 | ) 51 | ) { 52 | field.setAccessible(true); 53 | Object value = field.get(c); 54 | m.put(name, value); 55 | } 56 | } catch(Exception e) { 57 | Log.w(TAG, "Failed to extract constant " + name + " from " + c); 58 | } 59 | } 60 | return m; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /android/src/main/java/de/siteof/rn/androidspeechrecognizer/ListenerEvents.java: -------------------------------------------------------------------------------- 1 | package de.siteof.rn.androidspeechrecognizer; 2 | 3 | public enum ListenerEvents { 4 | ON_BEGINNING_OF_SPEECH("onBeginningOfSpeech"), 5 | ON_BUFFER_RECEIVED("onBufferReceived"), 6 | ON_END_OF_SPEECH("onEndOfSpeech"), 7 | ON_ERROR("onError"), 8 | ON_EVENT("onEvent"), 9 | ON_PARTIAL_RESULTS("onPartialResults"), 10 | ON_READY_FOR_SPEECH("onReadyForSpeech"), 11 | ON_RESULTS("onResults"), 12 | ON_RMS_CHANGED("onRmsChanged"); 13 | 14 | private String value; 15 | 16 | ListenerEvents(String value) { 17 | this.value = value; 18 | } 19 | 20 | public String value() { 21 | return this.value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/src/main/java/de/siteof/rn/androidspeechrecognizer/ListenerMapRecognitionListener.java: -------------------------------------------------------------------------------- 1 | package de.siteof.rn.androidspeechrecognizer; 2 | 3 | import android.os.Bundle; 4 | import android.speech.RecognitionListener; 5 | import android.util.Log; 6 | 7 | import com.facebook.react.bridge.Arguments; 8 | import com.facebook.react.bridge.Callback; 9 | import com.facebook.react.bridge.WritableMap; 10 | import com.facebook.react.modules.core.RCTNativeAppEventEmitter; 11 | 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.Set; 16 | 17 | public class ListenerMapRecognitionListener implements RecognitionListener { 18 | 19 | private static final String TAG = ListenerMapRecognitionListener.class.getSimpleName(); 20 | 21 | private final Set enabledEvents; 22 | private final RCTNativeAppEventEmitter emitter; 23 | private final String eventPrefix; 24 | 25 | public ListenerMapRecognitionListener( 26 | Set enabledEvents, 27 | RCTNativeAppEventEmitter emitter, 28 | String eventPrefix 29 | ) { 30 | this.enabledEvents = enabledEvents; 31 | this.emitter = emitter; 32 | this.eventPrefix = eventPrefix; 33 | } 34 | 35 | private boolean isEnabled(ListenerEvents event) { 36 | return enabledEvents.contains(event.value()); 37 | } 38 | 39 | private void emit(ListenerEvents event, Object data) { 40 | this.emitter.emit(this.eventPrefix + event.value(), data); 41 | } 42 | 43 | private void emit(ListenerEvents event) { 44 | this.emit(event, null); 45 | } 46 | 47 | private void emitIfEnabled(ListenerEvents event) { 48 | if (isEnabled(event)) { 49 | this.emit(event, null); 50 | } 51 | } 52 | 53 | @Override 54 | public void onBeginningOfSpeech() { 55 | Log.d(TAG, "onBeginningOfSpeech"); 56 | this.emitIfEnabled(ListenerEvents.ON_BEGINNING_OF_SPEECH); 57 | } 58 | 59 | @Override 60 | public void onBufferReceived(byte[] buffer) { 61 | Log.d(TAG, "onBufferReceived"); 62 | if (isEnabled(ListenerEvents.ON_BUFFER_RECEIVED)) { 63 | WritableMap data = Arguments.createMap(); 64 | data.putArray("buffer", ArgumentsConverter.fromArray(buffer)); 65 | emit(ListenerEvents.ON_BUFFER_RECEIVED, data); 66 | } 67 | } 68 | 69 | @Override 70 | public void onEndOfSpeech() { 71 | Log.d(TAG, "onEndOfSpeech"); 72 | this.emitIfEnabled(ListenerEvents.ON_END_OF_SPEECH); 73 | } 74 | 75 | @Override 76 | public void onError(int error) { 77 | Log.i(TAG, "onError: " + error); 78 | if (isEnabled(ListenerEvents.ON_ERROR)) { 79 | WritableMap data = Arguments.createMap(); 80 | data.putInt("error", error); 81 | emit(ListenerEvents.ON_ERROR, data); 82 | } 83 | } 84 | 85 | @Override 86 | public void onEvent(int eventType, Bundle params) { 87 | Log.d(TAG, "onEvent: " + eventType); 88 | if (isEnabled(ListenerEvents.ON_EVENT)) { 89 | WritableMap data = Arguments.createMap(); 90 | data.putInt("eventType", eventType); 91 | data.putMap("params", ArgumentsConverter.fromBundle(params)); 92 | emit(ListenerEvents.ON_EVENT, data); 93 | } 94 | } 95 | 96 | @Override 97 | public void onPartialResults(Bundle partialResults) { 98 | Log.d(TAG, "onPartialResults: " + partialResults); 99 | if (isEnabled(ListenerEvents.ON_PARTIAL_RESULTS)) { 100 | WritableMap data = Arguments.createMap(); 101 | data.putMap("partialResults", ArgumentsConverter.fromBundle(partialResults)); 102 | emit(ListenerEvents.ON_PARTIAL_RESULTS, data); 103 | } 104 | } 105 | 106 | @Override 107 | public void onReadyForSpeech(Bundle params) { 108 | Log.d(TAG, "onReadyForSpeech: " + params); 109 | if (isEnabled(ListenerEvents.ON_READY_FOR_SPEECH)) { 110 | WritableMap data = Arguments.createMap(); 111 | data.putMap("params", ArgumentsConverter.fromBundle(params)); 112 | emit(ListenerEvents.ON_READY_FOR_SPEECH, data); 113 | } 114 | } 115 | 116 | @Override 117 | public void onResults(Bundle results) { 118 | Log.d(TAG, "onResults: " + results); 119 | if (isEnabled(ListenerEvents.ON_RESULTS)) { 120 | WritableMap data = Arguments.createMap(); 121 | data.putMap("results", ArgumentsConverter.fromBundle(results)); 122 | emit(ListenerEvents.ON_RESULTS, data); 123 | } 124 | } 125 | 126 | @Override 127 | public void onRmsChanged(float rmsdB) { 128 | Log.d(TAG, "onRmsChanged: " + rmsdB); 129 | if (isEnabled(ListenerEvents.ON_RMS_CHANGED)) { 130 | WritableMap data = Arguments.createMap(); 131 | data.putDouble("rmsdB", rmsdB); 132 | emit(ListenerEvents.ON_RMS_CHANGED, data); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /android/src/main/java/de/siteof/rn/androidspeechrecognizer/RNAndroidSpeechRecognizerModule.java: -------------------------------------------------------------------------------- 1 | package de.siteof.rn.androidspeechrecognizer; 2 | 3 | import android.content.Intent; 4 | import android.os.Handler; 5 | import android.speech.SpeechRecognizer; 6 | import android.speech.RecognitionListener; 7 | import android.util.Log; 8 | 9 | import com.facebook.react.bridge.Promise; 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 12 | import com.facebook.react.bridge.ReactMethod; 13 | import com.facebook.react.bridge.ReadableArray; 14 | import com.facebook.react.bridge.ReadableMap; 15 | import com.facebook.react.bridge.ReadableMapKeySetIterator; 16 | import com.facebook.react.bridge.ReadableType; 17 | import com.facebook.react.bridge.Callback; 18 | import com.facebook.react.modules.core.RCTNativeAppEventEmitter; 19 | 20 | import java.util.Collections; 21 | import java.util.Map; 22 | import java.util.HashMap; 23 | import java.util.Set; 24 | import java.util.concurrent.ConcurrentHashMap; 25 | 26 | public class RNAndroidSpeechRecognizerModule extends ReactContextBaseJavaModule { 27 | 28 | private static final String TAG = RNAndroidSpeechRecognizerModule.class.getSimpleName(); 29 | 30 | private final ReactApplicationContext reactContext; 31 | private SpeechRecognizer speechRecognizer; 32 | private String eventPrefix = ""; 33 | private final Map listenerMap = new HashMap<>(); 34 | private final Map enabledEventsMap = new ConcurrentHashMap<>(); 35 | private final Set enabledEvents = Collections.newSetFromMap( 36 | enabledEventsMap 37 | ); 38 | private final Handler mainHandler; 39 | 40 | public RNAndroidSpeechRecognizerModule(ReactApplicationContext reactContext) { 41 | super(reactContext); 42 | this.reactContext = reactContext; 43 | this.mainHandler = new Handler(reactContext.getMainLooper()); 44 | } 45 | 46 | @Override 47 | public String getName() { 48 | return "RNAndroidSpeechRecognizer"; 49 | } 50 | 51 | @Override 52 | public Map getConstants() { 53 | return Constants.getConstants(); 54 | } 55 | 56 | // config methods 57 | 58 | @ReactMethod 59 | public void setEventPrefix(String eventPrefix) { 60 | Log.d(TAG, "setEventPrefix: " + eventPrefix); 61 | this.eventPrefix = eventPrefix; 62 | } 63 | 64 | @ReactMethod 65 | public void enableEvents(ReadableArray events) { 66 | Log.d(TAG, "enableEvents: " + events); 67 | for (String event: ArgumentsConverter.toStringList(events)) { 68 | this.enableEvent(event); 69 | } 70 | } 71 | 72 | @ReactMethod 73 | public void enableEvent(String event) { 74 | Log.d(TAG, "enableEvent: " + event); 75 | this.enabledEventsMap.put(event, Boolean.TRUE); 76 | } 77 | 78 | 79 | // static methods 80 | 81 | @ReactMethod 82 | public void isRecognitionAvailable(final Promise promise) { 83 | boolean available = SpeechRecognizer.isRecognitionAvailable(this.reactContext); 84 | Log.d(TAG, "isRecognitionAvailable: " + available); 85 | promise.resolve(available); 86 | } 87 | 88 | @ReactMethod 89 | public void createSpeechRecognizer(final Promise promise) { 90 | this.enabledEventsMap.clear(); 91 | Log.d(TAG, "createSpeechRecognizer, posting to main thread"); 92 | this.mainHandler.post(new Runnable() { 93 | @Override 94 | public void run() { 95 | Log.d(TAG, "createSpeechRecognizer (main thread)"); 96 | doCreateSpeechRecognizer(promise); 97 | } 98 | }); 99 | } 100 | 101 | private void doCreateSpeechRecognizer(final Promise promise) { 102 | this.speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this.reactContext); 103 | this.speechRecognizer.setRecognitionListener(new ListenerMapRecognitionListener( 104 | this.enabledEvents, 105 | this.reactContext.getJSModule(RCTNativeAppEventEmitter.class), 106 | this.eventPrefix 107 | )); 108 | promise.resolve(null); 109 | } 110 | 111 | // instance methods 112 | 113 | @ReactMethod 114 | public void cancel() { 115 | Log.d(TAG, "cancel, posting to main thread"); 116 | this.mainHandler.post(new Runnable() { 117 | @Override 118 | public void run() { 119 | Log.d(TAG, "cancel (main thread)"); 120 | speechRecognizer.cancel(); 121 | } 122 | }); 123 | } 124 | 125 | @ReactMethod 126 | public void destroy() { 127 | Log.d(TAG, "destroy, posting to main thread"); 128 | this.mainHandler.post(new Runnable() { 129 | @Override 130 | public void run() { 131 | Log.d(TAG, "destroy (main thread)"); 132 | speechRecognizer.destroy(); 133 | speechRecognizer = null; 134 | } 135 | }); 136 | } 137 | 138 | private Intent createIntent(String action, ReadableMap extra) { 139 | final Intent intent = new Intent(action); 140 | for ( 141 | ReadableMapKeySetIterator it = extra.keySetIterator(); 142 | it.hasNextKey(); 143 | ) { 144 | String key = it.nextKey(); 145 | ReadableType type = extra.getType(key); 146 | switch(type) { 147 | case Null: 148 | break; 149 | case Boolean: 150 | intent.putExtra(key, extra.getBoolean(key)); 151 | break; 152 | case Number: 153 | intent.putExtra(key, extra.getInt(key)); 154 | break; 155 | case String: 156 | intent.putExtra(key, extra.getString(key)); 157 | break; 158 | default: 159 | throw new IllegalArgumentException("Unsupported type " + type); 160 | } 161 | } 162 | return intent; 163 | } 164 | 165 | @ReactMethod 166 | public void startListening(String action, ReadableMap recognizerIntentParameters) { 167 | final Intent intent = createIntent(action, recognizerIntentParameters); 168 | Log.d(TAG, "startListening, posting to main thread"); 169 | this.mainHandler.post(new Runnable() { 170 | @Override 171 | public void run() { 172 | Log.d(TAG, "startListening (main thread)"); 173 | speechRecognizer.startListening(intent); 174 | } 175 | }); 176 | } 177 | 178 | @ReactMethod 179 | public void stopListening() { 180 | Log.d(TAG, "stopListening, posting to main thread"); 181 | this.mainHandler.post(new Runnable() { 182 | @Override 183 | public void run() { 184 | Log.d(TAG, "stopListening (main thread)"); 185 | speechRecognizer.stopListening(); 186 | } 187 | }); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /android/src/main/java/de/siteof/rn/androidspeechrecognizer/RNAndroidSpeechRecognizerPackage.java: -------------------------------------------------------------------------------- 1 | package de.siteof.rn.androidspeechrecognizer; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.bridge.NativeModule; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.uimanager.ViewManager; 11 | import com.facebook.react.bridge.JavaScriptModule; 12 | 13 | public class RNAndroidSpeechRecognizerPackage implements ReactPackage { 14 | @Override 15 | public List createNativeModules(ReactApplicationContext reactContext) { 16 | return Arrays.asList(new RNAndroidSpeechRecognizerModule(reactContext)); 17 | } 18 | 19 | @Override 20 | public List> createJSModules() { 21 | return Collections.emptyList(); 22 | } 23 | 24 | @Override 25 | public List createViewManagers(ReactApplicationContext reactContext) { 26 | return Collections.emptyList(); 27 | } 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-android-speech-recognizer", 3 | "repository": "de-code/react-native-android-speech-recognizer", 4 | "version": "0.0.3", 5 | "description": "Thin wrapper around Android's SpeechRecognizer API", 6 | "main": "src/index.js", 7 | "scripts": { 8 | "test": "cross-env NODE_ENV=test tape-watch -1 -r babel-register src/**/*.spec.js | faucet", 9 | "~test": "cross-env NODE_ENV=test tape-watch -w -r babel-register src/**/*.spec.js | faucet" 10 | }, 11 | "keywords": [ 12 | "react-native", 13 | "android", 14 | "SpeechRecognizer" 15 | ], 16 | "author": "Daniel Ecer", 17 | "license": "MIT", 18 | "peerDependencies": { 19 | "react-native": "0.x" 20 | }, 21 | "devDependencies": { 22 | "babel-plugin-module-alias": "^1.6.0", 23 | "babel-preset-es2015": "^6.18.0", 24 | "babel-preset-react-native": "^1.9.1", 25 | "babel-register": "^6.18.0", 26 | "cross-env": "^3.1.4", 27 | "faucet": "0.0.1", 28 | "sinon": "^1.17.7", 29 | "tape": "^4.6.3", 30 | "tape-watch": "^2.2.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import sinon from 'sinon'; 3 | 4 | import { NativeModules, NativeAppEventEmitter } from 'react-native'; 5 | 6 | const { RNAndroidSpeechRecognizer } = NativeModules; 7 | 8 | import { SpeechRecognizer } from '../index'; 9 | 10 | const DEFAULT_EVENT_PREFIX = 'RNAndroidSpeechRecognizer_'; 11 | const EVENT_NAME = 'onResults'; 12 | const OTHER_EVENT_NAME = 'onError'; 13 | 14 | const createObject = (keys, f) => keys.reduce((o, key) => { 15 | o[key] = f(key); 16 | return o; 17 | }, {}); 18 | 19 | const createSpies = keys => createObject(keys, () => sinon.spy()); 20 | 21 | const createSubscription = () => createSpies(['remove']); 22 | 23 | const sinonAssert = t => createObject(Object.keys(sinon.assert), key => (...args) => { 24 | try { 25 | sinon.assert[key].apply(sinon.assert, args); 26 | t.ok(true); 27 | } catch (e) { 28 | sinon.fail(e); 29 | } 30 | }); 31 | 32 | const resetAllSpies = () => { 33 | RNAndroidSpeechRecognizer.enableEvents.reset(); 34 | NativeAppEventEmitter.addListener.reset(); 35 | }; 36 | 37 | test('index', g => { 38 | g.test('SpeechRecognizer defined', t => { 39 | t.equal(typeof SpeechRecognizer, "object"); 40 | t.end(); 41 | }); 42 | 43 | g.test('SpeechRecognizer.isRecognitionAvailable is reference to RNAndroidSpeechRecognizer.isRecognitionAvailable', t => { 44 | t.equal(SpeechRecognizer.isRecognitionAvailable, RNAndroidSpeechRecognizer.isRecognitionAvailable); 45 | t.end(); 46 | }); 47 | 48 | g.test('setEventPrefix called with default prefix', t => { 49 | sinonAssert(t).calledWith( 50 | RNAndroidSpeechRecognizer.setEventPrefix, 51 | DEFAULT_EVENT_PREFIX 52 | ); 53 | t.end(); 54 | }); 55 | 56 | const testWithSpeechRecognizer = f => t => { 57 | resetAllSpies(); 58 | const subscription = createSubscription(); 59 | NativeAppEventEmitter.addListener.returns(subscription); 60 | RNAndroidSpeechRecognizer.createSpeechRecognizer.returns(Promise.resolve()); 61 | SpeechRecognizer.createSpeechRecognizer().then(speechRecognizer => 62 | f(t, {speechRecognizer, subscription}) 63 | ).catch(err => { 64 | if (err.stack) { 65 | t.comment(err.stack); 66 | } 67 | t.fail(err); 68 | t.end(); 69 | }); 70 | }; 71 | 72 | g.test('createSpeechRecognizer returns object', testWithSpeechRecognizer((t, {speechRecognizer}) => { 73 | t.equal(typeof speechRecognizer, "object"); 74 | t.end(); 75 | })); 76 | 77 | g.test('setRecognitionListener should call enableEvents with keys', 78 | testWithSpeechRecognizer((t, {speechRecognizer}) => { 79 | 80 | const listeners = createSpies([EVENT_NAME]); 81 | speechRecognizer.setRecognitionListener(listeners); 82 | sinonAssert(t).calledWith( 83 | RNAndroidSpeechRecognizer.enableEvents, 84 | [EVENT_NAME] 85 | ); 86 | t.end(); 87 | })); 88 | 89 | g.test('setRecognitionListener should call addListener for each listener', 90 | testWithSpeechRecognizer((t, {speechRecognizer}) => { 91 | 92 | const listeners = createSpies([EVENT_NAME]); 93 | speechRecognizer.setRecognitionListener(listeners); 94 | sinonAssert(t).calledWith( 95 | NativeAppEventEmitter.addListener, 96 | DEFAULT_EVENT_PREFIX + EVENT_NAME, 97 | listeners[EVENT_NAME] 98 | ); 99 | t.end(); 100 | })); 101 | 102 | g.test('setRecognitionListener called twice should remove old listeners', 103 | testWithSpeechRecognizer((t, {speechRecognizer}) => { 104 | 105 | const subscription = createSubscription(); 106 | const listeners = createSpies([EVENT_NAME]); 107 | NativeAppEventEmitter.addListener.returns(subscription); 108 | speechRecognizer.setRecognitionListener(listeners); 109 | speechRecognizer.setRecognitionListener({}); 110 | sinonAssert(t).calledWith(subscription.remove); 111 | t.end(); 112 | })); 113 | 114 | g.test('setRecognitionListener called twice should add new listeners', 115 | testWithSpeechRecognizer((t, {speechRecognizer}) => { 116 | 117 | const listeners = createSpies([EVENT_NAME]); 118 | const newListeners = createSpies([OTHER_EVENT_NAME]); 119 | NativeAppEventEmitter.addListener.returns(createSubscription()); 120 | speechRecognizer.setRecognitionListener(listeners); 121 | speechRecognizer.setRecognitionListener(newListeners); 122 | sinonAssert(t).calledWith( 123 | NativeAppEventEmitter.addListener, 124 | DEFAULT_EVENT_PREFIX + OTHER_EVENT_NAME, 125 | newListeners[OTHER_EVENT_NAME] 126 | ); 127 | t.end(); 128 | })); 129 | 130 | g.test('setRecognitionListener called twice should call enableEvents with new event names', 131 | testWithSpeechRecognizer((t, {speechRecognizer}) => { 132 | 133 | const listeners = createSpies([EVENT_NAME]); 134 | const newListeners = createSpies([OTHER_EVENT_NAME]); 135 | NativeAppEventEmitter.addListener.returns(createSubscription()); 136 | speechRecognizer.setRecognitionListener(listeners); 137 | speechRecognizer.setRecognitionListener(newListeners); 138 | sinonAssert(t).calledWith( 139 | RNAndroidSpeechRecognizer.enableEvents, 140 | [OTHER_EVENT_NAME] 141 | ); 142 | t.end(); 143 | })); 144 | 145 | g.end(); 146 | }); 147 | -------------------------------------------------------------------------------- /src/__tests__/lib/react-native.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | 3 | // mocks for react-native and module (here due to limitations of rewire) 4 | export const NativeModules = { 5 | RNAndroidSpeechRecognizer: { 6 | SpeechRecognizer: {}, 7 | setEventPrefix: sinon.spy(), 8 | enableEvents: sinon.spy(), 9 | isRecognitionAvailable: sinon.stub(), 10 | createSpeechRecognizer: sinon.stub(), 11 | } 12 | }; 13 | 14 | export const NativeAppEventEmitter = { 15 | addListener: sinon.stub() 16 | }; 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { NativeModules, NativeAppEventEmitter } from 'react-native'; 3 | 4 | const { RNAndroidSpeechRecognizer } = NativeModules; 5 | 6 | const { 7 | SpeechRecognizer, 8 | RecognizerIntent, 9 | RecognitionListener, 10 | isRecognitionAvailable, 11 | cancel, 12 | destroy, 13 | startListening, 14 | stopListening 15 | } = RNAndroidSpeechRecognizer || {}; 16 | 17 | const eventPrefix = 'RNAndroidSpeechRecognizer_'; 18 | RNAndroidSpeechRecognizer.setEventPrefix(eventPrefix); 19 | 20 | let emitterSubscriptions = []; 21 | 22 | const setRecognitionListener = listener => { 23 | const keys = Object.keys(listener); 24 | emitterSubscriptions.forEach(subscription => subscription.remove()); 25 | RNAndroidSpeechRecognizer.enableEvents(keys); 26 | emitterSubscriptions = keys.map(key => 27 | NativeAppEventEmitter.addListener(eventPrefix + key, listener[key]) 28 | ); 29 | } 30 | 31 | const createSpeechRecognizer = (...args) => 32 | RNAndroidSpeechRecognizer.createSpeechRecognizer(...args).then(() => ({ 33 | cancel, 34 | destroy, 35 | setRecognitionListener, 36 | startListening, 37 | stopListening 38 | })); 39 | 40 | SpeechRecognizer.isRecognitionAvailable = isRecognitionAvailable; 41 | SpeechRecognizer.createSpeechRecognizer = createSpeechRecognizer; 42 | 43 | export { 44 | SpeechRecognizer, 45 | RecognizerIntent, 46 | RecognitionListener, 47 | isRecognitionAvailable, 48 | createSpeechRecognizer 49 | } 50 | 51 | export default RNAndroidSpeechRecognizer; 52 | --------------------------------------------------------------------------------