diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..0c0c338 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..2fc368b 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/README.md b/README.md index 0676801..85bdaae 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,10 @@ Contains some easy-to-use tools to go beyond the level of control allowed by And ### Tools - - [x] **Debloater** - Uninstall system apps and bloatware, with app information from [UAD](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation). - [x] **ThemePatcher** - Unlocks premium content for free, from the Oppo/Realme/Oneplus theme store. -- [x] **LookBack** - Allows downgrade of apps, without uninstallation. - [x] **MixedAudio** - Allows multiple media apps to play at the same time, or mute audio of specific apps. +- [x] **SoundMaster** - Independent volume control for every app, and more! Requires Android 10 or later. + ⚠ SoundMaster may not work on apps with strong copyright protection, like Spotify. In case SoundMaster crashes and some apps lose sound output, use MixedAudio to unmute them. +- [x] **LookBack** - Allows downgrade of apps, without uninstallation. - [x] **ADB Shell** - Manually execute other raw ADB commands. - [x] **Intent Shell** - Allows other apps (Tasker,MacroDroid,etc) to run ADB commands via intent requests. diff --git a/app/build.gradle b/app/build.gradle index cd0fe9d..9ef252c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ android { minSdk 27 targetSdk 34 versionCode 1 - versionName "1.3.0" + versionName "1.4.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -40,6 +40,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.activity:activity:1.9.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/release/app-release.apk b/app/release/app-release.apk index 53a9d9d..474f050 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9bc7ce8..ad0ea1c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,10 @@ + + + + @@ -25,17 +29,29 @@ android:supportsRtl="true" android:theme="@style/Theme.AdbTools" tools:targetApi="31"> + + + android:exported="true" + android:label="Intent Shell"> - + + + diff --git a/app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt b/app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt index 8100c51..b1bcc0c 100644 --- a/app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt +++ b/app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt @@ -1,9 +1,13 @@ package com.legendsayantan.adbtools +import android.annotation.SuppressLint +import android.app.PendingIntent import android.content.ClipData import android.content.ClipboardManager import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.Gravity @@ -14,10 +18,12 @@ import android.widget.LinearLayout import android.widget.ScrollView import android.widget.TextView import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout @@ -26,6 +32,7 @@ import com.legendsayantan.adbtools.lib.Utils.Companion.initialiseStatusBar import java.util.UUID class MainActivity : AppCompatActivity() { + @SuppressLint("LaunchActivityFromNotification") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -43,6 +50,7 @@ class MainActivity : AppCompatActivity() { val cardThemePatcher = findViewById(R.id.cardThemePatcher) val cardLookBack = findViewById(R.id.cardLookBack) val cardMixedAudio = findViewById(R.id.cardMixedAudio) + val cardSoundMaster = findViewById(R.id.cardSoundMaster) val cardShell = findViewById(R.id.cardShell) val cardIntentShell = findViewById(R.id.cardIntentShell) cardDebloat.setOnClickListener { @@ -77,6 +85,37 @@ class MainActivity : AppCompatActivity() { ) ) } + cardSoundMaster.setOnClickListener { + if(Build.VERSION.SDK_INT + get() = try { + File(applicationContext.filesDir, "soundmaster.txt").readText().split("\n") + .toMutableList() + } catch (f: FileNotFoundException) { + mutableListOf() + } + set(value) { + val file = File(applicationContext.filesDir, "soundmaster.txt") + if (!file.exists()) { + file.parentFile?.mkdirs() + file.createNewFile() + } + file.writeText(value.joinToString("\n")) + } + + val volumeBarView by lazy { findViewById(R.id.volumeBars) } + + @SuppressLint("ApplySharedPref") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_sound_master) + initialiseStatusBar() + //new slider + findViewById(R.id.newSlider).setOnClickListener { + NewSliderDialog(this@SoundMasterActivity) { pkg -> + val newPackages = packages + newPackages.add(pkg) + packages = newPackages + if (SoundMasterService.running) SoundMasterService.onDynamicAttach(pkg) + updateSliders() + }.show() + } + + //outside touch + findViewById(R.id.main).setOnClickListener { + finish() + } + } + + override fun onResume() { + updateBtnState() + updateSliders() + super.onResume() + } + + fun updateBtnState() { + val btnImage = findViewById(R.id.playPauseButton) + btnImage.setImageResource(if (SoundMasterService.running) R.drawable.baseline_stop_24 else R.drawable.baseline_play_arrow_24) + btnImage.setOnClickListener { + val state = SoundMasterService.running + if (state) { + stopService(Intent(this, SoundMasterService::class.java)) + } else if (packages.size > 0) { + if (packages.isEmpty()) { + Toast.makeText( + applicationContext, + "No apps selected to control", + Toast.LENGTH_SHORT + ) + .show() + } else { + ShizukuRunner.runAdbCommand("pm grant ${baseContext.packageName} android.permission.RECORD_AUDIO", + object : ShizukuRunner.CommandResultListener { + override fun onCommandResult(output: String, done: Boolean) { + if (done) { + ShizukuRunner.runAdbCommand("appops set ${baseContext.packageName} PROJECT_MEDIA allow", + object : ShizukuRunner.CommandResultListener { + override fun onCommandResult( + output: String, + done: Boolean + ) { + if (done) { + mediaProjectionManager = + applicationContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + startActivityForResult( + mediaProjectionManager.createScreenCaptureIntent(), + MEDIA_PROJECTION_REQUEST_CODE + ) + } + } + }) + } + } + }) + } + } + var count = 0 + Timer().schedule(timerTask { + if (SoundMasterService.running != state) { + updateBtnState() + updateSliders() + cancel() + } else count++ + if (count > 50) cancel() + }, 500, 500) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == MEDIA_PROJECTION_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + Toast.makeText( + applicationContext, + "Controlling audio from selected apps", + Toast.LENGTH_SHORT + ).show() + SoundMasterService.projectionData = data + startService(Intent(this, SoundMasterService::class.java).apply { + putExtra("packages", packages.toTypedArray()) + }) + } else { + Toast.makeText( + this, "Request to obtain MediaProjection denied.", + Toast.LENGTH_SHORT + ).show() + } + } + } + + private fun updateSliders() { + findViewById(R.id.none).visibility = + if (packages.size > 0) View.GONE else View.VISIBLE + Thread { + val sliderMap = HashMap() + for (pkg in packages) { + val volume = SoundMasterService.getVolumeOf(pkg) + sliderMap[pkg] = volume + } + val adapter = + VolumeBarAdapter(this@SoundMasterActivity, sliderMap, { app, vol -> + SoundMasterService.setVolumeOf(app, vol) + }, { + val newPackages = packages + newPackages.remove(it) + packages = newPackages + updateSliders() + SoundMasterService.onDynamicDetach(it) + }, { app, sliderIndex -> + if (sliderIndex == 0) SoundMasterService.getBalanceOf(app) + else SoundMasterService.getBandValueOf(app, sliderIndex - 1) + }, { app, slider, value -> + if (slider == 0) SoundMasterService.setBalanceOf(app, value) + else SoundMasterService.setBandValueOf(app, slider - 1, value) + }) + runOnUiThread { + volumeBarView.adapter = adapter + volumeBarView.invalidate() + } + }.start() + } + + companion object { + private const val MEDIA_PROJECTION_REQUEST_CODE = 13 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/legendsayantan/adbtools/adapters/VolumeBarAdapter.kt b/app/src/main/java/com/legendsayantan/adbtools/adapters/VolumeBarAdapter.kt new file mode 100644 index 0000000..bbbffc0 --- /dev/null +++ b/app/src/main/java/com/legendsayantan/adbtools/adapters/VolumeBarAdapter.kt @@ -0,0 +1,94 @@ +package com.legendsayantan.adbtools.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.card.MaterialCardView +import com.google.android.material.slider.Slider +import com.legendsayantan.adbtools.R + +/** + * @author legendsayantan + */ +class VolumeBarAdapter( + val context: Context, + val data: HashMap, + val onVolumeChanged: (String, Float) -> Unit, + val onItemDetached: (String) -> Unit, + val onSliderGet:(String,Int)->Float, + val onSliderSet:(String,Int,Float)->Unit +) : RecyclerView.Adapter() { + inner class VolumeBarHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val image = itemView.findViewById(R.id.image) + val volumeBar = itemView.findViewById(R.id.volume) + val expand = itemView.findViewById(R.id.expandBtn) + val expanded = itemView.findViewById(R.id.expanded) + val otherSliders = listOf( + itemView.findViewById(R.id.balance), + itemView.findViewById(R.id.lows), + itemView.findViewById(R.id.mids), + itemView.findViewById(R.id.highs) + ) + val resetBtns = listOf( + itemView.findViewById(R.id.balanceReset), + itemView.findViewById(R.id.lowReset), + itemView.findViewById(R.id.midReset), + itemView.findViewById(R.id.highReset) + ) + val detachBtn = itemView.findViewById(R.id.detachBtn) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VolumeBarHolder { + val itemView = + LayoutInflater.from(parent.context).inflate(R.layout.item_volumebar, parent, false) + return VolumeBarHolder(itemView) + } + + override fun getItemCount(): Int { + return data.size + } + + override fun onBindViewHolder(holder: VolumeBarHolder, position: Int) { + val currentItem = data.entries.elementAt(position) + try { + holder.image.setImageDrawable( + context.packageManager.getApplicationIcon(currentItem.key) + ) + } catch (_: Exception) { + } + holder.volumeBar.value = currentItem.value + holder.volumeBar.addOnChangeListener { slider, value, fromUser -> + onVolumeChanged(currentItem.key, value) + } + holder.expand.setOnClickListener { + if (holder.expanded.visibility == View.VISIBLE) { + holder.expanded.visibility = View.GONE + holder.expand.animate().rotationX(0f) + } else { + holder.expanded.visibility = View.VISIBLE + holder.expand.animate().rotationX(180f) + } + } + + holder.otherSliders.forEachIndexed { index, slider -> + slider.value = onSliderGet(currentItem.key,index) + slider.addOnChangeListener { s, value, fromUser -> + onSliderSet(currentItem.key,index,value) + } + } + + //reset + holder.resetBtns.forEachIndexed { index, imageView -> + imageView.setOnClickListener { + holder.otherSliders[index].value = if (index == 0) 0f else 50f + } + } + + //detach + holder.detachBtn.setOnClickListener { onItemDetached(currentItem.key) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/legendsayantan/adbtools/dialog/NewSliderDialog.kt b/app/src/main/java/com/legendsayantan/adbtools/dialog/NewSliderDialog.kt new file mode 100644 index 0000000..4a82644 --- /dev/null +++ b/app/src/main/java/com/legendsayantan/adbtools/dialog/NewSliderDialog.kt @@ -0,0 +1,57 @@ +package com.legendsayantan.adbtools.dialog + +import android.app.Dialog +import android.content.Context +import android.os.Handler +import android.view.WindowManager +import android.widget.ArrayAdapter +import androidx.recyclerview.widget.RecyclerView +import com.legendsayantan.adbtools.R +import com.legendsayantan.adbtools.adapters.SimpleAdapter +import com.legendsayantan.adbtools.lib.ShizukuRunner +import com.legendsayantan.adbtools.lib.Utils +import com.legendsayantan.adbtools.lib.Utils.Companion.loadApps + +/** + * @author legendsayantan + */ +class NewSliderDialog(context: Context,val onSelection:(String)->Unit) : Dialog(context) { + init { + setContentView(R.layout.dialog_new_slider) + window?.setBackgroundDrawableResource(android.R.color.transparent) + window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT) + } + + override fun show() { + super.show() + val list = findViewById(R.id.appSelection) + Thread { + loadApps { packageList -> + val packageMap = linkedMapOf() + val orderMap = linkedMapOf() + packageList.forEach { + if (it.isNotBlank() && (!it.equals(context.packageName))) orderMap.putIfAbsent( + it, + Utils.getAppNameFromPackage(context, it) + ) + } + orderMap.entries.sortedBy { it.value }.forEach { (k, v) -> + packageMap.putIfAbsent(k, v) + } + Handler(context.mainLooper).post { + val adapter = SimpleAdapter( + packageMap.values.toList() + ){ + dismiss() + onSelection(packageMap.keys.toList()[it]) + } + list.adapter = adapter + list.invalidate() + } + } + }.start() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/legendsayantan/adbtools/lib/Utils.kt b/app/src/main/java/com/legendsayantan/adbtools/lib/Utils.kt index 0e37548..1ac54c9 100644 --- a/app/src/main/java/com/legendsayantan/adbtools/lib/Utils.kt +++ b/app/src/main/java/com/legendsayantan/adbtools/lib/Utils.kt @@ -56,15 +56,6 @@ class Utils { fun Context.postNotification(title: String, message: String, success: Boolean = true) { val channelId = "notifications" - // Create a notification channel (for Android 8.0 and higher). - val channel = NotificationChannel( - channelId, - "Notifications", - NotificationManager.IMPORTANCE_HIGH - ) - val notificationManager = getSystemService(NotificationManager::class.java) - notificationManager.createNotificationChannel(channel) - // Create the notification using NotificationCompat. val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId) .setSmallIcon(if(success)R.drawable.baseline_verified_24 else R.drawable.outline_info_24) // Replace with your notification icon. @@ -81,5 +72,38 @@ class Utils { } } + fun Context.initialiseNotiChannel(){ + val channelId = "notifications" + + // Create a notification channel (for Android 8.0 and higher). + val channel = NotificationChannel( + channelId, + "Notifications", + NotificationManager.IMPORTANCE_HIGH + ) + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + + fun loadApps(callback: (List) -> Unit) { + ShizukuRunner.runAdbCommand( + "pm list packages", + object : ShizukuRunner.CommandResultListener { + override fun onCommandResult(output: String, done: Boolean) { + val packages = output.replace("package:", "").split("\n") + callback(packages) + } + }) + } + + fun getAppUidFromPackage(context: Context, packageName: String): Int { + return context.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).uid + } + + fun getAppNameFromPackage(context: Context, packageName: String): String { + return context.packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).loadLabel(context.packageManager).toString() + } + + } } \ No newline at end of file diff --git a/app/src/main/java/com/legendsayantan/adbtools/services/AudioThread.kt b/app/src/main/java/com/legendsayantan/adbtools/services/AudioThread.kt new file mode 100644 index 0000000..a4a020b --- /dev/null +++ b/app/src/main/java/com/legendsayantan/adbtools/services/AudioThread.kt @@ -0,0 +1,200 @@ +package com.legendsayantan.adbtools.services + +import android.content.Context +import android.content.pm.PackageManager +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioPlaybackCaptureConfiguration +import android.media.AudioRecord +import android.media.AudioTrack +import android.media.audiofx.AcousticEchoCanceler +import android.media.audiofx.Equalizer +import android.media.audiofx.LoudnessEnhancer +import android.media.audiofx.NoiseSuppressor +import android.media.projection.MediaProjection +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.ActivityCompat +import com.legendsayantan.adbtools.lib.ShizukuRunner +import com.legendsayantan.adbtools.lib.Utils +import com.legendsayantan.adbtools.services.SoundMasterService.Companion.CHANNEL +import com.legendsayantan.adbtools.services.SoundMasterService.Companion.LOG_TAG +import com.legendsayantan.adbtools.services.SoundMasterService.Companion.SAMPLE_RATE +import com.legendsayantan.adbtools.services.SoundMasterService.Companion.bandDivision + +/** + * @author legendsayantan + */ +class AudioThread(val context: Context, val pkg:String, private val mediaProjection: MediaProjection) : Thread() { + var playback = true + var volume : Float = 1f + var targetVolume : Float = 100f + val dataBuffer = ByteArray(SoundMasterService.BUF_SIZE) + private var stereoGainFactor = arrayOf(1f,1f) + private var bandCompensations = arrayOf(0, 0, 0) + + val equalizer by lazy { Equalizer(0, mTrack.audioSessionId) } + val enhancer by lazy {LoudnessEnhancer(mTrack.audioSessionId)} + val suppress by lazy {NoiseSuppressor.create(mTrack.audioSessionId)} + val echoCancel by lazy {AcousticEchoCanceler.create(mTrack.audioSessionId)} + + lateinit var mRecord: AudioRecord + lateinit var mTrack : AudioTrack + var savedBands = arrayOf(50f,50f,50f) + override fun start() { + ShizukuRunner.runAdbCommand("appops set $pkg PLAY_AUDIO deny",object : ShizukuRunner.CommandResultListener{ + override fun onCommandResult(output: String, done: Boolean) {} + }) + super.start() + } + @RequiresApi(Build.VERSION_CODES.Q) + override fun run() { + if (ActivityCompat.checkSelfPermission( + context, + android.Manifest.permission.RECORD_AUDIO + ) != PackageManager.PERMISSION_GRANTED + ) { + interrupt() + return + } + + try { + val config = AudioPlaybackCaptureConfiguration.Builder(mediaProjection) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .addMatchingUsage(AudioAttributes.USAGE_ALARM) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) +// .addMatchingUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) //causes failure + .addMatchingUsage(AudioAttributes.USAGE_ASSISTANT) + .addMatchingUsage(AudioAttributes.USAGE_NOTIFICATION) + .addMatchingUid(Utils.getAppUidFromPackage(context, pkg)) + .build() + val audioFormat = AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(SAMPLE_RATE) + .setChannelMask(CHANNEL) + .build() + + mRecord = AudioRecord.Builder() + .setAudioFormat(audioFormat) + .setBufferSizeInBytes( + AudioRecord.getMinBufferSize( + SAMPLE_RATE, + CHANNEL, + AudioFormat.ENCODING_PCM_16BIT + ) + ) + .setAudioPlaybackCaptureConfig(config) + .build() + + mTrack = AudioTrack( + AudioManager.STREAM_MUSIC, + SAMPLE_RATE, CHANNEL, + AudioFormat.ENCODING_PCM_16BIT, AudioTrack.getMinBufferSize( + SAMPLE_RATE, + CHANNEL, + AudioFormat.ENCODING_PCM_16BIT + ), + AudioTrack.MODE_STREAM + ) + + mTrack.playbackRate = SAMPLE_RATE + + setCurrentVolume(targetVolume) + } catch (e: Exception) { + Log.e( + "Error", + "Initializing Audio Record and Play objects Failed ${e.message} for $pkg" + ) + } + try { + mRecord.startRecording() + Log.i(LOG_TAG, "Audio Recording started") + mTrack.play() + Log.i(LOG_TAG, "Audio Playing started") + try { + equalizer.enabled = true + } catch (e: Exception) { + Log.i(LOG_TAG, "EQ NOT SUPPORTED") + } + while (playback) { + mRecord.read(dataBuffer, 0, SoundMasterService.BUF_SIZE) + mTrack.write(dataBuffer, 0, dataBuffer.size) + } + }catch (e:Exception){ + Log.e(LOG_TAG,"Error in VolumeThread: ${e.message}") + } + } + + fun setCurrentVolume(it:Float){ + volume = (it / 100f).coerceAtMost(1f) + mTrack.setStereoVolume(volume * stereoGainFactor[0], volume * stereoGainFactor[1]) + try { + enhancer.enabled = it > 100 + if (it > 100) enhancer.setTargetGain(((it.toInt() - 100) * 150)) + } catch (e: Exception) { + Log.i(LOG_TAG, "ENHANCER NOT SUPPORTED") + } + try { + suppress.enabled = it > 100 + } catch (e: Exception) { + Log.i(LOG_TAG, "NOISE SUPPRESSION NOT SUPPORTED") + } + try { + echoCancel.enabled = it > 100 + } catch (e: Exception) { + Log.i(LOG_TAG, "ECHO CANCELLATION NOT SUPPORTED") + } + } + + fun getBalance(): Float { + return (100 - (stereoGainFactor[0] * 100).toInt()) - (100 - (stereoGainFactor[1] * 100)) + } + + fun setBalance(value:Float){ + stereoGainFactor = arrayOf( + if (value <= 0) 1f else 1f - (value / 100f), + if (value >= 0) 1f else 1f + (value / 100f) + ) + mTrack.setStereoVolume(volume * stereoGainFactor[0], volume * stereoGainFactor[1]) + } + + fun setBand(band:Int,value:Float){ + savedBands[band] = value + updateBandLevel(band, value) + } + private fun updateBandLevel(bandRange: Int, percentage: Float = -1f) { + try { + // Iterate through the frequency bands + val modifiedLevel = + equalizer.bandLevelRange[0] + + ((equalizer.bandLevelRange[1] - equalizer.bandLevelRange[0]) * percentage / 100f) + + bandCompensations[bandRange] + for (i in 0 until equalizer.numberOfBands) { + val centerFreq = equalizer.getCenterFreq(i.toShort()) / 1000 + // Reduce gain for specific frequencies + if (centerFreq in bandDivision[bandRange]..bandDivision[bandRange + 1]) { + equalizer.setBandLevel( + i.toShort(), + if (percentage >= 0) modifiedLevel.toInt().toShort() + else (equalizer.getBandLevel(i.toShort()) + bandCompensations[bandRange]).toShort() + .coerceIn(equalizer.bandLevelRange[0], equalizer.bandLevelRange[1]) + ) + } + } + } catch (e: Exception) { + Log.i(LOG_TAG, "EQ NOT SUPPORTED") + } + } + + override fun interrupt() { + playback = false + ShizukuRunner.runAdbCommand("appops set $pkg PLAY_AUDIO allow",object : ShizukuRunner.CommandResultListener{ + override fun onCommandResult(output: String, done: Boolean) {} + }) + super.interrupt() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/legendsayantan/adbtools/services/SoundMasterService.kt b/app/src/main/java/com/legendsayantan/adbtools/services/SoundMasterService.kt new file mode 100644 index 0000000..f18b254 --- /dev/null +++ b/app/src/main/java/com/legendsayantan/adbtools/services/SoundMasterService.kt @@ -0,0 +1,139 @@ +package com.legendsayantan.adbtools.services + +import android.app.Activity +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.projection.MediaProjection +import android.media.projection.MediaProjectionManager +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.legendsayantan.adbtools.R + +class SoundMasterService : Service() { + private var mediaProjectionManager: MediaProjectionManager? = null + private var mediaProjection: MediaProjection? = null + var threadMap = hashMapOf() + var apps = mutableListOf() + override fun onBind(intent: Intent): IBinder { + return null!! + } + + override fun onCreate() { + super.onCreate() + + //foreground service + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val notification = NotificationCompat.Builder(this, "notifications") + .setContentTitle(applicationContext.getString(R.string.soundmaster)+" is controlling apps.") + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build() + startForeground( + 1, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION + ) + } + mediaProjectionManager = + applicationContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + + getVolumeOf = { + (threadMap[it]?.volume?.times(100f)) ?: volumeTemp[it] ?: 100f + } + + setVolumeOf = { pkg, vol -> + threadMap[pkg].let { + if (it != null) it.setCurrentVolume(vol) else volumeTemp[pkg] = vol + } + } + + getBalanceOf = { + threadMap[it]?.getBalance() ?: 0f + } + + setBalanceOf = {it,value-> + threadMap[it]?.setBalance(value) + } + + getBandValueOf = {it,band-> + threadMap[it]?.savedBands?.get(band) ?: 50f + } + + setBandValueOf = { it,band,value-> + threadMap[it]?.setBand(band,value) + } + + onDynamicAttach = { + if (!apps.contains(it)) { + apps += it + val mThread = AudioThread(applicationContext, it, mediaProjection!!) + threadMap[it] = mThread + mThread.start() + } + } + + onDynamicDetach = { pkg-> + if (apps.contains(pkg)) { + apps.remove(pkg) + threadMap[pkg]?.interrupt() + threadMap.remove(pkg) + } + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent != null) { + apps = intent.getStringArrayExtra("packages")?.toMutableList() ?: mutableListOf() + if (apps.isNotEmpty()) { + running = true + mediaProjection = mediaProjectionManager?.getMediaProjection( + Activity.RESULT_OK, + projectionData!! + ) as MediaProjection + apps.forEach { + val mThread = AudioThread(applicationContext, it, mediaProjection!!).apply { + targetVolume = volumeTemp[it]?:100f + } + threadMap[it] = mThread + mThread.start() + } + } + } + return START_STICKY + } + + override fun onDestroy() { + running = false + threadMap.forEach { it.value.interrupt() } + super.onDestroy() + } + + companion object { + var running = false + var projectionData: Intent? = null + var onDynamicAttach: (String) -> Unit = {} + var onDynamicDetach: (String) -> Unit = {} + var volumeTemp = HashMap() + var setVolumeOf: (String, Float) -> Unit = { a, b -> volumeTemp[a] = b } + var getVolumeOf: (String) -> Float = { p -> volumeTemp[p]?:100f } + var setBalanceOf: (String, Float) -> Unit = { a, b -> } + var getBalanceOf: (String) -> Float = {_->0f} + var setBandValueOf: (String,Int,Float) ->Unit = {_,_,_->} + var getBandValueOf:(String,Int)->Float = {_,_-> 50f} + + const val SAMPLE_RATE = 44100 + const val LOG_TAG = "VolumePlus" + const val CHANNEL = AudioFormat.CHANNEL_IN_STEREO + val BUF_SIZE = + AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL, AudioFormat.ENCODING_PCM_16BIT) + + val bandDivision = arrayOf(0, 250, 2000, 20000) + } + + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_add_24.xml b/app/src/main/res/drawable/baseline_add_24.xml new file mode 100644 index 0000000..9f83b8f --- /dev/null +++ b/app/src/main/res/drawable/baseline_add_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_close_24.xml b/app/src/main/res/drawable/baseline_close_24.xml new file mode 100644 index 0000000..f8ca0c6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_close_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_delete_24.xml b/app/src/main/res/drawable/baseline_delete_24.xml new file mode 100644 index 0000000..883bcaa --- /dev/null +++ b/app/src/main/res/drawable/baseline_delete_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_expand_more_24.xml b/app/src/main/res/drawable/baseline_expand_more_24.xml new file mode 100644 index 0000000..26be5ff --- /dev/null +++ b/app/src/main/res/drawable/baseline_expand_more_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_play_arrow_24.xml b/app/src/main/res/drawable/baseline_play_arrow_24.xml new file mode 100644 index 0000000..b176182 --- /dev/null +++ b/app/src/main/res/drawable/baseline_play_arrow_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_stop_24.xml b/app/src/main/res/drawable/baseline_stop_24.xml new file mode 100644 index 0000000..817d57b --- /dev/null +++ b/app/src/main/res/drawable/baseline_stop_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/baseline_stop_circle_24.xml b/app/src/main/res/drawable/baseline_stop_circle_24.xml new file mode 100644 index 0000000..3af798e --- /dev/null +++ b/app/src/main/res/drawable/baseline_stop_circle_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 0ff8f1f..59a8d5c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -77,7 +77,7 @@ + android:text="@string/desc_debloater" /> @@ -123,7 +123,7 @@ + android:text="@string/desc_themepatcher" /> + + android:text="@string/desc_mixedaudio" /> + + + + + + + + + + + + + + + + + + + android:text="@string/desc_lookback" /> + - + android:text="@string/desc_adb_shell" /> @@ -306,7 +353,7 @@ + android:text="@string/desc_intent_shell" /> diff --git a/app/src/main/res/layout/activity_sound_master.xml b/app/src/main/res/layout/activity_sound_master.xml new file mode 100644 index 0000000..8e79e10 --- /dev/null +++ b/app/src/main/res/layout/activity_sound_master.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_new_slider.xml b/app/src/main/res/layout/dialog_new_slider.xml new file mode 100644 index 0000000..76a8825 --- /dev/null +++ b/app/src/main/res/layout/dialog_new_slider.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_volumebar.xml b/app/src/main/res/layout/item_volumebar.xml new file mode 100644 index 0000000..b910fac --- /dev/null +++ b/app/src/main/res/layout/item_volumebar.xml @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 7b34255..2c44e5f 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -7,4 +7,10 @@ @color/theme_light @color/theme_light + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb0d803..cda5781 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,23 @@ ShizuTools This app requires Shizuku.\nPlease make sure you have shizuku installed and running. Force preventing media interruption may crash some apps, and doing so on system apps may crash your entire system. + Debloater + ThemePatcher + LookBack + MixedAudio + ADB Shell + Intent Shell + Execute raw adb commands, on-demand through intents. This is still experimental. + Execute raw adb commands, on-device. + Allow multiple apps to play audio at the same time, or mute specific apps. + Seamlessly downgrade apps to older versions, without re-install. + Use premium themes from the theme store in oppo, oneplus or realme devices for free. + Uninstall unnecessary system apps to remove ads, save battery and improve performance. + 100 + % + SoundMaster + Control audio output of individual apps, on Android 10 or newer device. + Select which app to control : + No sliders added + New Slider \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 820bf35..8ce402e 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -9,4 +9,11 @@ \ No newline at end of file