1 /*
2  * Copyright (C) 2015 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 package com.android.messaging.ui.mediapicker;
17 
18 import android.animation.ObjectAnimator;
19 import android.animation.TimeAnimator;
20 import android.animation.TimeAnimator.TimeListener;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.Paint.Style;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.view.View;
30 import android.view.accessibility.AccessibilityNodeInfo;
31 
32 import com.android.messaging.R;
33 import com.android.messaging.util.LogUtil;
34 
35 /**
36  * This view draws circular sound levels. By default the sound levels are black, unless
37  * otherwise defined via {@link #mPrimaryLevelPaint}.
38  */
39 public class SoundLevels extends View {
40     private static final String TAG = LogUtil.BUGLE_TAG;
41     private static final boolean DEBUG = false;
42 
43     private boolean mCenterDefined;
44     private int mCenterX;
45     private int mCenterY;
46 
47     // Paint for the main level meter, most closely follows the mic.
48     private final Paint mPrimaryLevelPaint;
49 
50     // The minimum size of the levels as a percentage of the max, that is the size when volume is 0.
51     private final float mMinimumLevel;
52 
53     // The minimum size of the levels, that is the size when volume is 0.
54     private final float mMinimumLevelSize;
55 
56     // The maximum size of the levels, that is the size when volume is 100.
57     private final float mMaximumLevelSize;
58 
59     // Generates clock ticks for the animation using the global animation loop.
60     private final TimeAnimator mSpeechLevelsAnimator;
61 
62     private float mCurrentVolume;
63 
64     // Indicates whether we should be animating the sound level.
65     private boolean mIsEnabled;
66 
67     // Input level is pulled from here.
68     private AudioLevelSource mLevelSource;
69 
SoundLevels(final Context context)70     public SoundLevels(final Context context) {
71         this(context, null);
72     }
73 
SoundLevels(final Context context, final AttributeSet attrs)74     public SoundLevels(final Context context, final AttributeSet attrs) {
75         this(context, attrs, 0);
76     }
77 
SoundLevels(final Context context, final AttributeSet attrs, final int defStyle)78     public SoundLevels(final Context context, final AttributeSet attrs, final int defStyle) {
79         super(context, attrs, defStyle);
80 
81         // Safe source, replaced with system one when attached.
82         mLevelSource = new AudioLevelSource();
83         mLevelSource.setSpeechLevel(0);
84 
85         final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SoundLevels,
86                 defStyle, 0);
87 
88         mMaximumLevelSize = a.getDimensionPixelOffset(
89                 R.styleable.SoundLevels_maxLevelRadius, 0);
90         mMinimumLevelSize = a.getDimensionPixelOffset(
91                 R.styleable.SoundLevels_minLevelRadius, 0);
92         mMinimumLevel = mMinimumLevelSize / mMaximumLevelSize;
93 
94         mPrimaryLevelPaint = new Paint();
95         mPrimaryLevelPaint.setColor(
96                 a.getColor(R.styleable.SoundLevels_primaryColor, Color.BLACK));
97         mPrimaryLevelPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
98 
99         a.recycle();
100 
101         // This animator generates ticks that invalidate the
102         // view so that the animation is synced with the global animation loop.
103         // TODO: We could probably remove this in favor of using postInvalidateOnAnimation
104         // which might improve things further.
105         mSpeechLevelsAnimator = new TimeAnimator();
106         mSpeechLevelsAnimator.setRepeatCount(ObjectAnimator.INFINITE);
107         mSpeechLevelsAnimator.setTimeListener(new TimeListener() {
108             @Override
109             public void onTimeUpdate(final TimeAnimator animation, final long totalTime,
110                     final long deltaTime) {
111                 invalidate();
112             }
113         });
114     }
115 
116     @Override
onDraw(final Canvas canvas)117     protected void onDraw(final Canvas canvas) {
118         if (!mIsEnabled) {
119             return;
120         }
121 
122         if (!mCenterDefined) {
123             // One time computation here, because we can't rely on getWidth() to be computed at
124             // constructor time or in onFinishInflate :(.
125             mCenterX = getWidth() / 2;
126             mCenterY = getWidth() / 2;
127             mCenterDefined = true;
128         }
129 
130         final int level = mLevelSource.getSpeechLevel();
131         // Either ease towards the target level, or decay away from it depending on whether
132         // its higher or lower than the current.
133         if (level > mCurrentVolume) {
134             mCurrentVolume = mCurrentVolume + ((level - mCurrentVolume) / 4);
135         } else {
136             mCurrentVolume = mCurrentVolume * 0.95f;
137         }
138 
139         final float radius = mMinimumLevel + (1f - mMinimumLevel) * mCurrentVolume / 100;
140         mPrimaryLevelPaint.setStyle(Style.FILL);
141         canvas.drawCircle(mCenterX, mCenterY, radius * mMaximumLevelSize, mPrimaryLevelPaint);
142     }
143 
setLevelSource(final AudioLevelSource source)144     public void setLevelSource(final AudioLevelSource source) {
145         if (DEBUG) {
146             Log.d(TAG, "Speech source set.");
147         }
148         mLevelSource = source;
149     }
150 
startSpeechLevelsAnimator()151     private void startSpeechLevelsAnimator() {
152         if (DEBUG) {
153             Log.d(TAG, "startAnimator()");
154         }
155         if (!mSpeechLevelsAnimator.isStarted()) {
156             mSpeechLevelsAnimator.start();
157         }
158     }
159 
stopSpeechLevelsAnimator()160     private void stopSpeechLevelsAnimator() {
161         if (DEBUG) {
162             Log.d(TAG, "stopAnimator()");
163         }
164         if (mSpeechLevelsAnimator.isStarted()) {
165             mSpeechLevelsAnimator.end();
166         }
167     }
168 
169     @Override
onDetachedFromWindow()170     protected void onDetachedFromWindow() {
171         super.onDetachedFromWindow();
172         stopSpeechLevelsAnimator();
173     }
174 
175     @Override
setEnabled(final boolean enabled)176     public void setEnabled(final boolean enabled) {
177         if (enabled == mIsEnabled) {
178             return;
179         }
180         if (DEBUG) {
181             Log.d("TAG", "setEnabled: " + enabled);
182         }
183         super.setEnabled(enabled);
184         mIsEnabled = enabled;
185         setKeepScreenOn(enabled);
186         updateSpeechLevelsAnimatorState();
187     }
188 
updateSpeechLevelsAnimatorState()189     private void updateSpeechLevelsAnimatorState() {
190         if (mIsEnabled) {
191             startSpeechLevelsAnimator();
192         } else {
193             stopSpeechLevelsAnimator();
194         }
195     }
196 
197     /**
198      * This is required to make the View findable by uiautomator
199      */
200     @Override
onInitializeAccessibilityNodeInfo(final AccessibilityNodeInfo info)201     public void onInitializeAccessibilityNodeInfo(final AccessibilityNodeInfo info) {
202         super.onInitializeAccessibilityNodeInfo(info);
203         info.setClassName(SoundLevels.class.getCanonicalName());
204     }
205 
206     /**
207      * Set the alpha level of the sound circles.
208      */
setPrimaryColorAlpha(final int alpha)209     public void setPrimaryColorAlpha(final int alpha) {
210         mPrimaryLevelPaint.setAlpha(alpha);
211     }
212 }
213