1/*
2 * Copyright (C) 2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.voice;
18
19import java.io.ByteArrayOutputStream;
20import java.nio.ByteBuffer;
21import java.nio.ByteOrder;
22import java.nio.ShortBuffer;
23import java.util.ArrayList;
24import java.util.List;
25
26import android.content.ContentResolver;
27import android.content.Context;
28import android.content.res.Resources;
29import android.graphics.Bitmap;
30import android.graphics.Canvas;
31import android.graphics.CornerPathEffect;
32import android.graphics.Paint;
33import android.graphics.Path;
34import android.graphics.PathEffect;
35import android.graphics.drawable.Drawable;
36import android.os.Handler;
37import android.util.TypedValue;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.View.OnClickListener;
41import android.view.ViewGroup.MarginLayoutParams;
42import android.widget.ImageView;
43import android.widget.ProgressBar;
44import android.widget.TextView;
45
46import com.android.inputmethod.latin.R;
47
48/**
49 * The user interface for the "Speak now" and "working" states.
50 * Displays a recognition dialog (with waveform, voice meter, etc.),
51 * plays beeps, shows errors, etc.
52 */
53public class RecognitionView {
54    private static final String TAG = "RecognitionView";
55
56    private Handler mUiHandler;  // Reference to UI thread
57    private View mView;
58    private Context mContext;
59
60    private ImageView mImage;
61    private TextView mText;
62    private View mButton;
63    private TextView mButtonText;
64    private View mProgress;
65
66    private Drawable mInitializing;
67    private Drawable mError;
68    private List<Drawable> mSpeakNow;
69
70    private float mVolume = 0.0f;
71    private int mLevel = 0;
72
73    private enum State {LISTENING, WORKING, READY}
74    private State mState = State.READY;
75
76    private float mMinMicrophoneLevel;
77    private float mMaxMicrophoneLevel;
78
79    /** Updates the microphone icon to show user their volume.*/
80    private Runnable mUpdateVolumeRunnable = new Runnable() {
81        public void run() {
82            if (mState != State.LISTENING) {
83                return;
84            }
85
86            final float min = mMinMicrophoneLevel;
87            final float max = mMaxMicrophoneLevel;
88            final int maxLevel = mSpeakNow.size() - 1;
89
90            int index = (int) ((mVolume - min) / (max - min) * maxLevel);
91            final int level = Math.min(Math.max(0, index), maxLevel);
92
93            if (level != mLevel) {
94                mImage.setImageDrawable(mSpeakNow.get(level));
95                mLevel = level;
96            }
97            mUiHandler.postDelayed(mUpdateVolumeRunnable, 50);
98        }
99      };
100
101    public RecognitionView(Context context, OnClickListener clickListener) {
102        mUiHandler = new Handler();
103
104        LayoutInflater inflater = (LayoutInflater) context.getSystemService(
105            Context.LAYOUT_INFLATER_SERVICE);
106        mView = inflater.inflate(R.layout.recognition_status, null);
107        ContentResolver cr = context.getContentResolver();
108        mMinMicrophoneLevel = SettingsUtil.getSettingsFloat(
109                cr, SettingsUtil.LATIN_IME_MIN_MICROPHONE_LEVEL, 15.f);
110        mMaxMicrophoneLevel = SettingsUtil.getSettingsFloat(
111                cr, SettingsUtil.LATIN_IME_MAX_MICROPHONE_LEVEL, 30.f);
112
113        // Pre-load volume level images
114        Resources r = context.getResources();
115
116        mSpeakNow = new ArrayList<Drawable>();
117        mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level0));
118        mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level1));
119        mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level2));
120        mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level3));
121        mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level4));
122        mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level5));
123        mSpeakNow.add(r.getDrawable(R.drawable.speak_now_level6));
124
125        mInitializing = r.getDrawable(R.drawable.mic_slash);
126        mError = r.getDrawable(R.drawable.caution);
127
128        mImage = (ImageView) mView.findViewById(R.id.image);
129        mButton = mView.findViewById(R.id.button);
130        mButton.setOnClickListener(clickListener);
131        mText = (TextView) mView.findViewById(R.id.text);
132        mButtonText = (TextView) mView.findViewById(R.id.button_text);
133        mProgress = mView.findViewById(R.id.progress);
134
135        mContext = context;
136    }
137
138    public View getView() {
139        return mView;
140    }
141
142    public void restoreState() {
143        mUiHandler.post(new Runnable() {
144            public void run() {
145                // Restart the spinner
146                if (mState == State.WORKING) {
147                    ((ProgressBar)mProgress).setIndeterminate(false);
148                    ((ProgressBar)mProgress).setIndeterminate(true);
149                }
150            }
151        });
152    }
153
154    public void showInitializing() {
155        mUiHandler.post(new Runnable() {
156            public void run() {
157                prepareDialog(false, mContext.getText(R.string.voice_initializing), mInitializing,
158                        mContext.getText(R.string.cancel));
159            }
160          });
161    }
162
163    public void showListening() {
164        mUiHandler.post(new Runnable() {
165            public void run() {
166                mState = State.LISTENING;
167                prepareDialog(false, mContext.getText(R.string.voice_listening), mSpeakNow.get(0),
168                        mContext.getText(R.string.cancel));
169            }
170          });
171        mUiHandler.postDelayed(mUpdateVolumeRunnable, 50);
172    }
173
174    public void updateVoiceMeter(final float rmsdB) {
175        mVolume = rmsdB;
176    }
177
178    public void showError(final String message) {
179        mUiHandler.post(new Runnable() {
180            public void run() {
181                mState = State.READY;
182                prepareDialog(false, message, mError, mContext.getText(R.string.ok));
183            }
184          });
185    }
186
187    public void showWorking(
188        final ByteArrayOutputStream waveBuffer,
189        final int speechStartPosition,
190        final int speechEndPosition) {
191
192        mUiHandler.post(new Runnable() {
193            public void run() {
194                mState = State.WORKING;
195                prepareDialog(true, mContext.getText(R.string.voice_working), null, mContext
196                        .getText(R.string.cancel));
197                final ShortBuffer buf = ByteBuffer.wrap(waveBuffer.toByteArray()).order(
198                        ByteOrder.nativeOrder()).asShortBuffer();
199                buf.position(0);
200                waveBuffer.reset();
201                showWave(buf, speechStartPosition / 2, speechEndPosition / 2);
202            }
203          });
204    }
205
206    private void prepareDialog(boolean spinVisible, CharSequence text, Drawable image,
207            CharSequence btnTxt) {
208        if (spinVisible) {
209            mProgress.setVisibility(View.VISIBLE);
210            mImage.setVisibility(View.GONE);
211        } else {
212            mProgress.setVisibility(View.GONE);
213            mImage.setImageDrawable(image);
214            mImage.setVisibility(View.VISIBLE);
215        }
216        mText.setText(text);
217        mButtonText.setText(btnTxt);
218    }
219
220    /**
221     * @return an average abs of the specified buffer.
222     */
223    private static int getAverageAbs(ShortBuffer buffer, int start, int i, int npw) {
224        int from = start + i * npw;
225        int end = from + npw;
226        int total = 0;
227        for (int x = from; x < end; x++) {
228            total += Math.abs(buffer.get(x));
229        }
230        return total / npw;
231    }
232
233
234    /**
235     * Shows waveform of input audio.
236     *
237     * Copied from version in VoiceSearch's RecognitionActivity.
238     *
239     * TODO: adjust stroke width based on the size of data.
240     * TODO: use dip rather than pixels.
241     */
242    private void showWave(ShortBuffer waveBuffer, int startPosition, int endPosition) {
243        final int w = ((View) mImage.getParent()).getWidth();
244        final int h = mImage.getHeight();
245        if (w <= 0 || h <= 0) {
246            // view is not visible this time. Skip drawing.
247            return;
248        }
249        final Bitmap b = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
250        final Canvas c = new Canvas(b);
251        final Paint paint = new Paint();
252        paint.setColor(0xFFFFFFFF); // 0xAARRGGBB
253        paint.setAntiAlias(true);
254        paint.setStyle(Paint.Style.STROKE);
255        paint.setAlpha(0x90);
256
257        final PathEffect effect = new CornerPathEffect(3);
258        paint.setPathEffect(effect);
259
260        final int numSamples = waveBuffer.remaining();
261        int endIndex;
262        if (endPosition == 0) {
263            endIndex = numSamples;
264        } else {
265            endIndex = Math.min(endPosition, numSamples);
266        }
267
268        int startIndex = startPosition - 2000; // include 250ms before speech
269        if (startIndex < 0) {
270            startIndex = 0;
271        }
272        final int numSamplePerWave = 200;  // 8KHz 25ms = 200 samples
273        final float scale = 10.0f / 65536.0f;
274
275        final int count = (endIndex - startIndex) / numSamplePerWave;
276        final float deltaX = 1.0f * w / count;
277        int yMax = h / 2 - 8;
278        Path path = new Path();
279        c.translate(0, yMax);
280        float x = 0;
281        path.moveTo(x, 0);
282        for (int i = 0; i < count; i++) {
283            final int avabs = getAverageAbs(waveBuffer, startIndex, i , numSamplePerWave);
284            int sign = ( (i & 01) == 0) ? -1 : 1;
285            final float y = Math.min(yMax, avabs * h * scale) * sign;
286            path.lineTo(x, y);
287            x += deltaX;
288            path.lineTo(x, y);
289        }
290        if (deltaX > 4) {
291            paint.setStrokeWidth(3);
292        } else {
293            paint.setStrokeWidth(Math.max(1, (int) (deltaX -.05)));
294        }
295        c.drawPath(path, paint);
296        mImage.setImageBitmap(b);
297        mImage.setVisibility(View.VISIBLE);
298        MarginLayoutParams mProgressParams = (MarginLayoutParams)mProgress.getLayoutParams();
299        mProgressParams.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
300                -h , mContext.getResources().getDisplayMetrics());
301
302        // Tweak the padding manually to fill out the whole view horizontally.
303        // TODO: Do this in the xml layout instead.
304        ((View) mImage.getParent()).setPadding(4, ((View) mImage.getParent()).getPaddingTop(), 3,
305                ((View) mImage.getParent()).getPaddingBottom());
306        mProgress.setLayoutParams(mProgressParams);
307    }
308
309
310    public void finish() {
311        mUiHandler.post(new Runnable() {
312            public void run() {
313                mState = State.READY;
314                exitWorking();
315            }
316          });
317    }
318
319    private void exitWorking() {
320        mProgress.setVisibility(View.GONE);
321        mImage.setVisibility(View.VISIBLE);
322    }
323}
324