diff --git a/app/build.gradle b/app/build.gradle index f442a4c..c830a91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ android { minSdk 27 targetSdk 34 versionCode 1 - versionName "1.4.0" + versionName "1.4.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/release/app-release.apk b/app/release/app-release.apk index 025ca9c..5f40095 100644 Binary files a/app/release/app-release.apk and b/app/release/app-release.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 55e55be..559cf10 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -12,7 +12,7 @@ "filters": [], "attributes": [], "versionCode": 1, - "versionName": "1.4.0", + "versionName": "1.4.1", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad0ea1c..e868867 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ - + + (R.id.apps_list) + list = findViewById(R.id.apps_list) ShizukuRunner.runAdbCommand("pm grant $packageName android.permission.QUERY_ALL_PACKAGES", object : ShizukuRunner.CommandResultListener { override fun onCommandResult(output: String, done: Boolean) {} diff --git a/app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt b/app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt index 6ed5b82..e6e607f 100644 --- a/app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt +++ b/app/src/main/java/com/legendsayantan/adbtools/MainActivity.kt @@ -29,6 +29,7 @@ import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import com.legendsayantan.adbtools.lib.ShizukuRunner import com.legendsayantan.adbtools.lib.Utils.Companion.initialiseStatusBar +import com.legendsayantan.adbtools.services.SoundMasterService import java.util.UUID /** * @author legendsayantan @@ -92,6 +93,7 @@ class MainActivity : AppCompatActivity() { //create notification val intent = Intent(this, SoundMasterActivity::class.java) intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + SoundMasterService.uiIntent = intent val channelId = "notifications" val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId) .setSmallIcon(R.drawable.outline_info_24) diff --git a/app/src/main/java/com/legendsayantan/adbtools/SoundMasterActivity.kt b/app/src/main/java/com/legendsayantan/adbtools/SoundMasterActivity.kt index 454c68c..303dbe8 100644 --- a/app/src/main/java/com/legendsayantan/adbtools/SoundMasterActivity.kt +++ b/app/src/main/java/com/legendsayantan/adbtools/SoundMasterActivity.kt @@ -6,7 +6,7 @@ import android.content.Context import android.content.Intent import android.media.projection.MediaProjectionManager import android.os.Bundle -import android.view.Gravity +import android.os.Handler import android.view.View import android.view.WindowManager import android.widget.ImageView @@ -56,16 +56,20 @@ class SoundMasterActivity : AppCompatActivity() { @SuppressLint("ApplySharedPref") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + showing = true setContentView(R.layout.activity_sound_master) initialiseStatusBar() + interacted() //new slider findViewById(R.id.newSlider).setOnClickListener { + lastInteractionAt = -1 NewSliderDialog(this@SoundMasterActivity) { pkg -> val newPackages = packages newPackages.add(pkg) packages = newPackages if (SoundMasterService.running) SoundMasterService.onDynamicAttach(pkg) updateSliders() + interacted() }.show() } @@ -79,6 +83,18 @@ class SoundMasterActivity : AppCompatActivity() { findViewById(R.id.main).setOnClickListener { finish() } + + setupAutoHide() + } + + private fun setupAutoHide(){ + Timer().schedule(timerTask { + if (lastInteractionAt>=0 + && lastInteractionAt+hideTimerInterval < System.currentTimeMillis()) { + finish() + cancel() + } + }, hideTimerInterval,hideTimerInterval) } override fun onResume() { @@ -100,8 +116,7 @@ class SoundMasterActivity : AppCompatActivity() { applicationContext, "No apps selected to control", Toast.LENGTH_SHORT - ) - .show() + ).show() } else { ShizukuRunner.runAdbCommand("pm grant ${baseContext.packageName} android.permission.RECORD_AUDIO", object : ShizukuRunner.CommandResultListener { @@ -125,6 +140,12 @@ class SoundMasterActivity : AppCompatActivity() { }) } } + + override fun onCommandError(error: String) { + Handler(mainLooper).post { + Toast.makeText(applicationContext,"Shizuku Error", Toast.LENGTH_SHORT).show() + } + } }) } } @@ -136,6 +157,7 @@ class SoundMasterActivity : AppCompatActivity() { cancel() } else count++ if (count > 50) cancel() + interacted() }, 500, 500) } } @@ -153,16 +175,24 @@ class SoundMasterActivity : AppCompatActivity() { startService(Intent(this, SoundMasterService::class.java).apply { putExtra("packages", packages.toTypedArray()) }) + interacted() } else { Toast.makeText( this, "Request to obtain MediaProjection denied.", Toast.LENGTH_SHORT ).show() + interacted() } } } + override fun finish() { + showing = false + super.finish() + } + private fun updateSliders() { + interacted() findViewById(R.id.none).visibility = if (packages.size > 0) View.GONE else View.VISIBLE Thread { @@ -173,17 +203,21 @@ class SoundMasterActivity : AppCompatActivity() { } val adapter = VolumeBarAdapter(this@SoundMasterActivity, sliderMap, { app, vol -> + interacted() SoundMasterService.setVolumeOf(app, vol) }, { + interacted() val newPackages = packages newPackages.remove(it) packages = newPackages updateSliders() SoundMasterService.onDynamicDetach(it) }, { app, sliderIndex -> + interacted() if (sliderIndex == 0) SoundMasterService.getBalanceOf(app) else SoundMasterService.getBandValueOf(app, sliderIndex - 1) }, { app, slider, value -> + interacted() if (slider == 0) SoundMasterService.setBalanceOf(app, value) else SoundMasterService.setBandValueOf(app, slider - 1, value) }) @@ -195,6 +229,12 @@ class SoundMasterActivity : AppCompatActivity() { } companion object { + var showing = false + private const val hideTimerInterval = 2000L + var lastInteractionAt = 0L + var interacted = { + lastInteractionAt = System.currentTimeMillis() + } private const val FILENAME_SOUNDMASTER = "soundmaster.txt" private const val MEDIA_PROJECTION_REQUEST_CODE = 13 } diff --git a/app/src/main/java/com/legendsayantan/adbtools/lib/ShizukuRunner.kt b/app/src/main/java/com/legendsayantan/adbtools/lib/ShizukuRunner.kt index d5e170a..c0dbff3 100644 --- a/app/src/main/java/com/legendsayantan/adbtools/lib/ShizukuRunner.kt +++ b/app/src/main/java/com/legendsayantan/adbtools/lib/ShizukuRunner.kt @@ -10,27 +10,33 @@ import java.io.InputStreamReader class ShizukuRunner { interface CommandResultListener { fun onCommandResult(output: String, done:Boolean) + fun onCommandError(error: String){} } companion object { fun runAdbCommand(command: String, listener: CommandResultListener) { Thread { - val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, "/") - val reader = BufferedReader(InputStreamReader(process.inputStream)) - val output = StringBuilder() + try{ + val process = Shizuku.newProcess(arrayOf("sh", "-c", command), null, "/") + val reader = BufferedReader(InputStreamReader(process.inputStream)) + val output = StringBuilder() - var line: String? - var linecount = 0 - while (reader.readLine().also { line = it } != null) { - linecount++ - output.append(line).append("\n") - if (linecount == 50) { - linecount = 0 - listener.onCommandResult(output.toString(),false) + var line: String? + var linecount = 0 + while (reader.readLine().also { line = it } != null) { + linecount++ + output.append(line).append("\n") + if (linecount == 50) { + linecount = 0 + listener.onCommandResult(output.toString(),false) + } } + listener.onCommandResult(output.toString(),true) + process.waitFor() + }catch (e:Exception){ + listener.onCommandError(e.message?:"No Shizuku") } - listener.onCommandResult(output.toString(),true) - process.waitFor() + }.start() } } diff --git a/app/src/main/java/com/legendsayantan/adbtools/services/AudioThread.kt b/app/src/main/java/com/legendsayantan/adbtools/services/AudioThread.kt index bef1365..874a82b 100644 --- a/app/src/main/java/com/legendsayantan/adbtools/services/AudioThread.kt +++ b/app/src/main/java/com/legendsayantan/adbtools/services/AudioThread.kt @@ -14,7 +14,9 @@ import android.media.audiofx.LoudnessEnhancer import android.media.audiofx.NoiseSuppressor import android.media.projection.MediaProjection import android.os.Build +import android.os.Handler import android.util.Log +import android.widget.Toast import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat import com.legendsayantan.adbtools.lib.ShizukuRunner @@ -24,32 +26,48 @@ 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 +import com.legendsayantan.adbtools.services.SoundMasterService.Companion.notiUpdateTime +import com.legendsayantan.adbtools.services.SoundMasterService.Companion.zeroByte /** * @author legendsayantan */ -class AudioThread(val context: Context, val pkg:String, private val mediaProjection: MediaProjection) : Thread() { +class AudioThread( + val context: Context, + val pkg: String, + private val mediaProjection: MediaProjection +) : Thread("$LOG_TAG : $pkg") { var playback = true - var volume : Float = 1f - var targetVolume : Float = 100f + var volume: Float = 1f + var targetVolume: Float = 100f val dataBuffer = ByteArray(BUF_SIZE) - private var stereoGainFactor = arrayOf(1f,1f) + lateinit var monitorThread : Thread + var loadedCycles = 0 + var latencyUpdate:(Int)->Unit = {} + 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)} + 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) + 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) {} - }) + ShizukuRunner.runAdbCommand("appops set $pkg PLAY_AUDIO deny", + object : ShizukuRunner.CommandResultListener { + override fun onCommandResult(output: String, done: Boolean) {} + override fun onCommandError(error: String) { + Handler(context.mainLooper).post { + Toast.makeText(context,"Shizuku Error", Toast.LENGTH_SHORT).show() + } + } + }) super.start() } + @RequiresApi(Build.VERSION_CODES.Q) override fun run() { if (ActivityCompat.checkSelfPermission( @@ -60,7 +78,6 @@ class AudioThread(val context: Context, val pkg:String, private val mediaProject interrupt() return } - try { val config = AudioPlaybackCaptureConfiguration.Builder(mediaProjection) .addMatchingUsage(AudioAttributes.USAGE_MEDIA) @@ -119,13 +136,14 @@ class AudioThread(val context: Context, val pkg:String, private val mediaProject while (playback) { mRecord.read(dataBuffer, 0, BUF_SIZE) mTrack.write(dataBuffer, 0, dataBuffer.size) + loadedCycles++ } - }catch (e:Exception){ - Log.e(LOG_TAG,"Error in VolumeThread: ${e.message}") + } catch (e: Exception) { + Log.e(LOG_TAG, "Error in VolumeThread: ${e.message}") } } - fun setCurrentVolume(it:Float){ + fun setCurrentVolume(it: Float) { volume = (it / 100f).coerceAtMost(1f) mTrack.setStereoVolume(volume * stereoGainFactor[0], volume * stereoGainFactor[1]) try { @@ -150,7 +168,7 @@ class AudioThread(val context: Context, val pkg:String, private val mediaProject return (100 - (stereoGainFactor[0] * 100).toInt()) - (100 - (stereoGainFactor[1] * 100)) } - fun setBalance(value:Float){ + fun setBalance(value: Float) { stereoGainFactor = arrayOf( if (value <= 0) 1f else 1f - (value / 100f), if (value >= 0) 1f else 1f + (value / 100f) @@ -158,7 +176,7 @@ class AudioThread(val context: Context, val pkg:String, private val mediaProject mTrack.setStereoVolume(volume * stereoGainFactor[0], volume * stereoGainFactor[1]) } - fun setBand(band:Int,value:Float){ + fun setBand(band: Int, value: Float) { savedBands[band] = value updateBandLevel(band, value) } @@ -189,16 +207,30 @@ class AudioThread(val context: Context, val pkg:String, private val mediaProject } } + fun getLatency(): Float { + return notiUpdateTime.toFloat()/loadedCycles.coerceAtLeast(1).also { loadedCycles=0 } + } + override fun interrupt() { playback = false - ShizukuRunner.runAdbCommand("appops set $pkg PLAY_AUDIO allow",object : ShizukuRunner.CommandResultListener{ - override fun onCommandResult(output: String, done: Boolean) {} - }) + ShizukuRunner.runAdbCommand("appops set $pkg PLAY_AUDIO allow", + object : ShizukuRunner.CommandResultListener { + override fun onCommandResult(output: String, done: Boolean) {} + override fun onCommandError(error: String) { + Handler(context.mainLooper).post { + Toast.makeText(context,"Shizuku Error", Toast.LENGTH_SHORT).show() + } + } + }) mRecord.stop() mRecord.release() mTrack.stop() mTrack.release() + try{ + monitorThread.interrupt() + }catch (_:Exception){ + + } 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 index c96001d..a7b828f 100644 --- a/app/src/main/java/com/legendsayantan/adbtools/services/SoundMasterService.kt +++ b/app/src/main/java/com/legendsayantan/adbtools/services/SoundMasterService.kt @@ -1,24 +1,51 @@ package com.legendsayantan.adbtools.services +import android.Manifest import android.app.Activity import android.app.Service import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.content.pm.ServiceInfo +import android.database.ContentObserver import android.media.AudioFormat +import android.media.AudioManager import android.media.AudioRecord import android.media.projection.MediaProjection import android.media.projection.MediaProjectionManager import android.os.Build +import android.os.Handler import android.os.IBinder +import android.provider.Settings +import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.PackageManagerCompat import com.legendsayantan.adbtools.R +import com.legendsayantan.adbtools.SoundMasterActivity +import com.legendsayantan.adbtools.lib.ShizukuRunner +import java.lang.Byte +import java.util.Timer +import kotlin.Boolean +import kotlin.Float +import kotlin.Int +import kotlin.String +import kotlin.Unit +import kotlin.arrayOf +import kotlin.concurrent.timerTask +import kotlin.let + class SoundMasterService : Service() { + private lateinit var mVolumeObserver: ContentObserver + private val audioManager by lazy { getSystemService(Context.AUDIO_SERVICE) as AudioManager } private var mediaProjectionManager: MediaProjectionManager? = null private var mediaProjection: MediaProjection? = null var threadMap = hashMapOf() var apps = mutableListOf() + var latency = mutableListOf(0) + var latencyUpdateTimer = Timer() override fun onBind(intent: Intent): IBinder { return null!! } @@ -27,18 +54,38 @@ class SoundMasterService : Service() { 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 - ) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return } + + val builder = NotificationCompat.Builder(this, "notifications") + .setContentText("You can change volume to configure ${applicationContext.getString(R.string.soundmaster)} as well.") + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOnlyAlertOnce(true) + startForeground( + NOTI_ID, + builder.build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION + ) + latencyUpdateTimer.schedule(timerTask { + val avg = threadMap.values.map { it.getLatency() }.average().toInt() + threadMap.values.forEach { it.loadedCycles = 0 } + builder.setContentTitle(applicationContext.getString(R.string.soundmaster) + " is controlling ${apps.size} apps.") + builder.setContentText("Average Latency: $avg ms") + latency.clear() + if (ActivityCompat.checkSelfPermission( + applicationContext, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + NotificationManagerCompat.from(applicationContext) + .notify(NOTI_ID, builder.build()) + } + }, notiUpdateTime, notiUpdateTime) + + initVolumeBtnControl() + mediaProjectionManager = applicationContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager @@ -56,28 +103,32 @@ class SoundMasterService : Service() { threadMap[it]?.getBalance() ?: 0f } - setBalanceOf = {it,value-> + setBalanceOf = { it, value -> threadMap[it]?.setBalance(value) } - getBandValueOf = {it,band-> + getBandValueOf = { it, band -> threadMap[it]?.savedBands?.get(band) ?: 50f } - setBandValueOf = { it,band,value-> - threadMap[it]?.setBand(band,value) + setBandValueOf = { it, band, value -> + threadMap[it]?.setBand(band, value) } - onDynamicAttach = { - if (!apps.contains(it)) { - apps += it + onDynamicAttach = { it -> + if (!threadMap.contains(it)) { + if (!apps.contains(it)) apps.add(it) val mThread = AudioThread(applicationContext, it, mediaProjection!!) + mThread.targetVolume = volumeTemp[it] ?: 100f + mThread.latencyUpdate = { value -> + latency.add(value) + } threadMap[it] = mThread mThread.start() } } - onDynamicDetach = { pkg-> + onDynamicDetach = { pkg -> if (apps.contains(pkg)) { apps.remove(pkg) threadMap[pkg]?.interrupt() @@ -96,19 +147,40 @@ class SoundMasterService : Service() { projectionData!! ) as MediaProjection apps.forEach { - val mThread = AudioThread(applicationContext, it, mediaProjection!!).apply { - targetVolume = volumeTemp[it]?:100f - } - threadMap[it] = mThread - mThread.start() + onDynamicAttach(it) } } } return START_STICKY } + private fun initVolumeBtnControl() { + mVolumeObserver = object : ContentObserver(Handler(mainLooper)) { + var prevVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + override fun onChange(selfChange: Boolean) { + super.onChange(selfChange) + val newVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + if(newVolume != prevVolume){ + prevVolume = newVolume + Handler(mainLooper).post { + if(SoundMasterActivity.showing) SoundMasterActivity.interacted() + else ShizukuRunner.runAdbCommand("am start -n $packageName/${SoundMasterActivity::class.java.canonicalName}",object : ShizukuRunner.CommandResultListener{ + override fun onCommandResult(output: String, done: Boolean) {} + }) + } + } + } + } + contentResolver.registerContentObserver( + Settings.System.CONTENT_URI, true, + mVolumeObserver + ) + } + override fun onDestroy() { running = false + contentResolver.unregisterContentObserver(mVolumeObserver) + latencyUpdateTimer.cancel() threadMap.forEach { it.value.interrupt() } mediaProjection?.stop() super.onDestroy() @@ -121,20 +193,23 @@ class SoundMasterService : Service() { 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} - + 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 NOTI_ID = 1 + const val notiUpdateTime = 30000L const val SAMPLE_RATE = 44100 const val LOG_TAG = "SoundMaster" const val CHANNEL = AudioFormat.CHANNEL_IN_STEREO val BUF_SIZE = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL, AudioFormat.ENCODING_PCM_16BIT) + val zeroByte = Byte.valueOf(0) val bandDivision = arrayOf(0, 250, 2000, 20000) + lateinit var uiIntent: Intent } - } \ 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 bc7ec78..8cec1da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - + ShizuTools This app requires Shizuku.\nPlease make sure you have shizuku installed and running. Force applying MixedAudio may crash some apps, and doing so on system apps may crash your entire system. @@ -37,4 +37,5 @@ Low Bands Mid Bands High Bands + getevent -q \ No newline at end of file