1/*
2 * Copyright (C) 2011 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
17package com.android.settings.tts;
18
19import android.app.AlertDialog;
20import android.content.Context;
21import android.content.DialogInterface;
22import android.content.Intent;
23import android.os.Bundle;
24import android.preference.Preference;
25import android.preference.PreferenceActivity;
26import android.speech.tts.TextToSpeech.EngineInfo;
27import android.util.Log;
28import android.view.View;
29import android.view.ViewGroup;
30import android.widget.Checkable;
31import android.widget.CompoundButton;
32import android.widget.RadioButton;
33
34
35import com.android.settings.R;
36import com.android.settings.Utils;
37
38
39public class TtsEnginePreference extends Preference {
40
41    private static final String TAG = "TtsEnginePreference";
42
43    /**
44     * Key for the name of the TTS engine passed in to the engine
45     * settings fragment {@link TtsEngineSettingsFragment}.
46     */
47    static final String FRAGMENT_ARGS_NAME = "name";
48
49    /**
50     * Key for the label of the TTS engine passed in to the engine
51     * settings fragment. This is used as the title of the fragment
52     * {@link TtsEngineSettingsFragment}.
53     */
54    static final String FRAGMENT_ARGS_LABEL = "label";
55
56    /**
57     * Key for the voice data data passed in to the engine settings
58     * fragmetn {@link TtsEngineSettingsFragment}.
59     */
60    static final String FRAGMENT_ARGS_VOICES = "voices";
61
62    /**
63     * The preference activity that owns this preference. Required
64     * for instantiating the engine specific settings screen.
65     */
66    private final PreferenceActivity mPreferenceActivity;
67
68    /**
69     * The engine information for the engine this preference represents.
70     * Contains it's name, label etc. which are used for display.
71     */
72    private final EngineInfo mEngineInfo;
73
74    /**
75     * The shared radio button state, which button is checked etc.
76     */
77    private final RadioButtonGroupState mSharedState;
78
79    /**
80     * When true, the change callbacks on the radio button will not
81     * fire.
82     */
83    private volatile boolean mPreventRadioButtonCallbacks;
84
85    private View mSettingsIcon;
86    private RadioButton mRadioButton;
87    private Intent mVoiceCheckData;
88
89    private final CompoundButton.OnCheckedChangeListener mRadioChangeListener =
90        new CompoundButton.OnCheckedChangeListener() {
91            @Override
92            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
93                onRadioButtonClicked(buttonView, isChecked);
94            }
95        };
96
97    public TtsEnginePreference(Context context, EngineInfo info, RadioButtonGroupState state,
98            PreferenceActivity prefActivity) {
99        super(context);
100        setLayoutResource(R.layout.preference_tts_engine);
101
102        mSharedState = state;
103        mPreferenceActivity = prefActivity;
104        mEngineInfo = info;
105        mPreventRadioButtonCallbacks = false;
106
107        setKey(mEngineInfo.name);
108        setTitle(mEngineInfo.label);
109    }
110
111    @Override
112    public View getView(View convertView, ViewGroup parent) {
113        if (mSharedState == null) {
114            throw new IllegalStateException("Call to getView() before a call to" +
115                    "setSharedState()");
116        }
117
118        View view = super.getView(convertView, parent);
119        final RadioButton rb = (RadioButton) view.findViewById(R.id.tts_engine_radiobutton);
120        rb.setOnCheckedChangeListener(mRadioChangeListener);
121
122        boolean isChecked = getKey().equals(mSharedState.getCurrentKey());
123        if (isChecked) {
124            mSharedState.setCurrentChecked(rb);
125        }
126
127        mPreventRadioButtonCallbacks = true;
128        rb.setChecked(isChecked);
129        mPreventRadioButtonCallbacks = false;
130
131        mRadioButton = rb;
132
133        View textLayout = view.findViewById(R.id.tts_engine_pref_text);
134        textLayout.setOnClickListener(new View.OnClickListener() {
135            @Override
136            public void onClick(View v) {
137                onRadioButtonClicked(rb, !rb.isChecked());
138            }
139        });
140
141        mSettingsIcon = view.findViewById(R.id.tts_engine_settings);
142        // Will be enabled only the engine has passed the voice check, and
143        // is currently enabled.
144        mSettingsIcon.setEnabled(isChecked && mVoiceCheckData != null);
145        if (!isChecked) {
146            mSettingsIcon.setAlpha(Utils.DISABLED_ALPHA);
147        }
148        mSettingsIcon.setOnClickListener(new View.OnClickListener() {
149            @Override
150            public void onClick(View v) {
151                Bundle args = new Bundle();
152                args.putString(FRAGMENT_ARGS_NAME, mEngineInfo.name);
153                args.putString(FRAGMENT_ARGS_LABEL, mEngineInfo.label);
154                if (mVoiceCheckData != null) {
155                    args.putParcelable(FRAGMENT_ARGS_VOICES, mVoiceCheckData);
156                }
157
158                // Note that we use this instead of the (easier to use)
159                // PreferenceActivity.startPreferenceFragment because the
160                // title will not be updated correctly in the fragment
161                // breadcrumb since it isn't inflated from the XML layout.
162                mPreferenceActivity.startPreferencePanel(
163                        TtsEngineSettingsFragment.class.getName(),
164                        args, 0, mEngineInfo.label, null, 0);
165            }
166        });
167
168        if (mVoiceCheckData != null) {
169            mSettingsIcon.setEnabled(mRadioButton.isChecked());
170        }
171
172        return view;
173    }
174
175    public void setVoiceDataDetails(Intent data) {
176        mVoiceCheckData = data;
177        // This might end up running before getView aboive, in which
178        // case mSettingsIcon && mRadioButton will be null. In this case
179        // getView will set the right values.
180        if (mSettingsIcon != null && mRadioButton != null) {
181            if (mRadioButton.isChecked()) {
182                mSettingsIcon.setEnabled(true);
183            } else {
184                mSettingsIcon.setEnabled(false);
185                mSettingsIcon.setAlpha(Utils.DISABLED_ALPHA);
186            }
187        }
188    }
189
190    private boolean shouldDisplayDataAlert() {
191        return !mEngineInfo.system;
192    }
193
194
195    private void displayDataAlert(
196            DialogInterface.OnClickListener positiveOnClickListener,
197            DialogInterface.OnClickListener negativeOnClickListener) {
198        Log.i(TAG, "Displaying data alert for :" + mEngineInfo.name);
199
200        AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
201        builder.setTitle(android.R.string.dialog_alert_title);
202        builder.setIconAttribute(android.R.attr.alertDialogIcon);
203        builder.setMessage(getContext().getString(
204                R.string.tts_engine_security_warning, mEngineInfo.label));
205        builder.setCancelable(true);
206        builder.setPositiveButton(android.R.string.ok, positiveOnClickListener);
207        builder.setNegativeButton(android.R.string.cancel, negativeOnClickListener);
208
209        AlertDialog dialog = builder.create();
210        dialog.show();
211    }
212
213
214    private void onRadioButtonClicked(final CompoundButton buttonView,
215            boolean isChecked) {
216        if (mPreventRadioButtonCallbacks ||
217                (mSharedState.getCurrentChecked() == buttonView)) {
218            return;
219        }
220
221        if (isChecked) {
222            // Should we alert user? if that's true, delay making engine current one.
223            if (shouldDisplayDataAlert()) {
224                displayDataAlert(new DialogInterface.OnClickListener() {
225                    @Override
226                    public void onClick(DialogInterface dialog, int which) {
227                        makeCurrentEngine(buttonView);
228                    }
229                },new DialogInterface.OnClickListener() {
230                    @Override
231                    public void onClick(DialogInterface dialog, int which) {
232                        // Undo the click.
233                        buttonView.setChecked(false);
234                    }
235                });
236            } else {
237                // Privileged engine, set it current
238                makeCurrentEngine(buttonView);
239            }
240        } else {
241            mSettingsIcon.setEnabled(false);
242        }
243    }
244
245    private void makeCurrentEngine(Checkable current) {
246        if (mSharedState.getCurrentChecked() != null) {
247            mSharedState.getCurrentChecked().setChecked(false);
248        }
249        mSharedState.setCurrentChecked(current);
250        mSharedState.setCurrentKey(getKey());
251        callChangeListener(mSharedState.getCurrentKey());
252        mSettingsIcon.setEnabled(true);
253    }
254
255
256    /**
257     * Holds all state that is common to this group of radio buttons, such
258     * as the currently selected key and the currently checked compound button.
259     * (which corresponds to this key).
260     */
261    public interface RadioButtonGroupState {
262        String getCurrentKey();
263        Checkable getCurrentChecked();
264
265        void setCurrentKey(String key);
266        void setCurrentChecked(Checkable current);
267    }
268
269}
270