Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 290d1119 authored by Alex Agranovich's avatar Alex Agranovich
Browse files

Add a system TextToSpeech implementation that initiates the connection...

    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
parent 6809b5c0
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -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.
     *
+29 −0
Original line number Diff line number Diff line
/*
 * 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);
}
+33 −0
Original line number Diff line number Diff line
/*
 * 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
+32 −0
Original line number Diff line number Diff line
/*
 * 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
+141 −27
Original line number Diff line number Diff line
@@ -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,12 +884,20 @@ public class TextToSpeech {
    }

    private void dispatchOnInit(int result) {
        Runnable onInitCommand = () -> {
            synchronized (mStartLock) {
                if (mInitListener != null) {
                    mInitListener.onInit(result);
                    mInitListener = null;
                }
            }
        };

        if (mInitExecutor != null) {
            mInitExecutor.execute(onInitCommand);
        } else {
            onInitCommand.run();
        }
    }

    private IBinder getCallerIdentity() {
@@ -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;
    }
Loading