1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.settings.tts;
18 
19 import static android.provider.Settings.Secure.TTS_DEFAULT_SYNTH;
20 
21 import android.car.drivingstate.CarUxRestrictions;
22 import android.content.Context;
23 import android.provider.Settings;
24 import android.speech.tts.TextToSpeech;
25 import android.speech.tts.TtsEngines;
26 import android.text.TextUtils;
27 
28 import androidx.preference.PreferenceGroup;
29 
30 import com.android.car.settings.R;
31 import com.android.car.settings.common.FragmentController;
32 import com.android.car.settings.common.Logger;
33 import com.android.car.settings.common.PreferenceController;
34 import com.android.car.ui.preference.CarUiPreference;
35 
36 /** Populates the possible tts engines to set as the preferred engine. */
37 public class PreferredEngineOptionsPreferenceController extends
38         PreferenceController<PreferenceGroup> {
39 
40     private static final Logger LOG = new Logger(PreferredEngineOptionsPreferenceController.class);
41 
42     private final TtsEngines mEnginesHelper;
43     private String mPreviousEngine;
44     private boolean mIsStarted;
45     private TextToSpeech mTts;
46 
PreferredEngineOptionsPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)47     public PreferredEngineOptionsPreferenceController(Context context, String preferenceKey,
48             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
49         super(context, preferenceKey, fragmentController, uxRestrictions);
50         mEnginesHelper = new TtsEngines(getContext());
51         mIsStarted = false;
52     }
53 
54     @Override
getPreferenceType()55     protected Class<PreferenceGroup> getPreferenceType() {
56         return PreferenceGroup.class;
57     }
58 
59     /**
60      * Creates the initial TTS object and constructs the related preferences when underlying
61      * fragment is created.
62      */
63     @Override
onCreateInternal()64     protected void onCreateInternal() {
65         mTts = new TextToSpeech(getContext(), /* listener= */ null);
66 
67         for (TextToSpeech.EngineInfo engine : mEnginesHelper.getEngines()) {
68             CarUiPreference preference = new CarUiPreference(getContext());
69             preference.setKey(engine.name);
70             preference.setTitle(engine.label);
71             preference.setShowChevron(false);
72             preference.setOnPreferenceClickListener(pref -> {
73                 TextToSpeech.EngineInfo current = mEnginesHelper.getEngineInfo(
74                         mTts.getCurrentEngine());
75                 if (TextUtils.equals(engine.label, current.label)) {
76                     return false;
77                 }
78                 updateDefaultEngine(engine.name);
79                 return true;
80             });
81             getPreference().addPreference(preference);
82         }
83     }
84 
85     /** Note that the preference controller was started. */
86     @Override
onStartInternal()87     protected void onStartInternal() {
88         mIsStarted = true;
89     }
90 
91     /** Note that the preference controller was stopped. */
92     @Override
onStopInternal()93     protected void onStopInternal() {
94         mIsStarted = false;
95     }
96 
97     /** Cleans up the TTS object and clears the preferences representing the TTS engines. */
98     @Override
onDestroyInternal()99     protected void onDestroyInternal() {
100         if (mTts != null) {
101             mTts.shutdown();
102             mTts = null;
103         }
104     }
105 
106     @Override
updateState(PreferenceGroup preference)107     protected void updateState(PreferenceGroup preference) {
108         TextToSpeech.EngineInfo current = mEnginesHelper.getEngineInfo(mTts.getCurrentEngine());
109         for (int i = 0; i < preference.getPreferenceCount(); i++) {
110             CarUiPreference pref = (CarUiPreference) preference.getPreference(i);
111             if (pref.getTitle().equals(current.label)) {
112                 pref.setSummary(R.string.text_to_speech_current_engine);
113             } else {
114                 pref.setSummary("");
115             }
116         }
117     }
118 
updateDefaultEngine(String engineName)119     private void updateDefaultEngine(String engineName) {
120         LOG.d("Updating default synth to : " + engineName);
121 
122         // Keep track of the previous engine that was being used. So that
123         // we can reuse the previous engine.
124         //
125         // Note that if TextToSpeech#getCurrentEngine is not null, it means at
126         // the very least that we successfully bound to the engine service.
127         mPreviousEngine = mTts.getCurrentEngine();
128 
129         // Step 1: Shut down the existing TTS engine.
130         LOG.i("Shutting down current tts engine");
131         if (mTts != null) {
132             mTts.shutdown();
133         }
134 
135         // Step 2: Connect to the new TTS engine.
136         // Step 3 is continued on #onUpdateEngine (below) which is called when
137         // the app binds successfully to the engine.
138         LOG.i("Updating engine : Attempting to connect to engine: " + engineName);
139         mTts = new TextToSpeech(getContext(), status -> {
140             if (mIsStarted) {
141                 onUpdateEngine(status);
142                 refreshUi();
143             }
144         }, engineName);
145         LOG.i("Success");
146     }
147 
148     /**
149      * We have now bound to the TTS engine the user requested. We will attempt to check voice data
150      * for the engine if we successfully bound to it, or revert to the previous engine if we
151      * didn't.
152      */
onUpdateEngine(int status)153     private void onUpdateEngine(int status) {
154         if (status == TextToSpeech.SUCCESS) {
155             LOG.d("Updating engine: Successfully bound to the engine: "
156                     + mTts.getCurrentEngine());
157             Settings.Secure.putString(getContext().getContentResolver(), TTS_DEFAULT_SYNTH,
158                     mTts.getCurrentEngine());
159         } else {
160             LOG.d("Updating engine: Failed to bind to engine, reverting.");
161             if (mPreviousEngine != null) {
162                 // This is guaranteed to at least bind, since mPreviousEngine would be
163                 // null if the previous bind to this engine failed.
164                 mTts = new TextToSpeech(getContext(), /* listener= */ null, mPreviousEngine);
165             }
166             mPreviousEngine = null;
167         }
168     }
169 }
170