Java tutorial
/* * Copyright 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.cloud.android.speech; import com.google.auth.Credentials; import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.speech.v1beta1.RecognitionConfig; import com.google.cloud.speech.v1beta1.SpeechGrpc; import com.google.cloud.speech.v1beta1.SpeechRecognitionAlternative; import com.google.cloud.speech.v1beta1.StreamingRecognitionConfig; import com.google.cloud.speech.v1beta1.StreamingRecognitionResult; import com.google.cloud.speech.v1beta1.StreamingRecognizeRequest; import com.google.cloud.speech.v1beta1.StreamingRecognizeResponse; import com.google.protobuf.ByteString; import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.text.TextUtils; import android.util.Log; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; import io.grpc.ClientInterceptor; import io.grpc.ClientInterceptors; import io.grpc.ManagedChannel; import io.grpc.Metadata; import io.grpc.MethodDescriptor; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.internal.DnsNameResolverProvider; import io.grpc.okhttp.OkHttpChannelProvider; import io.grpc.stub.StreamObserver; /** * Handles all the API requests of Cloud Speech API. * * <p>The calling {@link android.app.Activity} needs to implement * {@link ApiFragment.Listener}.</p> * * <p>This is a <em>retained</em> Fragment.</p> */ public class ApiFragment extends Fragment { public interface Listener { /** * Called when a new piece of text was recognized by the Speech API. * * @param text The text. * @param isFinal {@code true} when the API finished processing audio. */ void onSpeechRecognized(String text, boolean isFinal); } public static final List<String> SCOPE = Collections .singletonList("https://www.googleapis.com/auth/cloud-platform"); private static final String HOSTNAME = "speech.googleapis.com"; private static final int PORT = 443; private static final String TAG = "ApiFragment"; private SpeechGrpc.SpeechStub mApi; private final StreamObserver<StreamingRecognizeResponse> mResponseObserver = new StreamObserver<StreamingRecognizeResponse>() { @Override public void onNext(StreamingRecognizeResponse response) { String text = null; boolean isFinal = false; if (response.getResultsCount() > 0) { final StreamingRecognitionResult result = response.getResults(0); isFinal = result.getIsFinal(); if (result.getAlternativesCount() > 0) { final SpeechRecognitionAlternative alternative = result.getAlternatives(0); text = alternative.getTranscript(); } } if (text != null && mListener != null) { mListener.onSpeechRecognized(text, isFinal); } } @Override public void onError(Throwable t) { Log.e(TAG, "Error calling the API.", t); } @Override public void onCompleted() { Log.i(TAG, "API completed."); } }; private StreamObserver<StreamingRecognizeRequest> mRequestObserver; private Listener mListener; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public void onDestroy() { super.onDestroy(); // Release the gRPC channel. final ManagedChannel channel = (ManagedChannel) mApi.getChannel(); if (channel != null && !channel.isShutdown()) { try { channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e(TAG, "Error shutting down the gRPC channel.", e); } } } @Override public void onAttach(Context context) { super.onAttach(context); mListener = (Listener) context; } @Override public void onDetach() { mListener = null; super.onDetach(); } /** * Sets the {@link AccessToken} to be used to call the API. * * @param accessToken The {@link AccessToken}. */ public void setAccessToken(AccessToken accessToken) { final ManagedChannel channel = new OkHttpChannelProvider().builderForAddress(HOSTNAME, PORT) .nameResolverFactory(new DnsNameResolverProvider()) .intercept(new GoogleCredentialsInterceptor(new GoogleCredentials(accessToken).createScoped(SCOPE))) .build(); mApi = SpeechGrpc.newStub(channel); } /** * Starts recognizing speech audio. * * @param sampleRate The sample rate of the audio. */ public void startRecognizing(int sampleRate) { if (mApi == null) { Log.w(TAG, "API not ready. Ignoring the request."); return; } // Configure the API mRequestObserver = mApi.streamingRecognize(mResponseObserver); mRequestObserver.onNext(StreamingRecognizeRequest.newBuilder().setStreamingConfig(StreamingRecognitionConfig .newBuilder() .setConfig(RecognitionConfig.newBuilder().setLanguageCode(getDefaultLanguageCode()) .setEncoding(RecognitionConfig.AudioEncoding.LINEAR16).setSampleRate(sampleRate).build()) .setInterimResults(true).setSingleUtterance(true).build()).build()); } /** * Recognizes the speech audio. This method should be called every time a chunk of byte buffer * is ready. * * @param data The audio data. * @param size The number of elements that are actually relevant in the {@code data}. */ public void recognize(byte[] data, int size) { if (mRequestObserver == null) { return; } // Call the streaming recognition API mRequestObserver.onNext( StreamingRecognizeRequest.newBuilder().setAudioContent(ByteString.copyFrom(data, 0, size)).build()); } /** * Finishes recognizing speech audio. */ public void finishRecognizing() { if (mRequestObserver == null) { return; } mRequestObserver.onCompleted(); mRequestObserver = null; } private String getDefaultLanguageCode() { final Locale locale = Locale.getDefault(); final StringBuilder language = new StringBuilder(locale.getLanguage()); final String country = locale.getCountry(); if (!TextUtils.isEmpty(country)) { language.append("-"); language.append(country); } return language.toString(); } /** * Authenticates the gRPC channel using the specified {@link GoogleCredentials}. */ private static class GoogleCredentialsInterceptor implements ClientInterceptor { private final Credentials mCredentials; private Metadata mCached; private Map<String, List<String>> mLastMetadata; public GoogleCredentialsInterceptor(Credentials credentials) { mCredentials = credentials; } @Override public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(final MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, final Channel next) { return new ClientInterceptors.CheckedForwardingClientCall<ReqT, RespT>( next.newCall(method, callOptions)) { @Override protected void checkedStart(Listener<RespT> responseListener, Metadata headers) throws StatusException { Metadata cachedSaved; URI uri = serviceUri(next, method); synchronized (GoogleCredentialsInterceptor.this) { Map<String, List<String>> latestMetadata = getRequestMetadata(uri); if (mLastMetadata == null || mLastMetadata != latestMetadata) { mLastMetadata = latestMetadata; mCached = toHeaders(mLastMetadata); } cachedSaved = mCached; } headers.merge(cachedSaved); delegate().start(responseListener, headers); } }; } /** * Generate a JWT-specific service URI. The URI is simply an identifier with enough * information for a service to know that the JWT was intended for it. The URI will * commonly be verified with a simple string equality check. */ private URI serviceUri(Channel channel, MethodDescriptor<?, ?> method) throws StatusException { String authority = channel.authority(); if (authority == null) { throw Status.UNAUTHENTICATED.withDescription("Channel has no authority").asException(); } // Always use HTTPS, by definition. final String scheme = "https"; final int defaultPort = 443; String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName()); URI uri; try { uri = new URI(scheme, authority, path, null, null); } catch (URISyntaxException e) { throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI for auth") .withCause(e).asException(); } // The default port must not be present. Alternative ports should be present. if (uri.getPort() == defaultPort) { uri = removePort(uri); } return uri; } private URI removePort(URI uri) throws StatusException { try { return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */, uri.getPath(), uri.getQuery(), uri.getFragment()); } catch (URISyntaxException e) { throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI after removing port") .withCause(e).asException(); } } private Map<String, List<String>> getRequestMetadata(URI uri) throws StatusException { try { return mCredentials.getRequestMetadata(uri); } catch (IOException e) { throw Status.UNAUTHENTICATED.withCause(e).asException(); } } private static Metadata toHeaders(Map<String, List<String>> metadata) { Metadata headers = new Metadata(); if (metadata != null) { for (String key : metadata.keySet()) { Metadata.Key<String> headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); for (String value : metadata.get(key)) { headers.put(headerKey, value); } } } return headers; } } }