From 290d1119eebdfb31160b9df70ef5e02487938490 Mon Sep 17 00:00:00 2001 From: Alex Agranovich Date: Wed, 3 Feb 2021 00:27:42 +0200 Subject: Add a system TextToSpeech implementation that initiates the connection through the system server. This change includes the new System Service that allows the supervised binding to the TextToSpeech service provider. It proxies the binding process from the client instead of the direct client -> texttospeech connection. Bug: 178112052 Test: atest CtsSpeechTestCases Change-Id: Ie17800bae7a84bfd6e63633f5c914ddbe2c29e9d --- core/java/android/content/Context.java | 8 + .../android/speech/tts/ITextToSpeechManager.aidl | 29 ++++ .../android/speech/tts/ITextToSpeechSession.aidl | 33 ++++ .../speech/tts/ITextToSpeechSessionCallback.aidl | 32 ++++ core/java/android/speech/tts/TextToSpeech.java | 168 ++++++++++++++++--- services/Android.bp | 2 + .../server/infra/AbstractMasterSystemService.java | 14 ++ services/java/com/android/server/SystemServer.java | 10 ++ services/texttospeech/Android.bp | 13 ++ .../TextToSpeechManagerPerUserService.java | 184 +++++++++++++++++++++ .../texttospeech/TextToSpeechManagerService.java | 77 +++++++++ 11 files changed, 543 insertions(+), 27 deletions(-) create mode 100644 core/java/android/speech/tts/ITextToSpeechManager.aidl create mode 100644 core/java/android/speech/tts/ITextToSpeechSession.aidl create mode 100644 core/java/android/speech/tts/ITextToSpeechSessionCallback.aidl create mode 100644 services/texttospeech/Android.bp create mode 100644 services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java create mode 100644 services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerService.java diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 125b5ff625b2..92e2bad6955c 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4561,6 +4561,14 @@ public abstract class Context { */ public static final String AUTOFILL_MANAGER_SERVICE = "autofill"; + /** + * Official published name of the (internal) text to speech manager service. + * + * @hide + * @see #getSystemService(String) + */ + public static final String TEXT_TO_SPEECH_MANAGER_SERVICE = "texttospeech"; + /** * Official published name of the content capture service. * diff --git a/core/java/android/speech/tts/ITextToSpeechManager.aidl b/core/java/android/speech/tts/ITextToSpeechManager.aidl new file mode 100644 index 000000000000..e6b63dff1553 --- /dev/null +++ b/core/java/android/speech/tts/ITextToSpeechManager.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 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.tts; + +import android.speech.tts.ITextToSpeechSessionCallback; + +/** + * TextToSpeechManagerService interface. Allows opening {@link TextToSpeech} session with the + * specified provider proxied by the system service. + * + * @hide + */ +oneway interface ITextToSpeechManager { + void createSession(in String engine, in ITextToSpeechSessionCallback managerCallback); +} diff --git a/core/java/android/speech/tts/ITextToSpeechSession.aidl b/core/java/android/speech/tts/ITextToSpeechSession.aidl new file mode 100644 index 000000000000..b2afeb0d1ba8 --- /dev/null +++ b/core/java/android/speech/tts/ITextToSpeechSession.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 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.tts; + +/** + * TextToSpeech session interface. Allows to control remote TTS service session once connected. + * + * @see ITextToSpeechManager + * @see ITextToSpeechSessionCallback + * + * {@hide} + */ +oneway interface ITextToSpeechSession { + + /** + * Disconnects the client from the TTS provider. + */ + void disconnect(); +} \ No newline at end of file diff --git a/core/java/android/speech/tts/ITextToSpeechSessionCallback.aidl b/core/java/android/speech/tts/ITextToSpeechSessionCallback.aidl new file mode 100644 index 000000000000..545622a007f3 --- /dev/null +++ b/core/java/android/speech/tts/ITextToSpeechSessionCallback.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 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.tts; +import android.speech.tts.ITextToSpeechSession; + +/** + * Callback interface for a session created by {@link ITextToSpeechManager} API. + * + * @hide + */ +oneway interface ITextToSpeechSessionCallback { + + void onConnected(in ITextToSpeechSession session, in IBinder serviceBinder); + + void onDisconnected(); + + void onError(in String errorInfo); +} \ No newline at end of file diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index 7a185382a631..5d66dc7f29eb 100644 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -35,6 +35,7 @@ import android.os.Bundle; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.os.ServiceManager; import android.text.TextUtils; import android.util.Log; @@ -51,6 +52,7 @@ import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.Set; +import java.util.concurrent.Executor; /** * @@ -695,6 +697,8 @@ public class TextToSpeech { public static final String KEY_FEATURE_NETWORK_RETRIES_COUNT = "networkRetriesCount"; } + private static final boolean DEBUG = false; + private final Context mContext; @UnsupportedAppUsage private Connection mConnectingServiceConnection; @@ -716,6 +720,9 @@ public class TextToSpeech { private final Map mUtterances; private final Bundle mParams = new Bundle(); private final TtsEngines mEnginesHelper; + private final boolean mIsSystem; + @Nullable private final Executor mInitExecutor; + @UnsupportedAppUsage private volatile String mCurrentEngine = null; @@ -758,8 +765,21 @@ public class TextToSpeech { */ public TextToSpeech(Context context, OnInitListener listener, String engine, String packageName, boolean useFallback) { + this(context, /* initExecutor= */ null, listener, engine, packageName, + useFallback, /* isSystem= */ true); + } + + /** + * Used internally to instantiate TextToSpeech objects. + * + * @hide + */ + private TextToSpeech(Context context, @Nullable Executor initExecutor, + OnInitListener initListener, String engine, String packageName, boolean useFallback, + boolean isSystem) { mContext = context; - mInitListener = listener; + mInitExecutor = initExecutor; + mInitListener = initListener; mRequestedEngine = engine; mUseFallback = useFallback; @@ -768,6 +788,9 @@ public class TextToSpeech { mUtteranceProgressListener = null; mEnginesHelper = new TtsEngines(mContext); + + mIsSystem = isSystem; + initTts(); } @@ -842,10 +865,14 @@ public class TextToSpeech { } private boolean connectToEngine(String engine) { - Connection connection = new Connection(); - Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE); - intent.setPackage(engine); - boolean bound = mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE); + Connection connection; + if (mIsSystem) { + connection = new SystemConnection(); + } else { + connection = new DirectConnection(); + } + + boolean bound = connection.connect(engine); if (!bound) { Log.e(TAG, "Failed to bind to " + engine); return false; @@ -857,11 +884,19 @@ public class TextToSpeech { } private void dispatchOnInit(int result) { - synchronized (mStartLock) { - if (mInitListener != null) { - mInitListener.onInit(result); - mInitListener = null; + Runnable onInitCommand = () -> { + synchronized (mStartLock) { + if (mInitListener != null) { + mInitListener.onInit(result); + mInitListener = null; + } } + }; + + if (mInitExecutor != null) { + mInitExecutor.execute(onInitCommand); + } else { + onInitCommand.run(); } } @@ -2127,13 +2162,17 @@ public class TextToSpeech { return mEnginesHelper.getEngines(); } - private class Connection implements ServiceConnection { + private abstract class Connection implements ServiceConnection { private ITextToSpeechService mService; private SetupConnectionAsyncTask mOnSetupConnectionAsyncTask; private boolean mEstablished; + abstract boolean connect(String engine); + + abstract void disconnect(); + private final ITextToSpeechCallback.Stub mCallback = new ITextToSpeechCallback.Stub() { public void onStop(String utteranceId, boolean isStarted) @@ -2199,11 +2238,6 @@ public class TextToSpeech { }; private class SetupConnectionAsyncTask extends AsyncTask { - private final ComponentName mName; - - public SetupConnectionAsyncTask(ComponentName name) { - mName = name; - } @Override protected Integer doInBackground(Void... params) { @@ -2227,7 +2261,7 @@ public class TextToSpeech { mParams.putString(Engine.KEY_PARAM_VOICE_NAME, defaultVoiceName); } - Log.i(TAG, "Set up connection to " + mName); + Log.i(TAG, "Setting up the connection to TTS engine..."); return SUCCESS; } catch (RemoteException re) { Log.e(TAG, "Error connecting to service, setCallback() failed"); @@ -2249,11 +2283,11 @@ public class TextToSpeech { } @Override - public void onServiceConnected(ComponentName name, IBinder service) { + public void onServiceConnected(ComponentName componentName, IBinder service) { synchronized(mStartLock) { mConnectingServiceConnection = null; - Log.i(TAG, "Connected to " + name); + Log.i(TAG, "Connected to TTS engine"); if (mOnSetupConnectionAsyncTask != null) { mOnSetupConnectionAsyncTask.cancel(false); @@ -2263,7 +2297,7 @@ public class TextToSpeech { mServiceConnection = Connection.this; mEstablished = false; - mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask(name); + mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask(); mOnSetupConnectionAsyncTask.execute(); } } @@ -2277,7 +2311,7 @@ public class TextToSpeech { * * @return true if we cancel mOnSetupConnectionAsyncTask in progress. */ - private boolean clearServiceConnection() { + protected boolean clearServiceConnection() { synchronized(mStartLock) { boolean result = false; if (mOnSetupConnectionAsyncTask != null) { @@ -2295,8 +2329,8 @@ public class TextToSpeech { } @Override - public void onServiceDisconnected(ComponentName name) { - Log.i(TAG, "Asked to disconnect from " + name); + public void onServiceDisconnected(ComponentName componentName) { + Log.i(TAG, "Disconnected from TTS engine"); if (clearServiceConnection()) { /* We need to protect against a rare case where engine * dies just after successful connection - and we process onServiceDisconnected @@ -2308,11 +2342,6 @@ public class TextToSpeech { } } - public void disconnect() { - mContext.unbindService(this); - clearServiceConnection(); - } - public boolean isEstablished() { return mService != null && mEstablished; } @@ -2342,6 +2371,91 @@ public class TextToSpeech { } } + // Currently all the clients are routed through the System connection. Direct connection + // is left for debugging, testing and benchmarking purposes. + // TODO(b/179599071): Remove direct connection once system one is fully tested. + private class DirectConnection extends Connection { + @Override + boolean connect(String engine) { + Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE); + intent.setPackage(engine); + return mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); + } + + @Override + void disconnect() { + mContext.unbindService(this); + clearServiceConnection(); + } + } + + private class SystemConnection extends Connection { + + @Nullable + private volatile ITextToSpeechSession mSession; + + @Override + boolean connect(String engine) { + IBinder binder = ServiceManager.getService(Context.TEXT_TO_SPEECH_MANAGER_SERVICE); + + ITextToSpeechManager manager = ITextToSpeechManager.Stub.asInterface(binder); + + if (manager == null) { + Log.e(TAG, "System service is not available!"); + return false; + } + + if (DEBUG) { + Log.d(TAG, "Connecting to engine: " + engine); + } + + try { + manager.createSession(engine, new ITextToSpeechSessionCallback.Stub() { + @Override + public void onConnected(ITextToSpeechSession session, IBinder serviceBinder) { + mSession = session; + onServiceConnected( + /* componentName= */ null, + serviceBinder); + } + + @Override + public void onDisconnected() { + onServiceDisconnected(/* componentName= */ null); + } + + @Override + public void onError(String errorInfo) { + Log.w(TAG, "System TTS connection error: " + errorInfo); + // The connection was not established successfully - handle as + // disconnection: clear the state and notify the user. + onServiceDisconnected(/* componentName= */ null); + } + }); + + return true; + } catch (RemoteException ex) { + Log.e(TAG, "Error communicating with the System Server: ", ex); + throw ex.rethrowFromSystemServer(); + } + } + + @Override + void disconnect() { + ITextToSpeechSession session = mSession; + + if (session != null) { + try { + session.disconnect(); + } catch (RemoteException ex) { + Log.w(TAG, "Error disconnecting session", ex); + } + + clearServiceConnection(); + } + } + } + private interface Action { R run(ITextToSpeechService service) throws RemoteException; } diff --git a/services/Android.bp b/services/Android.bp index 1970b7d7b0dd..dee86ce535a3 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -32,6 +32,7 @@ filegroup { ":services.startop.iorap-sources", ":services.systemcaptions-sources", ":services.translation-sources", + ":services.texttospeech-sources", ":services.usage-sources", ":services.usb-sources", ":services.voiceinteraction-sources", @@ -81,6 +82,7 @@ java_library { "services.startop", "services.systemcaptions", "services.translation", + "services.texttospeech", "services.usage", "services.usb", "services.voiceinteraction", diff --git a/services/core/java/com/android/server/infra/AbstractMasterSystemService.java b/services/core/java/com/android/server/infra/AbstractMasterSystemService.java index c4c0f688bd67..bd577f35f0a5 100644 --- a/services/core/java/com/android/server/infra/AbstractMasterSystemService.java +++ b/services/core/java/com/android/server/infra/AbstractMasterSystemService.java @@ -371,6 +371,9 @@ public abstract class AbstractMasterSystemService { + + private static final String TAG = TextToSpeechManagerPerUserService.class.getSimpleName(); + + TextToSpeechManagerPerUserService( + @NonNull TextToSpeechManagerService master, + @NonNull Object lock, @UserIdInt int userId) { + super(master, lock, userId); + } + + void createSessionLocked(String engine, ITextToSpeechSessionCallback sessionCallback) { + TextToSpeechSessionConnection.start(getContext(), mUserId, engine, sessionCallback); + } + + @GuardedBy("mLock") + @Override // from PerUserSystemService + @NonNull + protected ServiceInfo newServiceInfoLocked( + @SuppressWarnings("unused") @NonNull ComponentName serviceComponent) + throws PackageManager.NameNotFoundException { + try { + return AppGlobals.getPackageManager().getServiceInfo(serviceComponent, + PackageManager.GET_META_DATA, mUserId); + } catch (RemoteException e) { + throw new PackageManager.NameNotFoundException( + "Could not get service for " + serviceComponent); + } + } + + private static class TextToSpeechSessionConnection extends + ServiceConnector.Impl { + + private final String mEngine; + private final ITextToSpeechSessionCallback mCallback; + private final DeathRecipient mUnbindOnDeathHandler; + + static void start(Context context, @UserIdInt int userId, String engine, + ITextToSpeechSessionCallback callback) { + new TextToSpeechSessionConnection(context, userId, engine, callback).start(); + } + + private TextToSpeechSessionConnection(Context context, @UserIdInt int userId, String engine, + ITextToSpeechSessionCallback callback) { + super(context, + new Intent(TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE).setPackage(engine), + Context.BIND_AUTO_CREATE, + userId, + ITextToSpeechService.Stub::asInterface); + mEngine = engine; + mCallback = callback; + mUnbindOnDeathHandler = () -> unbindEngine("client process death is reported"); + } + + private void start() { + Slog.d(TAG, "Trying to start connection to TTS engine: " + mEngine); + + connect() + .thenAccept( + serviceBinder -> { + if (serviceBinder != null) { + Slog.d(TAG, + "Connected successfully to TTS engine: " + mEngine); + try { + mCallback.onConnected(new ITextToSpeechSession.Stub() { + @Override + public void disconnect() { + unbindEngine("client disconnection request"); + } + }, serviceBinder.asBinder()); + + mCallback.asBinder().linkToDeath(mUnbindOnDeathHandler, 0); + } catch (RemoteException ex) { + Slog.w(TAG, "Error notifying the client on connection", ex); + + unbindEngine( + "failed communicating with the client - process " + + "is dead"); + } + } else { + Slog.w(TAG, "Failed to obtain TTS engine binder"); + runSessionCallbackMethod( + () -> mCallback.onError("Failed creating TTS session")); + } + }) + .exceptionally(ex -> { + Slog.w(TAG, "TTS engine binding error", ex); + runSessionCallbackMethod( + () -> mCallback.onError( + "Failed creating TTS session: " + ex.getCause())); + + return null; + }); + } + + @Override // from ServiceConnector.Impl + protected void onServiceConnectionStatusChanged( + ITextToSpeechService service, boolean connected) { + if (!connected) { + Slog.w(TAG, "Disconnected from TTS engine"); + runSessionCallbackMethod(mCallback::onDisconnected); + + try { + mCallback.asBinder().unlinkToDeath(mUnbindOnDeathHandler, 0); + } catch (NoSuchElementException ex) { + Slog.d(TAG, "The death recipient was not linked."); + } + } + } + + @Override // from ServiceConnector.Impl + protected long getAutoDisconnectTimeoutMs() { + return PERMANENT_BOUND_TIMEOUT_MS; + } + + private void unbindEngine(String reason) { + Slog.d(TAG, "Unbinding TTS engine: " + mEngine + ". Reason: " + reason); + unbind(); + } + } + + static void runSessionCallbackMethod(ThrowingRunnable callbackRunnable) { + try { + callbackRunnable.runOrThrow(); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed running callback method", ex); + } + } + + interface ThrowingRunnable { + void runOrThrow() throws RemoteException; + } +} diff --git a/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerService.java b/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerService.java new file mode 100644 index 000000000000..9015563f439e --- /dev/null +++ b/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerService.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2020 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 com.android.server.texttospeech; + +import static com.android.server.texttospeech.TextToSpeechManagerPerUserService.runSessionCallbackMethod; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.content.Context; +import android.os.UserHandle; +import android.speech.tts.ITextToSpeechManager; +import android.speech.tts.ITextToSpeechSessionCallback; + +import com.android.server.infra.AbstractMasterSystemService; + + +/** + * A service that allows secured synthesizing of text to speech audio. Upon request creates a + * session + * that is managed by {@link TextToSpeechManagerPerUserService}. + * + * @see ITextToSpeechManager + */ +public final class TextToSpeechManagerService extends + AbstractMasterSystemService { + + private static final String TAG = TextToSpeechManagerService.class.getSimpleName(); + + public TextToSpeechManagerService(@NonNull Context context) { + super(context, /* serviceNameResolver= */ null, + /* disallowProperty = */null); + } + + @Override // from SystemService + public void onStart() { + publishBinderService(Context.TEXT_TO_SPEECH_MANAGER_SERVICE, + new TextToSpeechManagerServiceStub()); + } + + @Override + protected TextToSpeechManagerPerUserService newServiceLocked( + @UserIdInt int resolvedUserId, boolean disabled) { + return new TextToSpeechManagerPerUserService(this, mLock, resolvedUserId); + } + + private final class TextToSpeechManagerServiceStub extends ITextToSpeechManager.Stub { + @Override + public void createSession(String engine, + ITextToSpeechSessionCallback sessionCallback) { + synchronized (mLock) { + TextToSpeechManagerPerUserService perUserService = getServiceForUserLocked( + UserHandle.getCallingUserId()); + if (perUserService != null) { + perUserService.createSessionLocked(engine, sessionCallback); + } else { + runSessionCallbackMethod( + () -> sessionCallback.onError("Service is not available for user")); + } + } + } + } +} -- cgit v1.2.3-59-g8ed1b