Initial commit
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/
|
||||
.DS_Store
|
||||
/build
|
||||
/dist
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
46
BUILD.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Build
|
||||
|
||||
## Debug
|
||||
|
||||
This project is an Android application with some shell scripts to execute on the
|
||||
computer. Therefore, just use `gradle` as usual:
|
||||
|
||||
```
|
||||
./gradlew assembleDebug
|
||||
```
|
||||
|
||||
(or build from _Android Studio_)
|
||||
|
||||
To run it:
|
||||
|
||||
```bash
|
||||
./run
|
||||
./run <serial> # if several devices are connected
|
||||
```
|
||||
|
||||
_Since building is very fast, `./run` also executes `./gradlew assembleDebug` to
|
||||
always run an up-to-date version._
|
||||
|
||||
|
||||
## Release
|
||||
|
||||
To build and install a release, you need to generate a signed APK.
|
||||
|
||||
For that purpose, first generate a _keystore_:
|
||||
|
||||
```bash
|
||||
# generate sndcpy.keystore file
|
||||
keytool -genkey -v -keystore sndcpy.keystore -alias sndcpy \
|
||||
-keyalg RSA -keysize 2048 -validity 30000
|
||||
```
|
||||
|
||||
Then, add these lines (and adapt) in `~/.gradle/gradle.properties`:
|
||||
|
||||
```bash
|
||||
SNDCPY_STORE_FILE=/path/to/your/sndcpy.keystore
|
||||
SNDCPY_STORE_PASSWORD=the_keystore_password
|
||||
SNDCPY_KEY_ALIAS=sndcpy
|
||||
SNDCPY_KEY_PASSWORD=the_key_password
|
||||
```
|
||||
|
||||
Then, execute `./release.sh`. It will generate a release in `dist/`.
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Romain Vimont
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
119
README.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# sndcpy
|
||||
|
||||
This tool forwards audio from an Android 10 device to the computer. It does not
|
||||
require any _root_ access. It works on _GNU/Linux_, _Windows_ and _macOS_.
|
||||
|
||||
The purpose is to enable [audio forwarding][issue14] while mirroring with
|
||||
[scrcpy]. However, it can be used independently.
|
||||
|
||||
[issue14]: https://github.com/Genymobile/scrcpy/issues/14
|
||||
[scrcpy]: https://github.com/Genymobile/scrcpy
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
- The Android device requires at least Android 10.
|
||||
- [VLC] must be installed on the computer.
|
||||
|
||||
[vlc]: https://www.videolan.org/
|
||||
|
||||
|
||||
## Get the app
|
||||
|
||||
Download the latest release:
|
||||
|
||||
- [`sndcpy-v1.0.zip`][release]
|
||||
_SHA256: TODO_
|
||||
- [`sndcpy-with-adb-windows-v1.0.zip`][release-adb]
|
||||
_SHA256: TODO_
|
||||
|
||||
_On Windows, for simplicity, take the second archive, which also contains
|
||||
`adb`._
|
||||
|
||||
[release]: https://github.com/rom1v/sndcpy/releases/download/v1.0/sndcpy-v1.0.zip
|
||||
[release-adb]: https://github.com/rom1v/sndcpy/releases/download/v1.0/sndcpy-with-adb-windows-v1.0.zip
|
||||
|
||||
Alternatively, you could [build the app][BUILD].
|
||||
|
||||
[BUILD]: BUILD.md
|
||||
|
||||
## Run the app
|
||||
|
||||
Plug an Android 10 device with USB debugging enabled, and execute:
|
||||
|
||||
```bash
|
||||
./sndcpy
|
||||
```
|
||||
|
||||
If several devices are connected (listed by `adb devices`):
|
||||
|
||||
```bash
|
||||
./sndcpy <serial> # replace <serial> by the device serial
|
||||
```
|
||||
|
||||
_(omit `./` on Windows)_
|
||||
|
||||
It will install the app on the device, and request permission to start audio
|
||||
capture:
|
||||
|
||||

