Split out ListenService and VolumeHistory
This moves the model of the historic volume to its own class (VolumeHistory), and extracts the listening out of the ListenActivity to the ListenService.
This commit is contained in:
@@ -18,12 +18,19 @@
|
||||
<uses-permission
|
||||
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
|
||||
android:required="true" />
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@android:style/Theme.Holo" >
|
||||
android:theme="@android:style/Theme.Holo">
|
||||
<service
|
||||
android:name=".ListenService"
|
||||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".StartActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* This file is part of Child Monitor.
|
||||
*
|
||||
* Child Monitor is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Child Monitor is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Child Monitor. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package de.rochefort.childmonitor;
|
||||
|
||||
public interface AudioListener {
|
||||
void onAudio(short[] audioBytes);
|
||||
}
|
||||
@@ -16,169 +16,132 @@
|
||||
*/
|
||||
package de.rochefort.childmonitor;
|
||||
|
||||
import static de.rochefort.childmonitor.AudioCodecDefines.CODEC;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.media.MediaPlayer;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.os.IBinder;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.UnknownHostException;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class ListenActivity extends Activity {
|
||||
final String TAG = "ChildMonitor";
|
||||
// Sets an ID for the notification
|
||||
final static int mNotificationId = 1;
|
||||
final String TAG = "ListenActivity";
|
||||
|
||||
String _address;
|
||||
int _port;
|
||||
String _name;
|
||||
NotificationManagerCompat _mNotifyMgr;
|
||||
String address;
|
||||
int port;
|
||||
String name;
|
||||
|
||||
Thread _listenThread;
|
||||
private final int frequency = AudioCodecDefines.FREQUENCY;
|
||||
private final int channelConfiguration = AudioCodecDefines.CHANNEL_CONFIGURATION_OUT;
|
||||
private final int audioEncoding = AudioCodecDefines.ENCODING;
|
||||
private final int bufferSize = AudioTrack.getMinBufferSize(frequency, channelConfiguration, audioEncoding);
|
||||
private final int byteBufferSize = bufferSize*2;
|
||||
// Don't attempt to unbind from the service unless the client has received some
|
||||
// information about the service's state.
|
||||
private boolean shouldUnbind;
|
||||
|
||||
private void streamAudio(final Socket socket, AudioListener listener) throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
Log.i(TAG, "Setting up stream");
|
||||
// To invoke the bound service, first make sure that this value
|
||||
// is not null.
|
||||
private ListenService boundService;
|
||||
|
||||
final AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
frequency,
|
||||
channelConfiguration,
|
||||
audioEncoding,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM);
|
||||
private final ServiceConnection connection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
// This is called when the connection with the service has been
|
||||
// established, giving us the service object we can use to
|
||||
// interact with the service. Because we have bound to a explicit
|
||||
// service that we know is running in our own process, we can
|
||||
// cast its IBinder to a concrete class and directly access it.
|
||||
ListenService bs = ((ListenService.ListenBinder) service).getService();
|
||||
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
Toast.makeText(ListenActivity.this, R.string.connect,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
final VolumeView volumeView = findViewById(R.id.volume);
|
||||
|
||||
final InputStream is = socket.getInputStream();
|
||||
int read = 0;
|
||||
volumeView.setVolumeHistory(bs.getVolumeHistory());
|
||||
bs.setUpdateCallback(volumeView::postInvalidate);
|
||||
bs.setErrorCallback(() -> {
|
||||
TextView status = findViewById(R.id.textStatus);
|
||||
status.setText(R.string.disconnected);
|
||||
});
|
||||
|
||||
audioTrack.play();
|
||||
boundService = bs;
|
||||
}
|
||||
|
||||
try {
|
||||
final byte [] readBuffer = new byte[byteBufferSize];
|
||||
final short [] decodedBuffer = new short[byteBufferSize*2];
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
// This is called when the connection with the service has been
|
||||
// unexpectedly disconnected -- that is, its process crashed.
|
||||
// Because it is running in our same process, we should never
|
||||
// see this happen.
|
||||
boundService = null;
|
||||
Toast.makeText(ListenActivity.this, R.string.disconnected,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
};
|
||||
|
||||
while(socket.isConnected() && read != -1 && !Thread.currentThread().isInterrupted()) {
|
||||
read = is.read(readBuffer);
|
||||
int decoded = CODEC.decode(decodedBuffer, readBuffer, read, 0);
|
||||
|
||||
if(decoded > 0) {
|
||||
audioTrack.write(decodedBuffer, 0, decoded);
|
||||
short[] decodedBytes = new short[decoded];
|
||||
System.arraycopy(decodedBuffer, 0, decodedBytes, 0, decoded);
|
||||
listener.onAudio(decodedBytes);
|
||||
void startAndBindService() {
|
||||
final Context context = this;
|
||||
final Intent intent = new Intent(context, ListenService.class);
|
||||
intent.putExtra("name", name);
|
||||
intent.putExtra("address", address);
|
||||
intent.putExtra("port", port);
|
||||
ContextCompat.startForegroundService(context, intent);
|
||||
// Attempts to establish a connection with the service. We use an
|
||||
// explicit class name because we want a specific service
|
||||
// implementation that we know will be running in our own process
|
||||
// (and thus won't be supporting component replacement by other
|
||||
// applications).
|
||||
if (bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
|
||||
shouldUnbind = true;
|
||||
Log.i(TAG, "Bound service");
|
||||
} else {
|
||||
Log.e(TAG, "Error: The requested service doesn't " +
|
||||
"exist, or this client isn't allowed access to it.");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Connection failed", e);
|
||||
} finally {
|
||||
audioTrack.stop();
|
||||
socket.close();
|
||||
|
||||
void doUnbindAndStopService() {
|
||||
if (shouldUnbind) {
|
||||
// Release information about the service's state.
|
||||
unbindService(connection);
|
||||
shouldUnbind = false;
|
||||
}
|
||||
final Context context = this;
|
||||
final Intent intent = new Intent(context, ListenService.class);
|
||||
context.stopService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
final Bundle b = getIntent().getExtras();
|
||||
_address = b.getString("address");
|
||||
_port = b.getInt("port");
|
||||
_name = b.getString("name");
|
||||
// Gets an instance of the NotificationManager service
|
||||
_mNotifyMgr =
|
||||
NotificationManagerCompat.from(this);
|
||||
final Bundle bundle = getIntent().getExtras();
|
||||
if (bundle != null) {
|
||||
address = bundle.getString("address");
|
||||
port = bundle.getInt("port");
|
||||
name = bundle.getString("name");
|
||||
startAndBindService();
|
||||
}
|
||||
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
setContentView(R.layout.activity_listen);
|
||||
|
||||
NotificationCompat.Builder mBuilder =
|
||||
new NotificationCompat.Builder(ListenActivity.this)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.listening_notification)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(getString(R.string.listening));
|
||||
final TextView connectedText = findViewById(R.id.connectedTo);
|
||||
connectedText.setText(name);
|
||||
|
||||
_mNotifyMgr.notify(mNotificationId, mBuilder.build());
|
||||
|
||||
final TextView connectedText = (TextView) findViewById(R.id.connectedTo);
|
||||
connectedText.setText(_name);
|
||||
|
||||
final TextView statusText = (TextView) findViewById(R.id.textStatus);
|
||||
final TextView statusText = findViewById(R.id.textStatus);
|
||||
if (bundle != null) {
|
||||
statusText.setText(R.string.listening);
|
||||
|
||||
final VolumeView volumeView = (VolumeView) findViewById(R.id.volume);
|
||||
|
||||
final AudioListener listener = audioData -> runOnUiThread(() -> volumeView.onAudioData(audioData));
|
||||
|
||||
|
||||
_listenThread = new Thread(() -> {
|
||||
try {
|
||||
final Socket socket = new Socket(_address, _port);
|
||||
streamAudio(socket, listener);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to stream audio", e);
|
||||
}
|
||||
|
||||
if(!Thread.currentThread().isInterrupted()) {
|
||||
// If this thread has not been interrupted, likely something
|
||||
// bad happened with the connection to the child device. Play
|
||||
// an alert to notify the user that the connection has been
|
||||
// interrupted.
|
||||
playAlert();
|
||||
|
||||
ListenActivity.this.runOnUiThread(() -> {
|
||||
final TextView connectedText1 = (TextView) findViewById(R.id.connectedTo);
|
||||
connectedText1.setText("");
|
||||
|
||||
final TextView statusText1 = (TextView) findViewById(R.id.textStatus);
|
||||
statusText1.setText(R.string.disconnected);
|
||||
NotificationCompat.Builder mBuilder1 =
|
||||
new NotificationCompat.Builder(ListenActivity.this)
|
||||
.setOngoing(false)
|
||||
.setSmallIcon(R.drawable.listening_notification)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(getString(R.string.disconnected));
|
||||
_mNotifyMgr.notify(mNotificationId, mBuilder1.build());
|
||||
});
|
||||
else {
|
||||
statusText.setText(R.string.error_please_retry);
|
||||
}
|
||||
});
|
||||
|
||||
_listenThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
_listenThread.interrupt();
|
||||
_listenThread = null;
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
doUnbindAndStopService();
|
||||
boundService = null;
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void playAlert() {
|
||||
final MediaPlayer mp = MediaPlayer.create(this, R.raw.upward_beep_chromatic_fifths);
|
||||
if(mp != null) {
|
||||
Log.i(TAG, "Playing alert");
|
||||
mp.setOnCompletionListener(mp1 -> mp1.release());
|
||||
mp.start();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to play alert");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
237
app/src/main/java/de/rochefort/childmonitor/ListenService.java
Normal file
237
app/src/main/java/de/rochefort/childmonitor/ListenService.java
Normal file
@@ -0,0 +1,237 @@
|
||||
package de.rochefort.childmonitor;
|
||||
|
||||
import static de.rochefort.childmonitor.AudioCodecDefines.CODEC;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.media.AudioTrack;
|
||||
import android.media.MediaPlayer;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.Messenger;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ListenService extends Service {
|
||||
ArrayList<Messenger> clients = new ArrayList<>();
|
||||
|
||||
private static final String TAG = "ListenService";
|
||||
public static final String CHANNEL_ID = TAG;
|
||||
public static final int ID = 1337;
|
||||
|
||||
private final int frequency = AudioCodecDefines.FREQUENCY;
|
||||
private final int channelConfiguration = AudioCodecDefines.CHANNEL_CONFIGURATION_OUT;
|
||||
private final int audioEncoding = AudioCodecDefines.ENCODING;
|
||||
private final int bufferSize = AudioTrack.getMinBufferSize(frequency, channelConfiguration, audioEncoding);
|
||||
private final int byteBufferSize = bufferSize*2;
|
||||
|
||||
private final IBinder binder = new ListenBinder();
|
||||
private NotificationManager notificationManager;
|
||||
private String address;
|
||||
private int port;
|
||||
private Thread listenThread;
|
||||
|
||||
private final VolumeHistory volumeHistory = new VolumeHistory(16_384);
|
||||
|
||||
public ListenService() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.i(TAG, "Received start id " + startId + ": " + intent);
|
||||
// Display a notification about us starting. We put an icon in the status bar.
|
||||
createNotificationChannel();
|
||||
Notification n = buildNotification(intent);
|
||||
startForeground(ID, n);
|
||||
doListen();
|
||||
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Thread lt = listenThread;
|
||||
if (lt != null) {
|
||||
lt.interrupt();
|
||||
listenThread = null;
|
||||
}
|
||||
|
||||
// Cancel the persistent notification.
|
||||
int NOTIFICATION = R.string.listening;
|
||||
notificationManager.cancel(NOTIFICATION);
|
||||
|
||||
stopForeground(true);
|
||||
// Tell the user we stopped.
|
||||
Toast.makeText(this, R.string.stopped, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return binder;
|
||||
}
|
||||
|
||||
public VolumeHistory getVolumeHistory() {
|
||||
return volumeHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a notification while this service is running.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Notification buildNotification(Intent intent) {
|
||||
// In this sample, we'll use the same text for the ticker and the expanded notification
|
||||
CharSequence text = getText(R.string.listening);
|
||||
|
||||
final Bundle bundle = intent.getExtras();
|
||||
if (bundle == null) {
|
||||
return null;
|
||||
}
|
||||
address = bundle.getString("address");
|
||||
port = bundle.getInt("port");
|
||||
String _name = bundle.getString("name");
|
||||
|
||||
// The PendingIntent to launch our activity if the user selects this notification
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
|
||||
new Intent(this, ListenActivity.class), PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
// Set the info for the views that show in the notification panel.
|
||||
NotificationCompat.Builder b = new NotificationCompat.Builder(this, CHANNEL_ID);
|
||||
b.setSmallIcon(R.drawable.listening_notification) // the status icon
|
||||
.setOngoing(true)
|
||||
.setTicker(text) // the status text
|
||||
.setContentTitle(text) // the label of the entry
|
||||
.setContentText(_name) // the contents of the entry
|
||||
.setContentIntent(contentIntent);
|
||||
return b.build();
|
||||
}
|
||||
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel serviceChannel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Foreground Service Channel",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
notificationManager.createNotificationChannel(serviceChannel);
|
||||
}
|
||||
}
|
||||
|
||||
public void setErrorCallback(Runnable errorCallback) {
|
||||
this.mErrorCallback = errorCallback;
|
||||
}
|
||||
|
||||
public void setUpdateCallback(Runnable updateCallback) {
|
||||
this.mUpdateCallback = updateCallback;
|
||||
}
|
||||
|
||||
public class ListenBinder extends Binder {
|
||||
ListenService getService() {
|
||||
return ListenService.this;
|
||||
}
|
||||
}
|
||||
|
||||
private Runnable mErrorCallback;
|
||||
private Runnable mUpdateCallback;
|
||||
|
||||
private void doListen() {
|
||||
listenThread = new Thread(() -> {
|
||||
try {
|
||||
final Socket socket = new Socket(address, port);
|
||||
streamAudio(socket);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to stream audio", e);
|
||||
}
|
||||
|
||||
if (!Thread.currentThread().isInterrupted()) {
|
||||
// If this thread has not been interrupted, likely something
|
||||
// bad happened with the connection to the child device. Play
|
||||
// an alert to notify the user that the connection has been
|
||||
// interrupted.
|
||||
playAlert();
|
||||
|
||||
final Runnable errorCallback = mErrorCallback;
|
||||
if (errorCallback != null) {
|
||||
errorCallback.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
listenThread.start();
|
||||
}
|
||||
|
||||
|
||||
private void streamAudio(final Socket socket) throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
Log.i(TAG, "Setting up stream");
|
||||
|
||||
final AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||
frequency,
|
||||
channelConfiguration,
|
||||
audioEncoding,
|
||||
bufferSize,
|
||||
AudioTrack.MODE_STREAM);
|
||||
|
||||
final InputStream is = socket.getInputStream();
|
||||
int read = 0;
|
||||
|
||||
audioTrack.play();
|
||||
|
||||
try {
|
||||
final byte [] readBuffer = new byte[byteBufferSize];
|
||||
final short [] decodedBuffer = new short[byteBufferSize*2];
|
||||
|
||||
while (socket.isConnected() && read != -1 && !Thread.currentThread().isInterrupted()) {
|
||||
read = is.read(readBuffer);
|
||||
int decoded = CODEC.decode(decodedBuffer, readBuffer, read, 0);
|
||||
|
||||
if (decoded > 0) {
|
||||
audioTrack.write(decodedBuffer, 0, decoded);
|
||||
short[] decodedBytes = new short[decoded];
|
||||
System.arraycopy(decodedBuffer, 0, decodedBytes, 0, decoded);
|
||||
volumeHistory.onAudioData(decodedBytes);
|
||||
final Runnable updateCallback = mUpdateCallback;
|
||||
if (updateCallback != null) {
|
||||
updateCallback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Connection failed", e);
|
||||
} finally {
|
||||
audioTrack.stop();
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void playAlert() {
|
||||
final MediaPlayer mp = MediaPlayer.create(this, R.raw.upward_beep_chromatic_fifths);
|
||||
if(mp != null) {
|
||||
Log.i(TAG, "Playing alert");
|
||||
mp.setOnCompletionListener(MediaPlayer::release);
|
||||
mp.start();
|
||||
} else {
|
||||
Log.e(TAG, "Failed to play alert");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.rochefort.childmonitor;
|
||||
|
||||
import android.support.v4.util.CircularArray;
|
||||
|
||||
public class VolumeHistory {
|
||||
private double mMaxVolume = 0.25;
|
||||
|
||||
private double mVolumeNorm = 1.0 / mMaxVolume;
|
||||
private final CircularArray<Double> mHistory;
|
||||
private final int mMaxHistory;
|
||||
|
||||
VolumeHistory(int maxHistory) {
|
||||
mMaxHistory = maxHistory;
|
||||
mHistory = new CircularArray<>(maxHistory);
|
||||
}
|
||||
|
||||
|
||||
public double getVolumeNorm() {
|
||||
return mVolumeNorm;
|
||||
}
|
||||
|
||||
public double get(int i) {
|
||||
return mHistory.get(i);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return mHistory.size();
|
||||
}
|
||||
|
||||
protected synchronized void addLast(double volume) {
|
||||
if (volume > mMaxVolume) {
|
||||
mMaxVolume = volume;
|
||||
mVolumeNorm = 1.0 / volume;
|
||||
}
|
||||
mHistory.addLast(volume);
|
||||
mHistory.removeFromStart(mHistory.size() - mMaxHistory);
|
||||
}
|
||||
|
||||
public void onAudioData(short[] data) {
|
||||
if (data.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final double scale = 1.0 / 128.0;
|
||||
double sum = 0;
|
||||
for (final short datum : data) {
|
||||
final double rel = datum * scale;
|
||||
sum += rel * rel;
|
||||
}
|
||||
final double volume = sum / data.length;
|
||||
addLast(volume);
|
||||
}
|
||||
|
||||
public synchronized VolumeHistory getSnapshot(int length) {
|
||||
length = Math.min(length, size());
|
||||
VolumeHistory copy = new VolumeHistory(length);
|
||||
copy.mMaxVolume = this.mMaxVolume;
|
||||
copy.mVolumeNorm = this.mVolumeNorm;
|
||||
for (int i = 0; i < length; ++i) {
|
||||
copy.mHistory.addLast(mHistory.get(i));
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,9 @@ import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class VolumeView extends View {
|
||||
private static final int MAX_HISTORY = 10_000;
|
||||
private double volume;
|
||||
private double maxVolume;
|
||||
private Paint paint;
|
||||
private LinkedList<Double> volumeHistory;
|
||||
private VolumeHistory _volumeHistory;
|
||||
|
||||
public VolumeView(Context context) {
|
||||
super(context);
|
||||
@@ -49,37 +44,25 @@ public class VolumeView extends View {
|
||||
}
|
||||
|
||||
private void init() {
|
||||
volume = 0;
|
||||
maxVolume = 0.25;
|
||||
paint = new Paint();
|
||||
volumeHistory = new LinkedList<>();
|
||||
paint.setColor(Color.rgb(255, 127, 0));
|
||||
}
|
||||
|
||||
public void onAudioData(short[] data) {
|
||||
double sum = 0;
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
double rel = data[i] / ((double)128);
|
||||
sum += Math.pow(rel, 2);
|
||||
}
|
||||
volume = sum / data.length;
|
||||
if (volume > maxVolume) {
|
||||
maxVolume = volume;
|
||||
}
|
||||
volumeHistory.addLast(volume);
|
||||
while (volumeHistory.size() > MAX_HISTORY) {
|
||||
volumeHistory.removeFirst();
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
int height = canvas.getHeight();
|
||||
int width = canvas.getWidth();
|
||||
double relativeBrightness = 0;
|
||||
double normalizedVolume = volume / maxVolume;
|
||||
relativeBrightness = Math.max(0.3, normalizedVolume);
|
||||
final VolumeHistory volumeHistory = _volumeHistory;
|
||||
if (volumeHistory == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int height = canvas.getHeight();
|
||||
final int width = canvas.getWidth();
|
||||
|
||||
final VolumeHistory history = volumeHistory.getSnapshot(width);
|
||||
final int size = history.size(); // Size is at most width
|
||||
final double volumeNorm = history.getVolumeNorm();
|
||||
final double normalizedVolume = history.get(size - 1);
|
||||
final double relativeBrightness = Math.max(0.3, normalizedVolume);
|
||||
int blue;
|
||||
int rest;
|
||||
if (relativeBrightness > 0.5) {
|
||||
@@ -89,28 +72,22 @@ public class VolumeView extends View {
|
||||
blue = (int) (255 * (relativeBrightness - 0.2) / 0.3);
|
||||
rest = 0;
|
||||
}
|
||||
int rgb = Color.rgb(rest, rest, blue);
|
||||
final int rgb = Color.rgb(rest, rest, blue);
|
||||
canvas.drawColor(rgb);
|
||||
double margins = height * 0.1;
|
||||
double graphHeight = height - 2*margins;
|
||||
int leftMost = Math.max(0, volumeHistory.size() - width);
|
||||
int yPrev = (int) (height - margins);
|
||||
for (int i = leftMost; i < volumeHistory.size() && i - leftMost < width; i++) {
|
||||
int xNext = i - leftMost;
|
||||
int yNext = (int) (margins + graphHeight - volumeHistory.get(i) / maxVolume * (graphHeight));
|
||||
int xPrev;
|
||||
if (i == leftMost) {
|
||||
xPrev = xNext;
|
||||
} else {
|
||||
xPrev = xNext - 1;
|
||||
}
|
||||
if (i == leftMost && i > 0){
|
||||
yPrev = (int) (margins + graphHeight - volumeHistory.get(i-1) / maxVolume * (graphHeight));
|
||||
}
|
||||
final double margins = height * 0.1;
|
||||
final double graphHeight = height - 2.0 * margins;
|
||||
final double graphScale = graphHeight * volumeNorm;
|
||||
int xPrev = 0;
|
||||
int yPrev = ((int) (margins + graphHeight - volumeHistory.get(0) * graphScale));
|
||||
for (int xNext = 1; xNext < size; ++xNext) {
|
||||
int yNext = (int) (margins + graphHeight - volumeHistory.get(xNext) * graphScale);
|
||||
canvas.drawLine(xPrev, yPrev, xNext, yNext, paint);
|
||||
xPrev = xNext;
|
||||
yPrev = yNext;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void setVolumeHistory(VolumeHistory volumeHistory) {
|
||||
this._volumeHistory = volumeHistory;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,5 @@
|
||||
<string name="discoverChildDescription">Select child from a list of discovered children on the network</string>
|
||||
<string name="enterChildAddress">Select Child by Address</string>
|
||||
<string name="enterChildAddressDescription">Enter the address and port of the child</string>
|
||||
<string name="error_please_retry">Error, please retry.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user