blob: 1cba281b34ba7b5eb3a62568925ce3415790f8fb [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 android.speech.hotword;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;
/**
* This class provides a base class for hotword detection service implementations.
* This class should be extended only if you wish to implement a new hotword recognizer.
*/
public abstract class HotwordRecognitionService extends Service {
/**
* The {@link Intent} that must be declared as handled by the service.
*/
@SdkConstant(SdkConstantType.SERVICE_ACTION)
public static final String SERVICE_INTERFACE =
"android.speech.hotword.HotwordRecognitionService";
/** Log messages identifier */
private static final String TAG = "HotwordRecognitionService";
/** Debugging flag */
private static final boolean DBG = false;
/**
* Key used to retrieve a string to be displayed to the user.
*/
public static final String KEY_PROMPT_TEXT = "prompt_text";
/**
* Event type used to indicate to the user that the prompt for
* hotword recognition has changed.
*/
public static final int EVENT_TYPE_PROMPT_CHANGED = 1;
/** Audio recording error. */
public static final int ERROR_AUDIO = 1;
/** RecognitionService busy. */
public static final int ERROR_RECOGNIZER_BUSY = 2;
/** This indicates a permanent failure and the clients shouldn't retry on this */
public static final int ERROR_FAILED = 3;
/** Client-side errors */
public static final int ERROR_CLIENT = 4;
/** The service timed out */
public static final int ERROR_TIMEOUT = 5;
/** The service received concurrent start calls */
public static final int ERROR_SERVICE_ALREADY_STARTED = 6;
/** Hotword recognition is unavailable on the device */
public static final int ERROR_UNAVAILABLE = 7;
private static final int MSG_START_RECOGNITION = 1;
private static final int MSG_STOP_RECOGNITION = 2;
/**
* The current callback of an application that invoked the
* {@link HotwordRecognitionService#onStartHotwordRecognition(Callback)} method
*/
private Callback mCurrentCallback = null;
// Handle the client dying.
private final IBinder.DeathRecipient mCallbackDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (DBG) Log.i(TAG, "HotwordRecognitionService listener died");
mCurrentCallback = null;
}
};
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_START_RECOGNITION:
dispatchStartRecognition((IHotwordRecognitionListener) msg.obj);
break;
case MSG_STOP_RECOGNITION:
dispatchStopRecognition((IHotwordRecognitionListener) msg.obj);
break;
}
}
};
/** Binder of the hotword recognition service */
private RecognitionServiceBinder mBinder = new RecognitionServiceBinder(this);
private void dispatchStartRecognition(IHotwordRecognitionListener listener) {
if (mCurrentCallback == null) {
if (DBG) Log.d(TAG, "created new mCurrentCallback, listener = " + listener.asBinder());
try {
listener.asBinder().linkToDeath(mCallbackDeathRecipient, 0);
} catch (RemoteException e) {
if (DBG) Log.d(TAG, "listener died before linkToDeath()");
}
mCurrentCallback = new Callback(listener);
HotwordRecognitionService.this.onStartHotwordRecognition(mCurrentCallback);
} else {
try {
listener.onHotwordError(ERROR_RECOGNIZER_BUSY);
} catch (RemoteException e) {
if (DBG) Log.d(TAG, "onError call from startRecognition failed");
}
if (DBG) Log.d(TAG, "concurrent startRecognition received - ignoring this call");
}
}
private void dispatchStopRecognition(IHotwordRecognitionListener listener) {
try {
if (mCurrentCallback == null) {
listener.onHotwordError(ERROR_CLIENT);
Log.w(TAG, "stopRecognition called with no preceding startRecognition - ignoring");
} else if (mCurrentCallback.mListener.asBinder() != listener.asBinder()) {
listener.onHotwordError(ERROR_RECOGNIZER_BUSY);
Log.w(TAG, "stopRecognition called by a different caller - ignoring");
} else { // the correct state
mCurrentCallback.onHotwordRecognitionStopped();
mCurrentCallback = null;
HotwordRecognitionService.this.onStopHotwordRecognition();
}
} catch (RemoteException e) { // occurs if onError fails
if (DBG) Log.d(TAG, "onError call from stopRecognition failed");
}
}
@Override
public IBinder onBind(final Intent intent) {
if (DBG) Log.d(TAG, "onBind, intent=" + intent);
return mBinder;
}
@Override
public void onDestroy() {
if (DBG) Log.d(TAG, "onDestroy");
if (mCurrentCallback != null) {
mCurrentCallback.mListener.asBinder().unlinkToDeath(mCallbackDeathRecipient, 0);
mCurrentCallback = null;
}
mBinder.clearReference();
super.onDestroy();
}
/**
* Notifies the service to start a recognition.
*
* @param callback that receives the callbacks from the service.
*/
public abstract void onStartHotwordRecognition(Callback callback);
/**
* Notifies the service to stop recognition.
*/
public abstract void onStopHotwordRecognition();
/** Binder of the hotword recognition service */
private static class RecognitionServiceBinder extends IHotwordRecognitionService.Stub {
private HotwordRecognitionService mInternalService;
public RecognitionServiceBinder(HotwordRecognitionService service) {
mInternalService = service;
}
public void startHotwordRecognition(IHotwordRecognitionListener listener) {
if (DBG) Log.d(TAG, "startRecognition called by: " + listener.asBinder());
if (mInternalService != null && mInternalService.checkPermissions(listener)) {
mInternalService.mHandler.sendMessage(
Message.obtain(mInternalService.mHandler, MSG_START_RECOGNITION, listener));
}
}
public void stopHotwordRecognition(IHotwordRecognitionListener listener) {
if (DBG) Log.d(TAG, "stopRecognition called by: " + listener.asBinder());
if (mInternalService != null && mInternalService.checkPermissions(listener)) {
mInternalService.mHandler.sendMessage(
Message.obtain(mInternalService.mHandler, MSG_STOP_RECOGNITION, listener));
}
}
private void clearReference() {
mInternalService = null;
}
}
/**
* Checks whether the caller has sufficient permissions
*
* @param listener to send the error message to in case of error.
* @return {@code true} if the caller has enough permissions, {@code false} otherwise.
*/
private boolean checkPermissions(IHotwordRecognitionListener listener) {
if (DBG) Log.d(TAG, "checkPermissions");
if (checkCallingOrSelfPermission(android.Manifest.permission.HOTWORD_RECOGNITION) ==
PackageManager.PERMISSION_GRANTED) {
return true;
}
try {
Log.e(TAG, "Recognition service called without HOTWORD_RECOGNITION permissions");
listener.onHotwordError(ERROR_FAILED);
} catch (RemoteException e) {
Log.e(TAG, "onHotwordError(ERROR_FAILED) message failed", e);
}
return false;
}
/**
* This class acts passes on the callbacks received from the Hotword service
* to the listener.
*/
public static class Callback {
private final IHotwordRecognitionListener mListener;
private Callback(IHotwordRecognitionListener listener) {
mListener = listener;
}
/**
* Called when the service starts listening for hotword.
*/
public void onHotwordRecognitionStarted() throws RemoteException {
mListener.onHotwordRecognitionStarted();
}
/**
* Called when the service starts listening for hotword.
*/
public void onHotwordRecognitionStopped() throws RemoteException {
mListener.onHotwordRecognitionStopped();
}
/**
* Called on an event of interest to the client.
*
* @param eventType the event type.
* @param eventBundle a Bundle containing the hotword event(s).
*/
public void onHotwordEvent(int eventType, Bundle eventBundle) throws RemoteException {
mListener.onHotwordEvent(eventType, eventBundle);
}
/**
* Called back when hotword is detected.
*
* @param activityIntent for the activity to launch, post hotword detection.
*/
public void onHotwordRecognized(Intent activityIntent) throws RemoteException {
mListener.onHotwordRecognized(activityIntent);
}
/**
* Called when the HotwordRecognitionService encounters an error.
*
* @param errorCode the error code describing the error that was encountered.
*/
public void onError(int errorCode) throws RemoteException {
mListener.onHotwordError(errorCode);
}
}
}