Merge "    Add a system TextToSpeech implementation that initiates the connection through the system server." into sc-dev
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 5166943..10b00f2 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4583,6 +4583,14 @@
     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 0000000..e6b63df
--- /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 0000000..b2afeb0
--- /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 0000000..545622a
--- /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 7a18538..5d66dc7 100644
--- a/core/java/android/speech/tts/TextToSpeech.java
+++ b/core/java/android/speech/tts/TextToSpeech.java
@@ -35,6 +35,7 @@
 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.Map;
 import java.util.MissingResourceException;
 import java.util.Set;
+import java.util.concurrent.Executor;
 
 /**
  *
@@ -695,6 +697,8 @@
         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 @@
     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 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 @@
         mUtteranceProgressListener = null;
 
         mEnginesHelper = new TtsEngines(mContext);
+
+        mIsSystem = isSystem;
+
         initTts();
     }
 
@@ -842,10 +865,14 @@
     }
 
     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 @@
     }
 
     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 @@
         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 @@
                 };
 
         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 @@
                             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 @@
         }
 
         @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 @@
                 mServiceConnection = Connection.this;
 
                 mEstablished = false;
-                mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask(name);
+                mOnSetupConnectionAsyncTask = new SetupConnectionAsyncTask();
                 mOnSetupConnectionAsyncTask.execute();
             }
         }
@@ -2277,7 +2311,7 @@
          *
          * @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 @@
         }
 
         @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 void disconnect() {
-            mContext.unbindService(this);
-            clearServiceConnection();
-        }
-
         public boolean isEstablished() {
             return mService != null && mEstablished;
         }
@@ -2342,6 +2371,91 @@
         }
     }
 
+    // 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 61591c2..3154628 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -33,6 +33,7 @@
         ":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 @@
         "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 c4c0f68..bd577f3 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 @@
             int durationMs) {
         Slog.i(mTag, "setTemporaryService(" + userId + ") to " + componentName + " for "
                 + durationMs + "ms");
+        if (mServiceNameResolver == null) {
+            return;
+        }
         enforceCallingPermissionForManagement();
 
         Objects.requireNonNull(componentName);
@@ -404,6 +407,9 @@
         enforceCallingPermissionForManagement();
 
         synchronized (mLock) {
+            if (mServiceNameResolver == null) {
+                return false;
+            }
             final boolean changed = mServiceNameResolver.setDefaultServiceEnabled(userId, enabled);
             if (!changed) {
                 if (verbose) {
@@ -434,6 +440,10 @@
     public final boolean isDefaultServiceEnabled(@UserIdInt int userId) {
         enforceCallingPermissionForManagement();
 
+        if (mServiceNameResolver == null) {
+            return false;
+        }
+
         synchronized (mLock) {
             return mServiceNameResolver.isDefaultServiceEnabled(userId);
         }
@@ -958,6 +968,10 @@
             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 2b09d12..dd2dd81 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -326,6 +326,8 @@
             "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 @@
             startAttentionService(context, t);
             startRotationResolverService(context, t);
             startSystemCaptionsManagerService(context, t);
+            startTextToSpeechManagerService(context, t);
 
             // System Speech Recognition Service
             if (deviceHasConfigString(context,
@@ -2918,6 +2921,13 @@
         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 0000000..bacc932
--- /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 0000000..f805904
--- /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 0000000..9015563
--- /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"));
+                }
+            }
+        }
+    }
+}