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.R;
20 
21 import android.animation.ObjectAnimator;
22 import android.animation.TimeAnimator;
23 import android.animation.TimeAnimator.TimeListener;
24 import android.content.Context;
25 import android.content.res.TypedArray;
26 import android.graphics.Bitmap;
27 import android.graphics.BitmapFactory;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Paint;
31 import android.graphics.Rect;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.View;
35 import android.view.accessibility.AccessibilityNodeInfo;
36 
37 /**
38  * Displays the recording value of the microphone.
39  */
40 public class BitmapSoundLevelView extends View {
41     private static final boolean DEBUG = false;
42     private static final String TAG = "BitmapSoundLevelsView";
43 
44     private static final int MIC_PRIMARY_LEVEL_IMAGE_OFFSET = 3;
45     private static final int MIC_LEVEL_GUIDELINE_OFFSET = 13;
46 
47     private final Paint mEmptyPaint = new Paint();
48     private Rect mDestRect;
49 
50     private final int mEnableBackgroundColor;
51     private final int mDisableBackgroundColor;
52 
53     // Generates clock ticks for the animation using the global animation loop.
54     private TimeAnimator mAnimator;
55 
56     private int mCurrentVolume;
57 
58     // Bitmap for the main level meter, most closely follows the mic.
59     private final Bitmap mPrimaryLevel;
60 
61     // Bitmap for trailing level meter, shows a peak level.
62     private final Bitmap mTrailLevel;
63 
64     // The minimum size of the levels, that is the size when volume is 0.
65     private final int mMinimumLevelSize;
66 
67     // A translation to apply to the center of the levels, allows the levels to be offset from
68     // the center of the mView without having to translate the whole mView.
69     private final int mCenterTranslationX;
70     private final int mCenterTranslationY;
71 
72     // Peak level observed, and how many frames left before it starts decaying.
73     private int mPeakLevel;
74     private int mPeakLevelCountDown;
75 
76     // Input level is pulled from here.
77     private SpeechLevelSource mLevelSource;
78 
79     private Paint mPaint;
80 
BitmapSoundLevelView(Context context)81     public BitmapSoundLevelView(Context context) {
82         this(context, null);
83     }
84 
BitmapSoundLevelView(Context context, AttributeSet attrs)85     public BitmapSoundLevelView(Context context, AttributeSet attrs) {
86         this(context, attrs, 0);
87     }
88 
BitmapSoundLevelView(Context context, AttributeSet attrs, int defStyleAttr)89     public BitmapSoundLevelView(Context context, AttributeSet attrs, int defStyleAttr) {
90         super(context, attrs, defStyleAttr);
91 
92         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BitmapSoundLevelView,
93                 defStyleAttr, 0);
94         mEnableBackgroundColor = a.getColor(R.styleable.BitmapSoundLevelView_enabledBackgroundColor,
95                 Color.parseColor("#66FFFFFF"));
96 
97         mDisableBackgroundColor = a.getColor(
98                 R.styleable.BitmapSoundLevelView_disabledBackgroundColor,
99                 Color.WHITE);
100 
101         boolean primaryLevelEnabled = false;
102         boolean peakLevelEnabled = false;
103         int primaryLevelId = 0;
104         if (a.hasValue(R.styleable.BitmapSoundLevelView_primaryLevels)) {
105             primaryLevelId = a.getResourceId(
106                     R.styleable.BitmapSoundLevelView_primaryLevels, R.drawable.vs_reactive_dark);
107             primaryLevelEnabled = true;
108         }
109 
110         int trailLevelId = 0;
111         if (a.hasValue(R.styleable.BitmapSoundLevelView_trailLevels)) {
112             trailLevelId = a.getResourceId(
113                     R.styleable.BitmapSoundLevelView_trailLevels, R.drawable.vs_reactive_light);
114             peakLevelEnabled = true;
115         }
116 
117         mCenterTranslationX = a.getDimensionPixelOffset(
118                 R.styleable.BitmapSoundLevelView_levelsCenterX, 0);
119 
120         mCenterTranslationY = a.getDimensionPixelOffset(
121                 R.styleable.BitmapSoundLevelView_levelsCenterY, 0);
122 
123         mMinimumLevelSize = a.getDimensionPixelOffset(
124                 R.styleable.BitmapSoundLevelView_minLevelRadius, 0);
125 
126         a.recycle();
127 
128         if (primaryLevelEnabled) {
129             mPrimaryLevel = BitmapFactory.decodeResource(getResources(), primaryLevelId);
130         } else {
131             mPrimaryLevel = null;
132         }
133 
134         if (peakLevelEnabled) {
135             mTrailLevel = BitmapFactory.decodeResource(getResources(), trailLevelId);
136         } else {
137             mTrailLevel = null;
138         }
139 
140         mPaint = new Paint();
141 
142         mDestRect = new Rect();
143 
144         mEmptyPaint.setFilterBitmap(true);
145 
146         // Safe source, replaced with system one when attached.
147         mLevelSource = new SpeechLevelSource();
148         mLevelSource.setSpeechLevel(0);
149 
150         // This animator generates ticks that invalidate the
151         // mView so that the animation is synced with the global animation loop.
152         mAnimator = new TimeAnimator();
153         mAnimator.setRepeatCount(ObjectAnimator.INFINITE);
154         mAnimator.setTimeListener(new TimeListener() {
155             @Override
156             public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
157                 invalidate();
158             }
159         });
160     }
161 
162     @Override
setEnabled(boolean enabled)163     public void setEnabled(boolean enabled) {
164         super.setEnabled(enabled);
165         updateAnimatorState();
166     }
167 
updateAnimatorState()168     private void updateAnimatorState() {
169         if (isEnabled()) {
170             startAnimator();
171         } else {
172             stopAnimator();
173         }
174     }
175 
startAnimator()176     private void startAnimator() {
177         if (DEBUG) Log.d(TAG, "startAnimator()");
178         if (!mAnimator.isStarted()) {
179             mAnimator.start();
180         }
181     }
182 
stopAnimator()183     private void stopAnimator() {
184         if (DEBUG) Log.d(TAG, "stopAnimator()");
185         mAnimator.cancel();
186     }
187 
188     @Override
onAttachedToWindow()189     protected void onAttachedToWindow() {
190         super.onAttachedToWindow();
191         updateAnimatorState();
192     }
193 
194     @Override
onDetachedFromWindow()195     protected void onDetachedFromWindow() {
196         stopAnimator();
197         super.onDetachedFromWindow();
198     }
199 
200     @Override
onWindowFocusChanged(boolean hasWindowFocus)201     public void onWindowFocusChanged(boolean hasWindowFocus) {
202         super.onWindowFocusChanged(hasWindowFocus);
203         if (hasWindowFocus) {
204             updateAnimatorState();
205         } else {
206             stopAnimator();
207         }
208     }
209 
setLevelSource(SpeechLevelSource source)210     public void setLevelSource(SpeechLevelSource source) {
211         if (DEBUG) {
212             Log.d(TAG, "Speech source set");
213         }
214         mLevelSource = source;
215     }
216 
217     @Override
onDraw(Canvas canvas)218     public void onDraw(Canvas canvas) {
219         if (isEnabled()) {
220             canvas.drawColor(mEnableBackgroundColor);
221 
222             int level = mLevelSource.getSpeechLevel();
223 
224             // Set the peak level for the trailing circle, goes to a peak, waits there for
225             // some frames, then starts to decay.
226             if (level > mPeakLevel) {
227                 mPeakLevel = level;
228                 mPeakLevelCountDown = 25;
229             } else {
230                 if (mPeakLevelCountDown == 0) {
231                     mPeakLevel = Math.max(0, mPeakLevel - 2);
232                 } else {
233                     mPeakLevelCountDown--;
234                 }
235             }
236 
237             // Either ease towards the target level, or decay away from it depending on whether
238             // its higher or lower than the current.
239             if (level > mCurrentVolume) {
240                 mCurrentVolume = mCurrentVolume + ((level - mCurrentVolume) / 4);
241             } else {
242                 mCurrentVolume = (int) (mCurrentVolume * 0.95f);
243             }
244 
245             int centerX = mCenterTranslationX + (getWidth() / 2);
246             int centerY = mCenterTranslationY + (getWidth() / 2);
247             if (mTrailLevel != null) {
248                 int size = ((centerX - mMinimumLevelSize) * mPeakLevel) / 100 + mMinimumLevelSize;
249 
250                 mDestRect.set(
251                         centerX - size,
252                         centerY - size,
253                         centerX + size,
254                         centerY + size);
255                 canvas.drawBitmap(mTrailLevel, null, mDestRect, mEmptyPaint);
256             }
257 
258             if (mPrimaryLevel != null) {
259                 int size =
260                         ((centerX - mMinimumLevelSize) * mCurrentVolume) / 100 + mMinimumLevelSize;
261 
262                 mDestRect.set(
263                         centerX - size,
264                         centerY - size,
265                         centerX + size,
266                         centerY + size);
267                 canvas.drawBitmap(mPrimaryLevel, null, mDestRect, mEmptyPaint);
268                 mPaint.setColor(getResources().getColor(R.color.search_mic_background));
269                 mPaint.setStyle(Paint.Style.FILL);
270                 canvas.drawCircle(centerX, centerY, mMinimumLevelSize -
271                         MIC_PRIMARY_LEVEL_IMAGE_OFFSET, mPaint);
272             }
273             if(mTrailLevel != null && mPrimaryLevel != null) {
274                 mPaint.setColor(getResources().getColor(R.color.search_mic_levels_guideline));
275                 mPaint.setStyle(Paint.Style.STROKE);
276                 canvas.drawCircle(centerX, centerY, centerX - MIC_LEVEL_GUIDELINE_OFFSET, mPaint);
277             }
278         } else {
279             canvas.drawColor(mDisableBackgroundColor);
280         }
281     }
282 }
283