Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(./gradlew -h)",
"Bash(chmod +x ./gradlew)",
"Bash(./gradlew tasks *)",
"Bash(./gradlew assembleDebug)",
"Bash(./gradlew clean *)",
"Bash(./gradlew assembleRelease)",
"Bash(awk '{print $NF}')"
]
}
}
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
Simple application to set the USB DAC volume on UNROOTED Android Devices

## This is a fork of the original app
I improved it so it detects when you connect a usb dac, automatically unlocks the volume and exits. You need to enable recording permission (is necessary on latest android versions) and enable auto exiting.
Works flawlessly with the Apple USB to Jack adapter :)
Go to releases for the latest alpha version.
I improved it so it only open the app when apple dongle detected and have options for auto exit the app (thanks to [polkaulfield](https://github.com/polkaulfield/usbDacVolumeAndroid))

# Why
The vast majority of high end android smartphones sold today do not contain a 3.5mm headphone jack. To remedy this, most people will use a USB-C to 3.5mm DAC. However, there are certian DACs (namely the Apple USB- C DAC) that do not default to their highest output setting. On most platforms (Windows, Linux, macOS, iOS), this isn't an issue because they either force the highest DAC volume and adjust their own mixer volume, or they control the DAC volume explicitly. Android does neither, so as a result, some DACs are quieter than they possibly can be.
Expand Down Expand Up @@ -40,3 +38,4 @@ You will temporarily lose sound during the setting of volume, however you should

# Special thanks to:
[ibaiGorordo](https://github.com/ibaiGorordo/libusbAndroidTest) for most of the code :>
[polkaulfield](https://github.com/polkaulfield/usbDacVolumeAndroid) for auto exit code :>
21 changes: 15 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,30 @@ plugins {
}

android {
compileSdk 30
compileSdk 36
ndkVersion "28.2.13676358"

defaultConfig {
applicationId 'com.example.libusbAndroidTest'
minSdk 28
targetSdk 30
versionCode 1
versionName "1.0"
targetSdk 36
versionCode 2
versionName "1.1"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags '-std=c++17'
arguments '-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON', '-DCMAKE_ANDROID_NATIVE_SUPPORT_FLEXIBLE_PAGE_SIZES=ON'
}
}
}

buildTypes {
release {
minifyEnabled false
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
Expand All @@ -39,10 +43,15 @@ android {
viewBinding true
}
namespace 'com.example.libusbAndroidTest'
packagingOptions {
jniLibs {
useLegacyPackaging = false
}
}
}

dependencies {

implementation 'androidx.core:core:1.9.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<!-- Used for auto starting when usb is connected -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<uses-feature android:name="android.hardware.usb.host" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand All @@ -24,7 +24,8 @@
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>

<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
</application>
Expand Down
1 change: 0 additions & 1 deletion app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ cmake_minimum_required(VERSION 3.10.2)

project("libusbAndroidTest")


###### libusb ######
set(LIBUSB_DIR ${CMAKE_SOURCE_DIR}/libusb)
set(LIBUSB_SOURCES ${LIBUSB_DIR}/libusb/core.c
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/cpp/libusb
103 changes: 60 additions & 43 deletions app/src/main/java/com/example/libusbAndroidTest/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
package com.example.libusbAndroidTest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
Expand All @@ -21,12 +15,15 @@
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import com.example.libusbAndroidTest.databinding.ActivityMainBinding;

import java.util.HashMap;
Expand All @@ -41,20 +38,26 @@ public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private UsbManager usbManager;

private TextView tv;
private TextView tvDeviceName;
private EditText volInput;

private CheckBox autoApply;
private CheckBox quitAfterApply;

private Button recordPermissionButton;

private int deviceDescriptor = -1;
private static String deviceName;

// in Hex 0x5AC is apple vendor id
private static final String APPLE_VENDOR_ID = "1452";

// in Hex 0x110A is apple dongle product id
private static final String APPLE_DONGLE_PRODUCT_ID = "4362";


private static final String TAG = "USB DAC Volume Adjustment" ;
private static final String ACTION_USB_PERMISSION =
"com.android.example.USB_PERMISSION";
private static final int RECORD_AUDIO_PERMISSION_CODE = 1;

private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {

Expand All @@ -77,8 +80,22 @@ public void onReceive(Context context, Intent intent) {
}
};

private boolean isAppleDongle(UsbDevice device){
return device.getVendorId() == Integer.parseInt(APPLE_VENDOR_ID) && device.getProductId() == Integer.parseInt(APPLE_DONGLE_PRODUCT_ID);
}

protected void connectDevice(UsbDevice device)
{
Log.d("UsbDevice", "device: " + device.getDeviceName() + " " + device.getVendorId() + " " + device.getProductId());
Log.d("UsbDevice",
"class: " + device.getDeviceClass() + " " + device.getDeviceSubclass() + " " + device.getDeviceProtocol());
boolean isAppleDongle = isAppleDongle(device);

String vendorId = "0x" + Integer.toHexString(device.getVendorId()).toUpperCase();
String productId = "0x" + Integer.toHexString(device.getProductId()).toUpperCase();
String deviceVendorIdAndProductId = isAppleDongle ? "Apple Dongle Detected!" :
"vendorId: " + vendorId + " productId:" + productId;
tvDeviceName.setText(deviceVendorIdAndProductId);
UsbInterface intf = device.getInterface(0);
UsbEndpoint endpoint = intf.getEndpoint(0);
UsbDeviceConnection connection = usbManager.openDevice(device);
Expand All @@ -88,17 +105,20 @@ protected void connectDevice(UsbDevice device)
deviceName = initializeNativeDevice(fileDescriptor);
deviceDescriptor = fileDescriptor;

if(autoApply.isChecked() && quitAfterApply.isChecked()){
if(autoApply.isChecked()){
setDeviceVolume(fileDescriptor);
finishAndRemoveTask();
if(quitAfterApply.isChecked()){
finishAndRemoveTask();
}
}
}

protected void checkUsbDevices()
{
PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE);

HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();

for (UsbDevice device : deviceList.values()) {
if(usbManager.hasPermission(device))
{
Expand All @@ -116,38 +136,54 @@ protected void onCreate(Bundle savedInstanceState) {

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
tv = binding.sampleText;
tvDeviceName = binding.deviceName;
volInput = binding.volume;
autoApply = binding.autoApply;
quitAfterApply = binding.quitAfterApply;
recordPermissionButton = binding.recordPermissionButton;

SharedPreferences settings = getApplicationContext().getSharedPreferences("myPrefs", 0);
volInput.setText(settings.getString("volume", "0000"));
volInput.setText(settings.getString("volume", "007f"));
autoApply.setChecked(settings.getBoolean("autoApply", false));
quitAfterApply.setChecked(settings.getBoolean("quitAfterApply", false));

if (ContextCompat.checkSelfPermission(this,
Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED) {
recordPermissionButton.setEnabled(false);
}

// Initialize UsbManager
usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);

// Initialize the receiver for getting the device permission
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
registerReceiver(usbReceiver, filter);
ContextCompat.registerReceiver(this, usbReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);

checkUsbDevices();

requestRecordAudioPermission();
}

private void requestRecordAudioPermission() {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO)
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{android.Manifest.permission.RECORD_AUDIO},
RECORD_AUDIO_PERMISSION_CODE);
}
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == RECORD_AUDIO_PERMISSION_CODE) {
if (grantResults.length > 0 && grantResults[0] == android.content.pm.PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "RECORD_AUDIO permission granted");
} else {
Log.d(TAG, "RECORD_AUDIO permission denied");
}
}
}

public void applyButtonPressed(View view){
String volume = volInput.getText().toString();

if(deviceDescriptor < 0){
tv.setBackgroundColor(Color.RED);
tvDeviceName.setBackgroundColor(Color.RED);
return;
}

Expand Down Expand Up @@ -208,23 +244,4 @@ public void setDeviceVolume(int fileDescriptor){
setDeviceVolume(fileDescriptor, hexStringToByteArray(volume));
Toast.makeText(getApplicationContext(), "Volume set for DAC!", Toast.LENGTH_LONG).show();
}

// Trigger the permission popup

private final int CALLBACK_PERMISSION = 1;
public void recordPermissionButtonPressed(View view) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, CALLBACK_PERMISSION);
}

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == CALLBACK_PERMISSION) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
recordPermissionButton.setEnabled(false);
return;
}
}
}
}
Loading