|
||||
|
||||
Once you clicked on _START NOW_, press _Enter_ in the console to start playing
|
||||
on the computer. Press `Ctrl`+`c` in the terminal to stop (except on Windows,
|
||||
just disconnect the device or stop capture from the device notifications).
|
||||
|
||||
VLC may print this error message once:
|
||||
|
||||
```
|
||||
main stream error: connection error: Connection refused
|
||||
```
|
||||
|
||||
It is "expected", just ignore it.
|
||||
|
||||
The sound continues to be played on the device. The volume can be adjusted
|
||||
independently on the device and on the computer.
|
||||
|
||||
## Uninstall
|
||||
|
||||
To uninstall the app from the device:
|
||||
|
||||
```bash
|
||||
adb uninstall com.rom1v.sndcpy
|
||||
```
|
||||
|
||||
## Apps restrictions
|
||||
|
||||
`sndcpy` may only forward audio from apps which do not [prevent audio
|
||||
capture][allow]. The rules are detailed in [§capture policy][rules]:
|
||||
|
||||
> - By default, apps that target versions up to and including to Android 9.0 do
|
||||
> not permit playback capture. To enable it, include
|
||||
> `android:allowAudioPlaybackCapture="true"` in the app's `manifest.xml` file.
|
||||
> - By default, apps that target Android 10 (API level 29) or higher allow their
|
||||
> audio to be captured. To disable playback capture, include
|
||||
> `android:allowAudioPlaybackCapture="false"` in the app's `manifest.xml`
|
||||
> file.
|
||||
|
||||
So some apps might need to be updated to support audio capture.
|
||||
|
||||
[allow]: https://developer.android.com/guide/topics/media/playback-capture#allowing_playback_capture
|
||||
[rules]: https://developer.android.com/guide/topics/media/playback-capture#capture_policy
|
||||
|
||||
## Audio delay
|
||||
|
||||
This is just a proof-of-concept, so it's far from perfect.
|
||||
|
||||
For example, jitter may cause VLC to automatically increase its buffering,
|
||||
causing an unacceptable delay:
|
||||
|
||||
```
|
||||
main input error: ES_OUT_SET_(GROUP_)PCR is called too late (pts_delay increased to 377 ms)
|
||||
```
|
||||
|
||||
In that case, just restart it.
|
||||
|
||||
## Blog post
|
||||
|
||||
- [Audio forwarding on Android 10][blogpost]
|
||||
|
||||
[blogpost]: https://blog.rom1v.com/2020/06/audio-forwarding-on-android-10/
|
||||
1
app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
36
app/build.gradle
Normal file
@@ -0,0 +1,36 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.rom1v.sndcpy"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (project.hasProperty("SNDCPY_STORE_FILE")) {
|
||||
android.signingConfigs {
|
||||
release {
|
||||
// to be defined in ~/.gradle/gradle.properties
|
||||
storeFile file(RELEASE_STORE_FILE)
|
||||
storePassword RELEASE_STORE_PASSWORD
|
||||
keyAlias RELEASE_KEY_ALIAS
|
||||
keyPassword RELEASE_KEY_PASSWORD
|
||||
}
|
||||
}
|
||||
android.buildTypes.release.signingConfig = android.signingConfigs.release
|
||||
}
|
||||
21
app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
29
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.rom1v.sndcpy">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round">
|
||||
<activity
|
||||
android:name="com.rom1v.sndcpy.MainActivity"
|
||||
android:theme="@style/Theme.Transparent"
|
||||
android:showWhenLocked="true"
|
||||
android:turnScreenOn="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="com.rom1v.sndcpy.RecordService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
</application>
|
||||
</manifest>
|
||||
36
app/src/main/java/com/rom1v/sndcpy/MainActivity.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package com.rom1v.sndcpy;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
private static final int REQUEST_CODE_PERMISSION_AUDIO = 1;
|
||||
private static final int REQUEST_CODE_START_CAPTURE = 2;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
String[] permissions = {Manifest.permission.RECORD_AUDIO};
|
||||
requestPermissions(permissions, REQUEST_CODE_PERMISSION_AUDIO);
|
||||
}
|
||||
|
||||
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
|
||||
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
|
||||
startActivityForResult(intent, REQUEST_CODE_START_CAPTURE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == REQUEST_CODE_START_CAPTURE && resultCode == Activity.RESULT_OK) {
|
||||
RecordService.start(this, data);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
}
|
||||
224
app/src/main/java/com/rom1v/sndcpy/RecordService.java
Normal file
@@ -0,0 +1,224 @@
|
||||
package com.rom1v.sndcpy;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioPlaybackCaptureConfiguration;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.projection.MediaProjection;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.net.LocalServerSocket;
|
||||
import android.net.LocalSocket;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class RecordService extends Service {
|
||||
|
||||
private static final String TAG = "sndcpy";
|
||||
private static final String CHANNEL_ID = "sndcpy";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
|
||||
private static final String ACTION_RECORD = "com.rom1v.sndcpy.RECORD";
|
||||
private static final String ACTION_STOP = "com.rom1v.sndcpy.STOP";
|
||||
private static final String EXTRA_MEDIA_PROJECTION_DATA = "mediaProjectionData";
|
||||
|
||||
private static final int MSG_CONNECTION_ESTABLISHED = 1;
|
||||
|
||||
private static final String SOCKET_NAME = "sndcpy";
|
||||
|
||||
|
||||
private static final int SAMPLE_RATE = 48000;
|
||||
private static final int CHANNELS = 2;
|
||||
|
||||
private final Handler handler = new ConnectionHandler(this);
|
||||
private MediaProjectionManager mediaProjectionManager;
|
||||
private MediaProjection mediaProjection;
|
||||
private Thread recorderThread;
|
||||
|
||||
public static void start(Context context, Intent data) {
|
||||
Intent intent = new Intent(context, RecordService.class);
|
||||
intent.setAction(ACTION_RECORD);
|
||||
intent.putExtra(EXTRA_MEDIA_PROJECTION_DATA, data);
|
||||
context.startForegroundService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Notification notification = createNotification(false);
|
||||
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, getString(R.string.app_name), NotificationManager.IMPORTANCE_NONE);
|
||||
getNotificationManager().createNotificationChannel(channel);
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
String action = intent.getAction();
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
if (isRunning()) {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
Intent data = intent.getParcelableExtra(EXTRA_MEDIA_PROJECTION_DATA);
|
||||
mediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
|
||||
mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, data);
|
||||
if (mediaProjection != null) {
|
||||
startRecording();
|
||||
} else {
|
||||
Log.w(TAG, "Failed to capture audio");
|
||||
stopSelf();
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Notification createNotification(boolean established) {
|
||||
Notification.Builder notificationBuilder = new Notification.Builder(this, CHANNEL_ID);
|
||||
notificationBuilder.setContentTitle(getString(R.string.app_name));
|
||||
int textRes = established ? R.string.notification_forwarding : R.string.notification_waiting;
|
||||
notificationBuilder.setContentText(getText(textRes));
|
||||
notificationBuilder.setSmallIcon(R.drawable.ic_album_black_24dp);
|
||||
notificationBuilder.addAction(createStopAction());
|
||||
return notificationBuilder.build();
|
||||
}
|
||||
|
||||
|
||||
private Intent createStopIntent() {
|
||||
Intent intent = new Intent(this, RecordService.class);
|
||||
intent.setAction(ACTION_STOP);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Notification.Action createStopAction() {
|
||||
Intent stopIntent = createStopIntent();
|
||||
PendingIntent stopPendingIntent = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_ONE_SHOT);
|
||||
Icon stopIcon = Icon.createWithResource(this, R.drawable.ic_close_24dp);
|
||||
String stopString = getString(R.string.action_stop);
|
||||
Notification.Action.Builder actionBuilder = new Notification.Action.Builder(stopIcon, stopString, stopPendingIntent);
|
||||
return actionBuilder.build();
|
||||
}
|
||||
|
||||
private static LocalSocket connect() throws IOException {
|
||||
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
|
||||
try {
|
||||
return localServerSocket.accept();
|
||||
} finally {
|
||||
localServerSocket.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static AudioPlaybackCaptureConfiguration createAudioPlaybackCaptureConfig(MediaProjection mediaProjection) {
|
||||
AudioPlaybackCaptureConfiguration.Builder confBuilder = new AudioPlaybackCaptureConfiguration.Builder(mediaProjection);
|
||||
confBuilder.addMatchingUsage(AudioAttributes.USAGE_MEDIA);
|
||||
confBuilder.addMatchingUsage(AudioAttributes.USAGE_GAME);
|
||||
confBuilder.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN);
|
||||
return confBuilder.build();
|
||||
}
|
||||
|
||||
private static AudioFormat createAudioFormat() {
|
||||
AudioFormat.Builder builder = new AudioFormat.Builder();
|
||||
builder.setEncoding(AudioFormat.ENCODING_PCM_16BIT);
|
||||
builder.setSampleRate(SAMPLE_RATE);
|
||||
builder.setChannelMask(CHANNELS == 2 ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private static AudioRecord createAudioRecord(MediaProjection mediaProjection) {
|
||||
AudioRecord.Builder builder = new AudioRecord.Builder();
|
||||
builder.setAudioFormat(createAudioFormat());
|
||||
builder.setBufferSizeInBytes(1024 * 1024);
|
||||
builder.setAudioPlaybackCaptureConfig(createAudioPlaybackCaptureConfig(mediaProjection));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void startRecording() {
|
||||
final AudioRecord recorder = createAudioRecord(mediaProjection);
|
||||
|
||||
recorderThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try (LocalSocket socket = connect()) {
|
||||
handler.sendEmptyMessage(MSG_CONNECTION_ESTABLISHED);
|
||||
|
||||
recorder.startRecording();
|
||||
int BUFFER_MS = 15; // do not buffer more than BUFFER_MS milliseconds
|
||||
byte[] buf = new byte[SAMPLE_RATE * CHANNELS * BUFFER_MS / 1000];
|
||||
while (true) {
|
||||
int r = recorder.read(buf, 0, buf.length);
|
||||
socket.getOutputStream().write(buf, 0, r);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
} finally {
|
||||
recorder.stop();
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
});
|
||||
recorderThread.start();
|
||||
}
|
||||
|
||||
private boolean isRunning() {
|
||||
return recorderThread != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopForeground(true);
|
||||
if (recorderThread != null) {
|
||||
recorderThread.interrupt();
|
||||
recorderThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationManager getNotificationManager() {
|
||||
return (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
}
|
||||
|
||||
private static final class ConnectionHandler extends Handler {
|
||||
|
||||
private RecordService service;
|
||||
|
||||
ConnectionHandler(RecordService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
if (!service.isRunning()) {
|
||||
// if the VPN is not running anymore, ignore obsolete events
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.what == MSG_CONNECTION_ESTABLISHED) {
|
||||
Notification notification = service.createNotification(true);
|
||||
service.getNotificationManager().notify(NOTIFICATION_ID, notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/src/main/res/drawable/ic_album_black_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_close_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
5
app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
6
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<string name="app_name">sndcpy</string>
|
||||
<string name="notification_waiting">Waiting for connection…</string>
|
||||
<string name="notification_forwarding">Audio forwarding enabled</string>
|
||||
<string name="action_stop">Stop</string>
|
||||
</resources>
|
||||
12
app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Transparent" parent="android:Theme">
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
BIN
assets/request.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
25
build.gradle
Normal file
@@ -0,0 +1,25 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.2'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
19
gradle.properties
Normal file
@@ -0,0 +1,19 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Mon Jun 08 22:54:59 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
172
gradlew
vendored
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
84
gradlew.bat
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
10
release.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
VERSION=$(git describe --tags --always)
|
||||
rm -rf dist
|
||||
./gradlew assembleRelease
|
||||
mkdir dist
|
||||
cd dist
|
||||
cp ../app/build/outputs/apk/release/app-release.apk sndcpy.apk
|
||||
cp ../sndcpy .
|
||||
cp ../sndcpy.bat .
|
||||
zip sndcpy-"$VERSION".zip sndcpy.apk sndcpy sndcpy.bat
|
||||
4
run
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
./gradlew assembleDebug
|
||||
SNDCPY_APK=app/build/outputs/apk/debug/app-debug.apk ./sndcpy "$@"
|
||||
2
settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
||||
rootProject.name='sndcpy'
|
||||
include ':app'
|
||||
29
sndcpy
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
ADB=${ADB:-adb}
|
||||
VLC=${VLC:-vlc}
|
||||
SNDCPY_APK=${SNDCPY_APK:-sndcpy.apk}
|
||||
SNDCPY_PORT=${SNDCPY_PORT:-28200}
|
||||
|
||||
serial=
|
||||
if [[ $# -ge 1 ]]
|
||||
then
|
||||
serial="-s $1"
|
||||
echo "Waiting for device $1..."
|
||||
else
|
||||
echo 'Waiting for device...'
|
||||
fi
|
||||
|
||||
"$ADB" $serial wait-for-device
|
||||
"$ADB" $serial install -t -r -g "$SNDCPY_APK" ||
|
||||
{
|
||||
echo 'Uninstalling existing version first...'
|
||||
"$ADB" $serial uninstall com.rom1v.sndcpy
|
||||
"$ADB" $serial install -t -g "$SNDCPY_APK"
|
||||
}
|
||||
|
||||
"$ADB" $serial forward tcp:$SNDCPY_PORT localabstract:sndcpy
|
||||
"$ADB" $serial shell am start com.rom1v.sndcpy/.MainActivity
|
||||
echo "Press Enter once audio capture is authorized on the device to start playing..."
|
||||
read dummy
|
||||
"$VLC" -Idummy --demux rawaud --network-caching=50 --play-and-exit tcp://localhost:"$SNDCPY_PORT"
|
||||
31
sndcpy.bat
Executable file
@@ -0,0 +1,31 @@
|
||||
@echo off
|
||||
if not defined ADB set ADB=adb
|
||||
if not defined VLC set VLC="C:\Program Files\VideoLAN\VLC\vlc.exe"
|
||||
if not defined SNDCPY_APK set SNDCPY_APK=sndcpy.apk
|
||||
if not defined SNDCPY_PORT set SNDCPY_PORT=28200
|
||||
|
||||
if not "%1"=="" (
|
||||
set serial=-s %1
|
||||
echo Waiting for device %1...
|
||||
) else (
|
||||
echo Waiting for device...
|
||||
)
|
||||
|
||||
%ADB% %serial% wait-for-device || goto :error
|
||||
%ADB% %serial% install -t -r -g %SNDCPY_APK% || (
|
||||
echo Uninstalling existing version first...
|
||||
%ADB% %serial% uninstall com.rom1v.sndcpy || goto :error
|
||||
%ADB% %serial% install -t -g %SNDCPY_APK% || goto :error
|
||||
)
|
||||
%ADB% %serial% forward tcp:%SNDCPY_PORT% localabstract:sndcpy || goto :error
|
||||
%ADB% %serial% shell am start com.rom1v.sndcpy/.MainActivity || goto :error
|
||||
echo Press Enter once audio capture is authorized on the device to start playing...
|
||||
pause >nul
|
||||
echo Playing audio...
|
||||
%VLC% -Idummy --demux rawaud --network-caching=50 --play-and-exit tcp://localhost:%SNDCPY_PORT%
|
||||
goto :EOF
|
||||
|
||||
:error
|
||||
echo Failed with error #%errorlevel%.
|
||||
pause
|
||||
exit /b %errorlevel%
|
||||