├── .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 | *
20 | * - List
21 | * - byte[]
22 | * - ReadableArray to List conversion
23 | * - toStringList
24 | *
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 |
--------------------------------------------------------------------------------