diff options
11 files changed, 543 insertions, 27 deletions
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 51669432e377..10b00f245d79 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4583,6 +4583,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. * * @hide 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<CharSequence, Uri> 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<Void, Void, Integer> { - 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> { R run(ITextToSpeechService service) throws RemoteException; } diff --git a/services/Android.bp b/services/Android.bp index 61591c2c29bd..315462838485 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -33,6 +33,7 @@ filegroup { ":services.startop.iorap-sources", ":services.systemcaptions-sources", ":services.translation-sources", + ":services.texttospeech-sources", ":services.usage-sources", ":services.usb-sources", ":services.voiceinteraction-sources", @@ -83,6 +84,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<M extends AbstractMasterSystem int durationMs) { Slog.i(mTag, "setTemporaryService(" + userId + ") to " + componentName + " for " + durationMs + "ms"); + if (mServiceNameResolver == null) { + return; + } enforceCallingPermissionForManagement(); Objects.requireNonNull(componentName); @@ -404,6 +407,9 @@ public abstract class AbstractMasterSystemService<M extends AbstractMasterSystem enforceCallingPermissionForManagement(); synchronized (mLock) { + if (mServiceNameResolver == null) { + return false; + } final boolean changed = mServiceNameResolver.setDefaultServiceEnabled(userId, enabled); if (!changed) { if (verbose) { @@ -434,6 +440,10 @@ public abstract class AbstractMasterSystemService<M extends AbstractMasterSystem public final boolean isDefaultServiceEnabled(@UserIdInt int userId) { enforceCallingPermissionForManagement(); + if (mServiceNameResolver == null) { + return false; + } + synchronized (mLock) { return mServiceNameResolver.isDefaultServiceEnabled(userId); } @@ -958,6 +968,10 @@ public abstract class AbstractMasterSystemService<M extends AbstractMasterSystem public void onPackageModified(String packageName) { if (verbose) Slog.v(mTag, "onPackageModified(): " + packageName); + if (mServiceNameResolver == null) { + return; + } + final int userId = getChangingUserId(); final String serviceName = mServiceNameResolver.getDefaultServiceName(userId); if (serviceName == null) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 2b09d12c97c7..dd2dd8150165 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -326,6 +326,8 @@ public final class SystemServer implements Dumpable { "com.android.server.musicrecognition.MusicRecognitionManagerService"; private static final String SYSTEM_CAPTIONS_MANAGER_SERVICE_CLASS = "com.android.server.systemcaptions.SystemCaptionsManagerService"; + private static final String TEXT_TO_SPEECH_MANAGER_SERVICE_CLASS = + "com.android.server.texttospeech.TextToSpeechManagerService"; private static final String TIME_ZONE_RULES_MANAGER_SERVICE_CLASS = "com.android.server.timezone.RulesManagerService$Lifecycle"; private static final String IOT_SERVICE_CLASS = @@ -1713,6 +1715,7 @@ public final class SystemServer implements Dumpable { startAttentionService(context, t); startRotationResolverService(context, t); startSystemCaptionsManagerService(context, t); + startTextToSpeechManagerService(context, t); // System Speech Recognition Service if (deviceHasConfigString(context, @@ -2918,6 +2921,13 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } + private void startTextToSpeechManagerService(@NonNull Context context, + @NonNull TimingsTraceAndSlog t) { + t.traceBegin("StartTextToSpeechManagerService"); + mSystemServiceManager.startService(TEXT_TO_SPEECH_MANAGER_SERVICE_CLASS); + t.traceEnd(); + } + private void startContentCaptureService(@NonNull Context context, @NonNull TimingsTraceAndSlog t) { // First check if it was explicitly enabled by DeviceConfig diff --git a/services/texttospeech/Android.bp b/services/texttospeech/Android.bp new file mode 100644 index 000000000000..bacc932f760f --- /dev/null +++ b/services/texttospeech/Android.bp @@ -0,0 +1,13 @@ +filegroup { + name: "services.texttospeech-sources", + srcs: ["java/**/*.java"], + path: "java", + visibility: ["//frameworks/base/services"], +} + +java_library_static { + name: "services.texttospeech", + defaults: ["platform_service_defaults"], + srcs: [":services.texttospeech-sources"], + libs: ["services.core"], +}
\ No newline at end of file diff --git a/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java b/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java new file mode 100644 index 000000000000..f80590420d09 --- /dev/null +++ b/services/texttospeech/java/com/android/server/texttospeech/TextToSpeechManagerPerUserService.java @@ -0,0 +1,184 @@ +/* + * 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.internal.infra.AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.IBinder.DeathRecipient; +import android.os.RemoteException; +import android.speech.tts.ITextToSpeechService; +import android.speech.tts.ITextToSpeechSession; +import android.speech.tts.ITextToSpeechSessionCallback; +import android.speech.tts.TextToSpeech; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.infra.ServiceConnector; +import com.android.server.infra.AbstractPerUserSystemService; + +import java.util.NoSuchElementException; + +/** + * Manages per-user text to speech session activated by {@link TextToSpeechManagerService}. + * Creates {@link TtsClient} interface object with direct connection to + * {@link android.speech.tts.TextToSpeechService} provider. + * + * @see ITextToSpeechSession + * @see TextToSpeech + */ +final class TextToSpeechManagerPerUserService extends + AbstractPerUserSystemService<TextToSpeechManagerPerUserService, + TextToSpeechManagerService> { + + 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<ITextToSpeechService> { + + 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<TextToSpeechManagerService, + TextToSpeechManagerPerUserService> { + + 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")); + } + } + } + } +} |