diff --git a/app/build.gradle b/app/build.gradle index 0ceb7aa..c8b4b59 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 26 + compileSdk 26 defaultConfig { applicationId "de.rochefort.childmonitor" @@ -28,4 +28,8 @@ android { abortOnError true warning 'MissingTranslation' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7703f19..1d1c324 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,44 +1,59 @@ + android:versionName="0.2"> - - - - - + + + + + + + android:theme="@android:style/Theme.Holo"> + + + + android:parentActivityName=".StartActivity" /> + android:parentActivityName=".StartActivity" + android:windowSoftInputMode="adjustPan" /> + android:parentActivityName=".DiscoverActivity" /> - + \ No newline at end of file diff --git a/app/src/main/java/de/rochefort/childmonitor/AudioListener.java b/app/src/main/java/de/rochefort/childmonitor/AudioListener.java deleted file mode 100644 index 81af69a..0000000 --- a/app/src/main/java/de/rochefort/childmonitor/AudioListener.java +++ /dev/null @@ -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 . - */ -package de.rochefort.childmonitor; - -public interface AudioListener { - void onAudio(short[] audioBytes); -} diff --git a/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java b/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java index 9232e70..4b5baf9 100644 --- a/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java +++ b/app/src/main/java/de/rochefort/childmonitor/ListenActivity.java @@ -16,169 +16,114 @@ */ 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; + // Don't attempt to unbind from the service unless the client has received some + // information about the service's state. + private boolean shouldUnbind; - 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; + 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(); - private void streamAudio(final Socket socket, AudioListener listener) 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); - - setVolumeControlStream(AudioManager.STREAM_MUSIC); - - 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); - listener.onAudio(decodedBytes); - } - } - } catch (Exception e) { - Log.e(TAG, "Connection failed", e); - } finally { - audioTrack.stop(); - socket.close(); + Toast.makeText(ListenActivity.this, R.string.connect, + Toast.LENGTH_SHORT).show(); + final TextView connectedText = findViewById(R.id.connectedTo); + connectedText.setText(bs.getChildDeviceName()); + final VolumeView volumeView = findViewById(R.id.volume); + volumeView.setVolumeHistory(bs.getVolumeHistory()); + bs.setUpdateCallback(volumeView::postInvalidate); + bs.setErrorCallback(ListenActivity.this::postErrorMessage); } + + 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. + Toast.makeText(ListenActivity.this, R.string.disconnected, + Toast.LENGTH_SHORT).show(); + } + }; + + + void ensureServiceRunningAndBind(Bundle bundle) { + final Context context = this; + final Intent intent = new Intent(context, ListenService.class); + if (bundle != null) { + intent.putExtra("name", bundle.getString("name")); + intent.putExtra("address", bundle.getString("address")); + intent.putExtra("port", bundle.getInt("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."); + } + } + + 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); + } + + public void postErrorMessage() { + TextView status = findViewById(R.id.textStatus); + status.post(() -> { + status.setText(R.string.disconnected); + }); } @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(); + ensureServiceRunningAndBind(bundle); + 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)); - - _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); 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()); - }); - } - }); - - _listenThread.start(); - } - - @Override - protected void onStop() { - _listenThread.interrupt(); - _listenThread = null; - super.onStop(); } @Override public void onDestroy() { + doUnbindAndStopService(); 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"); - } - } } diff --git a/app/src/main/java/de/rochefort/childmonitor/ListenService.java b/app/src/main/java/de/rochefort/childmonitor/ListenService.java new file mode 100644 index 0000000..835cdd5 --- /dev/null +++ b/app/src/main/java/de/rochefort/childmonitor/ListenService.java @@ -0,0 +1,230 @@ +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.support.v4.app.NotificationCompat; +import android.util.Log; +import android.widget.Toast; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; + +public class ListenService extends Service { + 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 Thread listenThread; + + private final VolumeHistory volumeHistory = new VolumeHistory(16_384); + private String childDeviceName; + + 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(); + Bundle extras = intent.getExtras(); + if (extras != null) { + String name = extras.getString("name"); + childDeviceName = name; + Notification n = buildNotification(name); + startForeground(ID, n); + String address = extras.getString("address"); + int port = extras.getInt("port"); + doListen(address, port); + } + + return START_REDELIVER_INTENT; + } + + @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; + } + + private Notification buildNotification(String name) { + // In this sample, we'll use the same text for the ticker and the expanded notification + CharSequence text = getText(R.string.listening); + + // 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(String address, int port) { + 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"); + } + } + + public String getChildDeviceName() { + return childDeviceName; + } +} diff --git a/app/src/main/java/de/rochefort/childmonitor/StartActivity.java b/app/src/main/java/de/rochefort/childmonitor/StartActivity.java index a3e12e7..9b42af6 100644 --- a/app/src/main/java/de/rochefort/childmonitor/StartActivity.java +++ b/app/src/main/java/de/rochefort/childmonitor/StartActivity.java @@ -39,7 +39,7 @@ public class StartActivity extends Activity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_start); - final Button monitorButton = (Button) findViewById(R.id.useChildDevice); + final Button monitorButton = findViewById(R.id.useChildDevice); monitorButton.setOnClickListener(v -> { Log.i(TAG, "Starting up monitor"); @@ -50,7 +50,7 @@ public class StartActivity extends Activity { } }); - final Button connectButton = (Button) findViewById(R.id.useParentDevice); + final Button connectButton = findViewById(R.id.useParentDevice); connectButton.setOnClickListener(v -> { Log.i(TAG, "Starting connection activity"); if (isMulticastPermissionGranted()) { diff --git a/app/src/main/java/de/rochefort/childmonitor/VolumeHistory.java b/app/src/main/java/de/rochefort/childmonitor/VolumeHistory.java new file mode 100644 index 0000000..36d0035 --- /dev/null +++ b/app/src/main/java/de/rochefort/childmonitor/VolumeHistory.java @@ -0,0 +1,61 @@ +package de.rochefort.childmonitor; + +import android.os.Handler; +import android.os.Looper; +import android.support.v4.util.CircularArray; + +public class VolumeHistory { + private double mMaxVolume = 0.25; + + private double mVolumeNorm = 1.0 / mMaxVolume; + private final CircularArray mHistory; + private final int mMaxHistory; + + private final Handler uiHandler; + + VolumeHistory(int maxHistory) { + uiHandler = new Handler(Looper.getMainLooper()); + 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(); + } + + private void addLast(double volume) { + // schedule editing of member vars on the ui event loop to avoid concurrency problems + uiHandler.post(() -> { + 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); + } +} diff --git a/app/src/main/java/de/rochefort/childmonitor/VolumeView.java b/app/src/main/java/de/rochefort/childmonitor/VolumeView.java index b338bcb..c74ae34 100644 --- a/app/src/main/java/de/rochefort/childmonitor/VolumeView.java +++ b/app/src/main/java/de/rochefort/childmonitor/VolumeView.java @@ -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 volumeHistory; + private VolumeHistory _volumeHistory; public VolumeView(Context context) { super(context); @@ -49,37 +44,29 @@ 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 int size = volumeHistory.size(); // Size is at most width + final double volumeNorm = volumeHistory.getVolumeNorm(); + final double relativeBrightness; + if (size > 0) { + final double normalizedVolume = volumeHistory.get(size - 1); + relativeBrightness = Math.max(0.3, normalizedVolume); + } else { + relativeBrightness = 0.3; + } int blue; int rest; if (relativeBrightness > 0.5) { @@ -89,28 +76,28 @@ 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)); - } - canvas.drawLine(xPrev, yPrev, xNext, yNext, paint); - yPrev = yNext; - + if (size == 0) { + return; } + final double margins = height * 0.1; + final double graphHeight = height - 2.0 * margins; + int leftMost = Math.max(0, volumeHistory.size() - width); + final double graphScale = graphHeight * volumeNorm; + int xPrev = 0; + int yPrev = ((int) (margins + graphHeight - volumeHistory.get(leftMost) * graphScale)); + int length = Math.min(size, width); + for (int xNext = 1; xNext < length-1; ++xNext) { + int yNext = (int) (margins + graphHeight - volumeHistory.get(leftMost + xNext) * graphScale); + canvas.drawLine(xPrev, yPrev, xNext, yNext, paint); + xPrev = xNext; + yPrev = yNext; + } + } + + public void setVolumeHistory(VolumeHistory volumeHistory) { + this._volumeHistory = volumeHistory; } } diff --git a/app/src/main/java/de/rochefort/childmonitor/audio/G711UCodec.java b/app/src/main/java/de/rochefort/childmonitor/audio/G711UCodec.java index c93763a..8736a57 100644 --- a/app/src/main/java/de/rochefort/childmonitor/audio/G711UCodec.java +++ b/app/src/main/java/de/rochefort/childmonitor/audio/G711UCodec.java @@ -29,8 +29,8 @@ public class G711UCodec { // s001wxyzabcde...s101wxyz // s01wxyzabcdef...s110wxyz // s1wxyzabcdefg...s111wxyz - private static byte[] table13to8 = new byte[8192]; - private static short[] table8to16 = new short[256]; + private static final byte[] table13to8 = new byte[8192]; + private static final short[] table8to16 = new short[256]; static { // b13 --> b8 for (int p = 1, q = 0; p <= 0x80; p <<= 1, q+=0x10) { diff --git a/build.gradle b/build.gradle index 2185f40..dcfffc8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.2.2' } } diff --git a/gradle.properties b/gradle.properties index 5f8be8c..6436ed6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,6 +20,6 @@ # The aapt2 tool creates an APK which fails to install on Android 5 and below if it contains # a bug. Build tools 27.0.1 has a mitigation. Avoiding aapt2 also avoids hitting the bug. # See: https://issuetracker.google.com/issues/64434571 -android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false -android.nonTransitiveRClass=false \ No newline at end of file +android.nonTransitiveRClass=true +org.gradle.configuration-cache=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7f5f864..6715352 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip -distributionSha256Sum=f30b29580fe11719087d698da23f3b0f0d04031d8995f7dd8275a31f7674dc01 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +distributionSha256Sum=7c3ad722e9b0ce8205b91560fd6ce8296ac3eadf065672242fd73c06b8eeb6ee