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.inputmethod.leanback.voice;
18 
19 import com.android.inputmethod.leanback.LeanbackUtils;
20 import com.android.inputmethod.leanback.R;
21 
22 import android.content.Context;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.util.AttributeSet;
26 import android.util.Log;
27 import android.view.LayoutInflater;
28 import android.view.View;
29 import android.view.accessibility.AccessibilityNodeInfo;
30 import android.widget.ImageView;
31 import android.widget.RelativeLayout;
32 
33 /**
34  * Displays the recognizer status.
35  * This mView includes a {@link BitmapSoundLevelsView} to display the recording value.
36  */
37 public class RecognizerView extends RelativeLayout {
38     private static final boolean DEBUG = false;
39     private static final String TAG = "RecognizerView";
40 
41     private BitmapSoundLevelView mSoundLevels;
42     protected ImageView mMicButton;
43 
44     private Callback mCallback;
45 
46     private State mState;
47 
48     private boolean mEnabled;
49 
50     private enum State {
51         NOT_LISTENING,
52         MIC_INITIALIZING,
53         LISTENING,
54         RECORDING,
55         RECOGNIZING,
56     }
57 
RecognizerView(Context context, AttributeSet attrs, int defStyle)58     public RecognizerView(Context context, AttributeSet attrs, int defStyle) {
59         super(context, attrs, defStyle);
60     }
61 
RecognizerView(Context context, AttributeSet attrs)62     public RecognizerView(Context context, AttributeSet attrs) {
63         super(context, attrs);
64     }
65 
RecognizerView(Context context)66     public RecognizerView(Context context) {
67         super(context);
68     }
69 
70     @Override
onFinishInflate()71     public void onFinishInflate() {
72         LayoutInflater inflater = LayoutInflater.from(getContext());
73         inflater.inflate(R.layout.recognizer_view, this, true);
74 
75         mSoundLevels = (BitmapSoundLevelView) findViewById(R.id.microphone);
76         mMicButton = (ImageView) findViewById(R.id.recognizer_mic_button);
77 
78         mState = State.NOT_LISTENING;
79     }
80 
getMicButton()81     public View getMicButton() {
82         return mMicButton;
83     }
84 
onClick()85     public void onClick() {
86         if (DEBUG) Log.v(TAG, "onClick " + mState);
87         switch (mState) {
88             case MIC_INITIALIZING:
89                 if (DEBUG)
90                     Log.d(TAG, "Ignore #onClick as mic is initializing");
91                 return;
92             case LISTENING:
93                 mCallback.onCancelRecordingClicked();
94                 break;
95             case RECORDING:
96                 mCallback.onStopRecordingClicked();
97                 break;
98             case RECOGNIZING:
99                 mCallback.onCancelRecordingClicked();
100                 break;
101             case NOT_LISTENING:
102                 mCallback.onStartRecordingClicked();
103                 break;
104             default:
105                 return;
106         }
107     }
108 
109     @Override
onAttachedToWindow()110     public void onAttachedToWindow() {
111         super.onAttachedToWindow();
112 
113         // When the mView is attached to a window, a callback has to be already set.
114         //
115         // This isn't true when this mView is used in the intent API layout. When the user hits
116         // retry, we reattach the layout to the window, which is being shown. onAttachedToWindow( )
117         // will be called from inside setContentView( ) and before we can attach a callback to it.
118         // We ensure that the callback is not used when not set.
119         //
120         // Preconditions.checkNotNull(mCallback);
121 
122         // The callbacks from the microphone happen before the mView is attached to the window.
123         // I need to investigate how to change the code to avoid it.
124         refreshUi();
125     }
126 
showRecording()127     public void showRecording() {
128         updateState(State.RECORDING);
129     }
130 
showListening()131     public void showListening() {
132         updateState(State.LISTENING);
133     }
134 
showNotListening()135     public void showNotListening() {
136         updateState(State.NOT_LISTENING);
137     }
138 
showRecognizing()139     public void showRecognizing() {
140         updateState(State.RECOGNIZING);
141     }
142 
setCallback(final Callback callback)143     public void setCallback(final Callback callback) {
144         mCallback = callback;
145     }
146 
updateState(State newState)147     private void updateState(State newState) {
148         if (DEBUG) Log.d(TAG, mState + " -> " + newState);
149         mState = newState;
150         refreshUi();
151     }
152 
setSpeechLevelSource(SpeechLevelSource source)153     public void setSpeechLevelSource(SpeechLevelSource source) {
154         mSoundLevels.setLevelSource(source);
155     }
156 
157     @Override
onSaveInstanceState()158     public Parcelable onSaveInstanceState() {
159         SavedState ss = new SavedState(super.onSaveInstanceState());
160         ss.mState = mState;
161         return ss;
162     }
163 
164     @Override
onRestoreInstanceState(Parcelable state)165     public void onRestoreInstanceState(Parcelable state) {
166         if (!(state instanceof SavedState)) {
167             super.onRestoreInstanceState(state);
168             return;
169         }
170 
171         SavedState ss = (SavedState) state;
172         super.onRestoreInstanceState(ss.getSuperState());
173 
174         mState = ss.mState;
175     }
176 
refreshUi()177     protected void refreshUi() {
178         if (!mEnabled) {
179             return;
180         }
181         switch (mState) {
182             case MIC_INITIALIZING:
183                 mMicButton.setImageResource(R.drawable.vs_micbtn_on_selector);
184                 mSoundLevels.setEnabled(false);
185                 break;
186             case LISTENING:
187                 mMicButton.setImageResource(R.drawable.vs_micbtn_on_selector);
188                 mSoundLevels.setEnabled(true);
189                 break;
190             case RECORDING:
191                 mMicButton.setImageResource(R.drawable.vs_micbtn_rec_selector);
192                 mSoundLevels.setEnabled(true);
193                 break;
194             case NOT_LISTENING:
195                 mMicButton.setImageResource(R.drawable.vs_micbtn_off_selector);
196                 mSoundLevels.setEnabled(false);
197                 break;
198             case RECOGNIZING:
199                 mMicButton.setImageResource(R.drawable.vs_micbtn_off_selector);
200                 mSoundLevels.setEnabled(false);
201                 break;
202         }
203     }
204 
setMicFocused(boolean hasFocus)205     public void setMicFocused(boolean hasFocus) {
206         if (mEnabled) {
207             if (hasFocus) {
208                 mMicButton.setImageResource(R.drawable.ic_voice_focus);
209             } else {
210                 mMicButton.setImageResource(R.drawable.ic_voice_available);
211             }
212 
213             LeanbackUtils.sendAccessibilityEvent(mMicButton, hasFocus);
214         }
215     }
216 
setMicEnabled(boolean enabled)217     public void setMicEnabled(boolean enabled) {
218         mEnabled = enabled;
219         if (enabled) {
220             mMicButton.setAlpha(1.0f);
221             mMicButton.setImageResource(R.drawable.ic_voice_available);
222         } else {
223             mMicButton.setAlpha(0.1f);
224             mMicButton.setImageResource(R.drawable.ic_voice_off);
225         }
226     }
227 
showInitializingMic()228     public void showInitializingMic() {
229         updateState(State.MIC_INITIALIZING);
230     }
231 
232     public interface Callback {
onStartRecordingClicked()233         void onStartRecordingClicked();
onStopRecordingClicked()234         void onStopRecordingClicked();
onCancelRecordingClicked()235         void onCancelRecordingClicked();
236     }
237 
238     public static class SavedState extends View.BaseSavedState {
239         State mState;
240 
SavedState(Parcelable superState)241         public SavedState(Parcelable superState) {
242             super(superState);
243         }
244 
245         @Override
writeToParcel(Parcel out, int flags)246         public void writeToParcel(Parcel out, int flags) {
247             super.writeToParcel(out, flags);
248             out.writeString(mState.toString());
249         }
250 
251         @SuppressWarnings("hiding")
252         public static final Parcelable.Creator<SavedState> CREATOR
253         = new Parcelable.Creator<SavedState>() {
254 
255             @Override
256             public SavedState createFromParcel(Parcel in) {
257                 return new SavedState(in);
258             }
259 
260             @Override
261             public SavedState[] newArray(int size) {
262                 return new SavedState[size];
263             }
264         };
265 
SavedState(Parcel in)266         private SavedState(Parcel in) {
267             super(in);
268             mState = State.valueOf(in.readString());
269         }
270     }
271 }
272