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.annotation.VisibleForTesting; 29 import androidx.preference.PreferenceGroup; 30 31 import com.android.car.settings.R; 32 import com.android.car.settings.common.FragmentController; 33 import com.android.car.settings.common.Logger; 34 import com.android.car.settings.common.PreferenceController; 35 import com.android.car.ui.preference.CarUiPreference; 36 37 /** Populates the possible tts engines to set as the preferred engine. */ 38 public class PreferredEngineOptionsPreferenceController extends 39 PreferenceController<PreferenceGroup> { 40 41 private static final Logger LOG = new Logger(PreferredEngineOptionsPreferenceController.class); 42 43 private final TtsEngines mEnginesHelper; 44 private String mPreviousEngine; 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 = createEnginesHelper(); 51 } 52 53 @Override getPreferenceType()54 protected Class<PreferenceGroup> getPreferenceType() { 55 return PreferenceGroup.class; 56 } 57 58 /** 59 * Creates the initial TTS object and constructs the related preferences when underlying 60 * fragment is created. 61 */ 62 @Override onCreateInternal()63 protected void onCreateInternal() { 64 mTts = createTts(/* listener= */ null, /* engine= */ null); 65 66 for (TextToSpeech.EngineInfo engine : mEnginesHelper.getEngines()) { 67 CarUiPreference preference = new CarUiPreference(getContext()); 68 preference.setKey(engine.name); 69 preference.setTitle(engine.label); 70 preference.setShowChevron(false); 71 preference.setOnPreferenceClickListener(pref -> { 72 TextToSpeech.EngineInfo current = mEnginesHelper.getEngineInfo( 73 mTts.getCurrentEngine()); 74 if (areEnginesEqual(current, engine)) { 75 return false; 76 } 77 updateDefaultEngine(engine.name); 78 return true; 79 }); 80 getPreference().addPreference(preference); 81 } 82 } 83 84 /** Cleans up the TTS object and clears the preferences representing the TTS engines. */ 85 @Override onDestroyInternal()86 protected void onDestroyInternal() { 87 if (mTts != null) { 88 mTts.shutdown(); 89 mTts = null; 90 } 91 } 92 93 @Override updateState(PreferenceGroup preference)94 protected void updateState(PreferenceGroup preference) { 95 TextToSpeech.EngineInfo current = mEnginesHelper.getEngineInfo(mTts.getCurrentEngine()); 96 for (int i = 0; i < preference.getPreferenceCount(); i++) { 97 CarUiPreference pref = (CarUiPreference) preference.getPreference(i); 98 if (areEnginesEqual(current, pref.getKey(), pref.getTitle())) { 99 pref.setSummary(R.string.text_to_speech_current_engine); 100 } else { 101 pref.setSummary(""); 102 } 103 } 104 } 105 areEnginesEqual(TextToSpeech.EngineInfo engine1, TextToSpeech.EngineInfo engine2)106 private boolean areEnginesEqual(TextToSpeech.EngineInfo engine1, 107 TextToSpeech.EngineInfo engine2) { 108 return areEnginesEqual(engine1, engine2.name, engine2.label); 109 } 110 areEnginesEqual(TextToSpeech.EngineInfo engine, CharSequence name, CharSequence label)111 private boolean areEnginesEqual(TextToSpeech.EngineInfo engine, CharSequence name, 112 CharSequence label) { 113 return TextUtils.equals(engine.name, name) && TextUtils.equals(engine.label, label); 114 } 115 updateDefaultEngine(String engineName)116 private void updateDefaultEngine(String engineName) { 117 LOG.d("Updating default synth to : " + engineName); 118 119 // Keep track of the previous engine that was being used. So that 120 // we can reuse the previous engine. 121 // 122 // Note that if TextToSpeech#getCurrentEngine is not null, it means at 123 // the very least that we successfully bound to the engine service. 124 mPreviousEngine = mTts.getCurrentEngine(); 125 126 // Step 1: Shut down the existing TTS engine. 127 LOG.i("Shutting down current tts engine"); 128 if (mTts != null) { 129 mTts.shutdown(); 130 } 131 132 // Step 2: Connect to the new TTS engine. 133 // Step 3 is continued on #onUpdateEngine (below) which is called when 134 // the app binds successfully to the engine. 135 LOG.i("Updating engine : Attempting to connect to engine: " + engineName); 136 mTts = createTts(status -> { 137 if (isStarted()) { 138 onUpdateEngine(status); 139 refreshUi(); 140 } 141 }, engineName); 142 LOG.i("Success"); 143 } 144 145 /** 146 * We have now bound to the TTS engine the user requested. We will attempt to check voice data 147 * for the engine if we successfully bound to it, or revert to the previous engine if we 148 * didn't. 149 */ 150 @VisibleForTesting onUpdateEngine(int status)151 void onUpdateEngine(int status) { 152 if (status == TextToSpeech.SUCCESS) { 153 LOG.d("Updating engine: Successfully bound to the engine: " 154 + mTts.getCurrentEngine()); 155 Settings.Secure.putString(getContext().getContentResolver(), TTS_DEFAULT_SYNTH, 156 mTts.getCurrentEngine()); 157 } else { 158 LOG.d("Updating engine: Failed to bind to engine, reverting."); 159 if (mPreviousEngine != null) { 160 // This is guaranteed to at least bind, since mPreviousEngine would be 161 // null if the previous bind to this engine failed. 162 mTts = createTts(/* listener= */ null, mPreviousEngine); 163 } 164 mPreviousEngine = null; 165 } 166 } 167 168 @VisibleForTesting createEnginesHelper()169 TtsEngines createEnginesHelper() { 170 return new TtsEngines(getContext()); 171 } 172 173 @VisibleForTesting createTts(TextToSpeech.OnInitListener listener, String engine)174 TextToSpeech createTts(TextToSpeech.OnInitListener listener, String engine) { 175 return new TextToSpeech(getContext(), listener, engine); 176 } 177 } 178