diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..87bee47 --- /dev/null +++ b/.claude/settings.local.json @@ -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}')" + ] + } +} diff --git a/README.md b/README.md index 8a881bd..c1315c2 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 :> diff --git a/app/build.gradle b/app/build.gradle index 14d651c..8808c67 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } } @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 802f2f0..b902803 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ - + - diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 8f5bae7..0b6a2f9 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -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 diff --git a/app/src/main/cpp/libusb b/app/src/main/cpp/libusb index 1a90627..b54814c 160000 --- a/app/src/main/cpp/libusb +++ b/app/src/main/cpp/libusb @@ -1 +1 @@ -Subproject commit 1a906274a66dd58bf81836db1306902d4a7dc185 +Subproject commit b54814c141f6fb68183a38641506e0ec51693124 diff --git a/app/src/main/java/com/example/libusbAndroidTest/MainActivity.java b/app/src/main/java/com/example/libusbAndroidTest/MainActivity.java index ed3ef9f..3a18ccb 100644 --- a/app/src/main/java/com/example/libusbAndroidTest/MainActivity.java +++ b/app/src/main/java/com/example/libusbAndroidTest/MainActivity.java @@ -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; @@ -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; @@ -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() { @@ -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); @@ -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 deviceList = usbManager.getDeviceList(); + for (UsbDevice device : deviceList.values()) { if(usbManager.hasPermission(device)) { @@ -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; } @@ -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; - } - } - } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 14ee2dc..9f58b81 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,57 +6,15 @@ android:layout_height="match_parent" tools:context=".MainActivity"> -